Coverage for britney2/policies/policy.py: 92%

1378 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-07-30 07:06 +0000

1import json 

2import logging 

3import optparse 

4import os 

5import re 

6import sys 

7import time 

8from abc import ABC, abstractmethod 

9from collections import defaultdict 

10from collections.abc import Callable, Container, Iterator 

11from dataclasses import dataclass, field 

12from enum import Enum, IntEnum, StrEnum, auto, unique 

13from itertools import chain 

14from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar, cast 

15from urllib.parse import quote 

16 

17import apt_pkg 

18import yaml 

19 

20from britney2 import ( 

21 BinaryPackage, 

22 BinaryPackageId, 

23 DependencyType, 

24 PackageId, 

25 SourcePackage, 

26 Suite, 

27 SuiteClass, 

28 Suites, 

29 TargetSuite, 

30) 

31from britney2.excusedeps import DependencySpec 

32from britney2.hints import ( 

33 Hint, 

34 HintAnnotate, 

35 HintCollection, 

36 HintParser, 

37 HintType, 

38 PolicyHintParserProto, 

39) 

40from britney2.inputs.suiteloader import SuiteContentLoader 

41from britney2.migrationitem import MigrationItem, MigrationItemFactory 

42from britney2.policies import ApplySrcPolicy, PolicyVerdict 

43from britney2.utils import ( 

44 GetDependencySolversProto, 

45 binaries_from_source_version, 

46 compute_reverse_tree, 

47 filter_out_faux, 

48 filter_out_faux_gen, 

49 find_newer_binaries, 

50 get_component, 

51 get_dependency_solvers, 

52 is_smooth_update_allowed, 

53 parse_option, 

54) 

55 

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

57 from ..britney import Britney 

58 from ..excuse import Excuse 

59 from ..installability.universe import BinaryPackageUniverse 

60 

61 

62class PolicyLoadRequest: 

63 __slots__ = ("_options_name", "_default_value", "_policy_constructor") 

64 

65 def __init__( 

66 self, 

67 policy_constructor: Callable[[optparse.Values, Suites], "BasePolicy"], 

68 options_name: str | None, 

69 default_value: bool, 

70 ) -> None: 

71 self._policy_constructor = policy_constructor 

72 self._options_name = options_name 

73 self._default_value = default_value 

74 

75 def is_enabled(self, options: optparse.Values) -> bool: 

76 if self._options_name is None: 

77 assert self._default_value 

78 return True 

79 actual_value = getattr(options, self._options_name, None) 

80 if actual_value is None: 

81 return self._default_value 

82 return actual_value.lower() in ("yes", "y", "true", "t") 

83 

84 def load(self, options: optparse.Values, suite_info: Suites) -> "BasePolicy": 

85 return self._policy_constructor(options, suite_info) 

86 

87 @classmethod 

88 def always_load( 

89 cls, policy_constructor: Callable[[optparse.Values, Suites], "BasePolicy"] 

90 ) -> "PolicyLoadRequest": 

91 return cls(policy_constructor, None, True) 

92 

93 @classmethod 

94 def conditionally_load( 

95 cls, 

96 policy_constructor: Callable[[optparse.Values, Suites], "BasePolicy"], 

97 option_name: str, 

98 default_value: bool, 

99 ) -> "PolicyLoadRequest": 

100 return cls(policy_constructor, option_name, default_value) 

101 

102 

103class PolicyEngine: 

104 def __init__(self) -> None: 

105 self._policies: list["BasePolicy"] = [] 

106 

107 def add_policy(self, policy: "BasePolicy") -> None: 

108 self._policies.append(policy) 

109 

110 def load_policies( 

111 self, 

112 options: optparse.Values, 

113 suite_info: Suites, 

114 policy_load_requests: list[PolicyLoadRequest], 

115 ) -> None: 

116 for policy_load_request in policy_load_requests: 

117 if policy_load_request.is_enabled(options): 

118 self.add_policy(policy_load_request.load(options, suite_info)) 

119 

120 def register_policy_hints(self, hint_parser: HintParser) -> None: 

121 for policy in self._policies: 

122 policy.register_hints(hint_parser) 

123 

124 def initialise(self, britney: "Britney", hints: HintCollection) -> None: 

125 for policy in self._policies: 

126 policy.hints = hints 

127 policy.initialise(britney) 

128 

129 def save_state(self, britney: "Britney") -> None: 

130 for policy in self._policies: 

131 policy.save_state(britney) 

132 

133 def apply_src_policies( 

134 self, 

135 source_t: SourcePackage | None, 

136 source_u: SourcePackage, 

137 excuse: "Excuse", 

138 ) -> None: 

139 excuse_verdict = excuse.policy_verdict 

140 source_suite = excuse.item.suite 

141 suite_class = source_suite.suite_class 

142 for policy in self._policies: 

143 pinfo: dict[str, Any] = {} 

144 policy_verdict = PolicyVerdict.NOT_APPLICABLE 

145 if suite_class in policy.applicable_suites: 

146 if policy.src_policy.run_arch: 

147 for arch in policy.options.architectures: 

148 v = policy.apply_srcarch_policy_impl( 

149 pinfo, arch, source_t, source_u, excuse 

150 ) 

151 policy_verdict = PolicyVerdict.worst_of(policy_verdict, v) 

152 if policy.src_policy.run_src: 

153 v = policy.apply_src_policy_impl(pinfo, source_t, source_u, excuse) 

154 policy_verdict = PolicyVerdict.worst_of(policy_verdict, v) 

155 # The base policy provides this field, so the subclass should leave it blank 

156 assert "verdict" not in pinfo 

157 if policy_verdict is not PolicyVerdict.NOT_APPLICABLE: 

158 excuse.policy_info[policy.policy_id] = pinfo 

159 pinfo["verdict"] = policy_verdict.name 

160 excuse_verdict = PolicyVerdict.worst_of(policy_verdict, excuse_verdict) 

161 excuse.policy_verdict = excuse_verdict 

162 

163 def apply_srcarch_policies( 

164 self, 

165 arch: str, 

166 source_t: SourcePackage | None, 

167 source_u: SourcePackage, 

168 excuse: "Excuse", 

169 ) -> None: 

170 excuse_verdict = excuse.policy_verdict 

171 source_suite = excuse.item.suite 

172 suite_class = source_suite.suite_class 

173 for policy in self._policies: 

174 pinfo: dict[str, Any] = {} 

175 if suite_class in policy.applicable_suites: 

176 policy_verdict = policy.apply_srcarch_policy_impl( 

177 pinfo, arch, source_t, source_u, excuse 

178 ) 

179 excuse_verdict = PolicyVerdict.worst_of(policy_verdict, excuse_verdict) 

180 # The base policy provides this field, so the subclass should leave it blank 

181 assert "verdict" not in pinfo 

182 if policy_verdict is not PolicyVerdict.NOT_APPLICABLE: 

183 excuse.policy_info[policy.policy_id] = pinfo 

184 pinfo["verdict"] = policy_verdict.name 

185 excuse.policy_verdict = excuse_verdict 

186 

187 

188class BasePolicy(ABC): 

189 britney: "Britney" 

190 policy_id: str 

191 hints: HintCollection | None 

192 applicable_suites: set[SuiteClass] 

193 src_policy: ApplySrcPolicy 

194 options: optparse.Values 

195 suite_info: Suites 

196 

197 def __init__( 

198 self, 

199 options: optparse.Values, 

200 suite_info: Suites, 

201 ) -> None: 

202 """The BasePolicy constructor 

203 

204 :param options: The options member of Britney with all the 

205 config values. 

206 """ 

207 

208 @property 

209 @abstractmethod 

210 def state_dir(self) -> str: ... 210 ↛ exitline 210 didn't return from function 'state_dir' because

211 

212 def register_hints(self, hint_parser: HintParser) -> None: # pragma: no cover 

213 """Register new hints that this policy accepts 

214 

215 :param hint_parser: (see HintParser.register_hint_type) 

216 """ 

217 

218 def initialise(self, britney: "Britney") -> None: # pragma: no cover 

219 """Called once to make the policy initialise any data structures 

220 

221 This is useful for e.g. parsing files or other "heavy do-once" work. 

222 

223 :param britney: This is the instance of the "Britney" class. 

224 """ 

225 self.britney = britney 

226 

227 def save_state(self, britney: "Britney") -> None: # pragma: no cover 

228 """Called once at the end of the run to make the policy save any persistent data 

229 

230 Note this will *not* be called for "dry-runs" as such runs should not change 

231 the state. 

232 

233 :param britney: This is the instance of the "Britney" class. 

234 """ 

235 

236 def apply_src_policy_impl( 

237 self, 

238 policy_info: dict[str, Any], 

239 source_data_tdist: SourcePackage | None, 

240 source_data_srcdist: SourcePackage, 

241 excuse: "Excuse", 

242 ) -> PolicyVerdict: # pragma: no cover 

243 """Apply a policy on a given source migration 

244 

245 Britney will call this method on a given source package, when 

246 Britney is considering to migrate it from the given source 

247 suite to the target suite. The policy will then evaluate the 

248 the migration and then return a verdict. 

249 

250 :param policy_info: A dictionary of all policy results. The 

251 policy can add a value stored in a key related to its name. 

252 (e.g. policy_info['age'] = {...}). This will go directly into 

253 the "excuses.yaml" output. 

254 

255 :param source_data_tdist: Information about the source package 

256 in the target distribution (e.g. "testing"). This is the 

257 data structure in source_suite.sources[source_name] 

258 

259 :param source_data_srcdist: Information about the source 

260 package in the source distribution (e.g. "unstable" or "tpu"). 

261 This is the data structure in target_suite.sources[source_name] 

262 

263 :return: A Policy Verdict (e.g. PolicyVerdict.PASS) 

264 """ 

265 return PolicyVerdict.NOT_APPLICABLE 

266 

267 def apply_srcarch_policy_impl( 

268 self, 

269 policy_info: dict[str, Any], 

270 arch: str, 

271 source_data_tdist: SourcePackage | None, 

272 source_data_srcdist: SourcePackage, 

273 excuse: "Excuse", 

274 ) -> PolicyVerdict: 

275 """Apply a policy on a given binary migration 

276 

277 Britney will call this method on binaries from a given source package 

278 on a given architecture, when Britney is considering to migrate them 

279 from the given source suite to the target suite. The policy will then 

280 evaluate the migration and then return a verdict. 

281 

282 :param policy_info: A dictionary of all policy results. The 

283 policy can add a value stored in a key related to its name. 

284 (e.g. policy_info['age'] = {...}). This will go directly into 

285 the "excuses.yaml" output. 

286 

287 :param arch: The architecture the item is applied to. This is mostly 

288 relevant for policies where src_policy is not ApplySrcPolicy.RUN_SRC 

289 (as that is the only case where arch can differ from item.architecture) 

290 

291 :param source_data_tdist: Information about the source package 

292 in the target distribution (e.g. "testing"). This is the 

293 data structure in source_suite.sources[source_name] 

294 

295 :param source_data_srcdist: Information about the source 

296 package in the source distribution (e.g. "unstable" or "tpu"). 

297 This is the data structure in target_suite.sources[source_name] 

298 

299 :return: A Policy Verdict (e.g. PolicyVerdict.PASS) 

300 """ 

301 # if the policy doesn't implement this function, assume it's OK 

302 return PolicyVerdict.NOT_APPLICABLE 

303 

304 

305class AbstractBasePolicy(BasePolicy): 

306 """ 

307 A shared abstract class for building BasePolicy objects. 

308 

309 tests/test_policy.py:initialize_policy() needs to be able to build BasePolicy 

310 objects with just a two-item constructor, while all other uses of BasePolicy- 

311 derived objects need the 5-item constructor. So AbstractBasePolicy was split 

312 out to document this. 

313 """ 

314 

315 def __init__( 

316 self, 

317 policy_id: str, 

318 options: optparse.Values, 

319 suite_info: Suites, 

320 applicable_suites: set[SuiteClass], 

321 src_policy: ApplySrcPolicy = ApplySrcPolicy.RUN_SRC, 

322 ) -> None: 

323 """Concrete initializer. 

324 

325 :param policy_id: Identifies the policy. It will 

326 determine the key used for the excuses.yaml etc. 

327 

328 :param options: The options member of Britney with all the 

329 config values. 

330 

331 :param applicable_suites: Where this policy applies. 

332 """ 

333 self.policy_id = policy_id 

334 self.options = options 

335 self.suite_info = suite_info 

336 self.applicable_suites = applicable_suites 

337 self.src_policy = src_policy 

338 self.hints: HintCollection | None = None 

339 logger_name = ".".join((self.__class__.__module__, self.__class__.__name__)) 

340 self.logger = logging.getLogger(logger_name) 

341 

342 @property 

343 def state_dir(self) -> str: 

344 return cast(str, self.options.state_dir) 

345 

346 

347_T = TypeVar("_T") 

348 

349 

350class SimplePolicyHint(Hint, Generic[_T]): 

351 def __init__( 

352 self, 

353 user: str, 

354 hint_type: HintType, 

355 policy_parameter: _T, 

356 packages: list[MigrationItem], 

357 ) -> None: 

358 super().__init__(user, hint_type, packages) 

359 self._policy_parameter = policy_parameter 

360 

361 def __eq__(self, other: Any) -> bool: 

362 if self.type != other.type or self._policy_parameter != other._policy_parameter: 

363 return False 

364 return super().__eq__(other) 

365 

366 def str(self) -> str: 

367 return "{} {} {}".format( 

368 self._type, 

369 str(self._policy_parameter), 

370 " ".join(x.name for x in self._packages), 

371 ) 

372 

373 

374class AgeDayHint(SimplePolicyHint[int]): 

375 @property 

376 def days(self) -> int: 

377 return self._policy_parameter 

378 

379 

380class IgnoreRCBugHint(SimplePolicyHint[frozenset[str]]): 

381 @property 

382 def ignored_rcbugs(self) -> frozenset[str]: 

383 return self._policy_parameter 

384 

385 

386def simple_policy_hint_parser_function( 

387 class_name: Callable[[str, HintType, _T, list[MigrationItem]], Hint], 

388 converter: Callable[[str], _T], 

389) -> PolicyHintParserProto: 

390 def f( 

391 mi_factory: MigrationItemFactory, 

392 hints: HintCollection, 

393 who: str, 

394 hint_type: HintType, 

395 *args: str, 

396 ) -> None: 

397 policy_parameter = args[0] 

398 args = args[1:] 

399 for item in mi_factory.parse_items(*args): 

400 hints.add_hint( 

401 class_name(who, hint_type, converter(policy_parameter), [item]) 

402 ) 

403 

404 return f 

405 

406 

407class AgePolicy(AbstractBasePolicy): 

408 """Configurable Aging policy for source migrations 

409 

410 The AgePolicy will let packages stay in the source suite for a pre-defined 

411 amount of days before letting migrate (based on their urgency, if any). 

412 

413 The AgePolicy's decision is influenced by the following: 

414 

415 State files: 

416 * ${STATE_DIR}/age-policy-urgencies: File containing urgencies for source 

417 packages. Note that urgencies are "sticky" and the most "urgent" urgency 

418 will be used (i.e. the one with lowest age-requirements). 

419 - This file needs to be updated externally, if the policy should take 

420 urgencies into consideration. If empty (or not updated), the policy 

421 will simply use the default urgency (see the "Config" section below) 

422 - In Debian, these values are taken from the .changes file, but that is 

423 not a requirement for Britney. 

424 * ${STATE_DIR}/age-policy-dates: File containing the age of all source 

425 packages. 

426 - The policy will automatically update this file. 

427 Config: 

428 * DEFAULT_URGENCY: Name of the urgency used for packages without an urgency 

429 (or for unknown urgencies). Will also be used to set the "minimum" 

430 aging requirements for packages not in the target suite. 

431 * MINDAYS_<URGENCY>: The age-requirements in days for packages with the 

432 given urgency. 

433 - Commonly used urgencies are: low, medium, high, emergency, critical 

434 Hints: 

435 * urgent <source>/<version>: Disregard the age requirements for a given 

436 source/version. 

437 * age-days X <source>/<version>: Set the age requirements for a given 

438 source/version to X days. Note that X can exceed the highest 

439 age-requirement normally given. 

440 

441 """ 

442 

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

444 super().__init__("age", options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE}) 

445 self._min_days = self._generate_mindays_table() 

446 self._min_days_default = 0 

447 # britney's "day" begins at 7pm (we want aging to occur in the 22:00Z run and we run Britney 2-4 times a day) 

448 # NB: _date_now is used in tests 

449 time_now = time.time() 

450 if hasattr(self.options, "fake_runtime"): 

451 time_now = int(self.options.fake_runtime) 

452 self.logger.info("overriding runtime with fake_runtime %d", time_now) 

453 

454 self._date_now = int(((time_now / (60 * 60)) - 19) / 24) 

455 self._dates: dict[str, tuple[str, int]] = {} 

456 self._urgencies: dict[str, str] = {} 

457 self._default_urgency: str = self.options.default_urgency 

458 self._penalty_immune_urgencies: frozenset[str] = frozenset() 

459 if hasattr(self.options, "no_penalties"): 

460 self._penalty_immune_urgencies = frozenset( 

461 x.strip() for x in self.options.no_penalties.split() 

462 ) 

463 self._bounty_min_age: int | None = None # initialised later 

464 

465 def _generate_mindays_table(self) -> dict[str, int]: 

466 mindays: dict[str, int] = {} 

467 for k in dir(self.options): 

468 if not k.startswith("mindays_"): 

469 continue 

470 v = getattr(self.options, k) 

471 try: 

472 as_days = int(v) 

473 except ValueError: 

474 raise ValueError( 

475 "Unable to parse " 

476 + k 

477 + " as a number of days. Must be 0 or a positive integer" 

478 ) 

479 if as_days < 0: 479 ↛ 480line 479 didn't jump to line 480 because the condition on line 479 was never true

480 raise ValueError( 

481 "The value of " + k + " must be zero or a positive integer" 

482 ) 

483 mindays[k.split("_")[1]] = as_days 

484 return mindays 

485 

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

487 hint_parser.register_hint_type( 

488 HintType( 

489 "age-days", 

490 simple_policy_hint_parser_function(AgeDayHint, int), 

491 min_args=2, 

492 ) 

493 ) 

494 hint_parser.register_hint_type(HintType("urgent")) 

495 

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

497 super().initialise(britney) 

498 self._read_dates_file() 

499 self._read_urgencies_file() 

500 if self._default_urgency not in self._min_days: # pragma: no cover 

501 raise ValueError( 

502 f"Missing age-requirement for default urgency (MINDAYS_{self._default_urgency})" 

503 ) 

504 self._min_days_default = self._min_days[self._default_urgency] 

505 try: 

506 self._bounty_min_age = int(self.options.bounty_min_age) 

507 except ValueError: 507 ↛ 508line 507 didn't jump to line 508 because the exception caught by line 507 didn't happen

508 if self.options.bounty_min_age in self._min_days: 

509 self._bounty_min_age = self._min_days[self.options.bounty_min_age] 

510 else: # pragma: no cover 

511 raise ValueError( 

512 "Please fix BOUNTY_MIN_AGE in the britney configuration" 

513 ) 

514 except AttributeError: 

515 # The option wasn't defined in the configuration 

516 self._bounty_min_age = 0 

517 

518 def save_state(self, britney: "Britney") -> None: 

519 super().save_state(britney) 

520 self._write_dates_file() 

521 

522 def apply_src_policy_impl( 

523 self, 

524 age_info: dict[str, Any], 

525 source_data_tdist: SourcePackage | None, 

526 source_data_srcdist: SourcePackage, 

527 excuse: "Excuse", 

528 ) -> PolicyVerdict: 

529 # retrieve the urgency for the upload, ignoring it if this is a NEW package 

530 # (not present in the target suite) 

531 source_name = excuse.item.package 

532 urgency = self._urgencies.get(source_name, self._default_urgency) 

533 

534 if urgency not in self._min_days: 534 ↛ 535line 534 didn't jump to line 535 because the condition on line 534 was never true

535 age_info["unknown-urgency"] = urgency 

536 urgency = self._default_urgency 

537 

538 if not source_data_tdist: 

539 if self._min_days[urgency] < self._min_days_default: 

540 age_info["urgency-reduced"] = { 

541 "from": urgency, 

542 "to": self._default_urgency, 

543 } 

544 urgency = self._default_urgency 

545 

546 if source_name not in self._dates: 

547 self._dates[source_name] = (source_data_srcdist.version, self._date_now) 

548 elif self._dates[source_name][0] != source_data_srcdist.version: 

549 self._dates[source_name] = (source_data_srcdist.version, self._date_now) 

550 

551 days_old = self._date_now - self._dates[source_name][1] 

552 min_days = self._min_days[urgency] 

553 for bounty, bounty_value in excuse.bounty.items(): 

554 if bounty_value: 554 ↛ 553line 554 didn't jump to line 553 because the condition on line 554 was always true

555 self.logger.info( 

556 "Applying bounty for %s granted by %s: %d days", 

557 source_name, 

558 bounty, 

559 bounty_value, 

560 ) 

561 excuse.addinfo( 

562 f"Required age reduced by {bounty_value} days because of {bounty}" 

563 ) 

564 assert bounty_value > 0, "negative bounties shouldn't happen" 

565 min_days -= bounty_value 

566 if urgency not in self._penalty_immune_urgencies: 

567 for penalty, penalty_value in excuse.penalty.items(): 

568 if penalty_value: 568 ↛ 567line 568 didn't jump to line 567 because the condition on line 568 was always true

569 self.logger.info( 

570 "Applying penalty for %s given by %s: %d days", 

571 source_name, 

572 penalty, 

573 penalty_value, 

574 ) 

575 excuse.addinfo( 

576 f"Required age increased by {penalty_value} days because of {penalty}" 

577 ) 

578 assert ( 

579 penalty_value > 0 

580 ), "negative penalties should be handled earlier" 

581 min_days += penalty_value 

582 

583 assert self._bounty_min_age is not None 

584 # the age in BOUNTY_MIN_AGE can be higher than the one associated with 

585 # the real urgency, so don't forget to take it into account 

586 bounty_min_age = min(self._bounty_min_age, self._min_days[urgency]) 

587 if min_days < bounty_min_age: 

588 min_days = bounty_min_age 

589 excuse.addinfo(f"Required age is not allowed to drop below {min_days} days") 

590 

591 age_info["current-age"] = days_old 

592 

593 assert self.hints is not None 

594 for hint in self.hints.search( 

595 "age-days", package=source_name, version=source_data_srcdist.version 

596 ): 

597 age_days_hint = cast("AgeDayHint", hint) 

598 

599 new_req = age_days_hint.days 

600 age_info["age-requirement-reduced"] = { 

601 "new-requirement": new_req, 

602 "changed-by": age_days_hint.user, 

603 } 

604 if "original-age-requirement" not in age_info: 604 ↛ 606line 604 didn't jump to line 606 because the condition on line 604 was always true

605 age_info["original-age-requirement"] = min_days 

606 min_days = new_req 

607 

608 age_info["age-requirement"] = min_days 

609 res = PolicyVerdict.PASS 

610 

611 if days_old < min_days: 

612 if ( 

613 urgent_hint := self.hints.search_first( 

614 "urgent", package=source_name, version=source_data_srcdist.version 

615 ) 

616 ) is not None: 

617 age_info["age-requirement-reduced"] = { 

618 "new-requirement": 0, 

619 "changed-by": urgent_hint.user, 

620 } 

621 res = PolicyVerdict.PASS_HINTED 

622 else: 

623 res = PolicyVerdict.REJECTED_TEMPORARILY 

624 

625 # update excuse 

626 age_hint = age_info.get("age-requirement-reduced", None) 

627 age_min_req = age_info["age-requirement"] 

628 if age_hint is not None: 

629 new_req = age_hint["new-requirement"] 

630 who = age_hint["changed-by"] 

631 if new_req: 

632 excuse.addinfo( 

633 f"Overriding age needed from {age_min_req} days to {new_req} by {who}" 

634 ) 

635 age_min_req = new_req 

636 else: 

637 excuse.addinfo(f"Too young, but urgency pushed by {who}") 

638 age_min_req = 0 

639 excuse.setdaysold(age_info["current-age"], age_min_req) 

640 

641 if age_min_req == 0: 

642 excuse.addinfo(f"{days_old} days old") 

643 elif days_old < age_min_req: 

644 excuse.add_verdict_info( 

645 res, f"Too young, only {days_old} of {age_min_req} days old" 

646 ) 

647 else: 

648 excuse.addinfo(f"{days_old} days old (needed {age_min_req} days)") 

649 

650 return res 

651 

652 def _read_dates_file(self) -> None: 

653 """Parse the dates file""" 

654 dates = self._dates 

655 fallback_filename = os.path.join(self.suite_info.target_suite.path, "Dates") 

656 using_new_name = False 

657 try: 

658 filename = os.path.join(self.state_dir, "age-policy-dates") 

659 if not os.path.exists(filename) and os.path.exists(fallback_filename): 659 ↛ 660line 659 didn't jump to line 660 because the condition on line 659 was never true

660 filename = fallback_filename 

661 else: 

662 using_new_name = True 

663 except AttributeError: 

664 if os.path.exists(fallback_filename): 

665 filename = fallback_filename 

666 else: 

667 raise RuntimeError("Please set STATE_DIR in the britney configuration") 

668 

669 try: 

670 with open(filename, encoding="utf-8") as fd: 

671 for line in fd: 

672 if line.startswith("#"): 

673 # Ignore comment lines (mostly used for tests) 

674 continue 

675 # <source> <version> <date>) 

676 ln = line.split() 

677 if len(ln) != 3: # pragma: no cover 

678 continue 

679 try: 

680 dates[ln[0]] = (ln[1], int(ln[2])) 

681 except ValueError: # pragma: no cover 

682 pass 

683 except FileNotFoundError: 

684 if not using_new_name: 684 ↛ 686line 684 didn't jump to line 686 because the condition on line 684 was never true

685 # If we using the legacy name, then just give up 

686 raise 

687 self.logger.info("%s does not appear to exist. Creating it", filename) 

688 with open(filename, mode="x", encoding="utf-8"): 

689 pass 

690 

691 def _read_urgencies_file(self) -> None: 

692 urgencies = self._urgencies 

693 min_days_default = self._min_days_default 

694 fallback_filename = os.path.join(self.suite_info.target_suite.path, "Urgency") 

695 try: 

696 filename = os.path.join(self.state_dir, "age-policy-urgencies") 

697 if not os.path.exists(filename) and os.path.exists(fallback_filename): 697 ↛ 698line 697 didn't jump to line 698 because the condition on line 697 was never true

698 filename = fallback_filename 

699 except AttributeError: 

700 filename = fallback_filename 

701 

702 sources_s = self.suite_info.primary_source_suite.sources 

703 sources_t = self.suite_info.target_suite.sources 

704 

705 with open(filename, errors="surrogateescape", encoding="ascii") as fd: 

706 for line in fd: 

707 if line.startswith("#"): 

708 # Ignore comment lines (mostly used for tests) 

709 continue 

710 # <source> <version> <urgency> 

711 ln = line.split() 

712 if len(ln) != 3: 712 ↛ 713line 712 didn't jump to line 713 because the condition on line 712 was never true

713 continue 

714 

715 # read the minimum days associated with the urgencies 

716 urgency_old = urgencies.get(ln[0], None) 

717 mindays_old = self._min_days.get(urgency_old, 1000) # type: ignore[arg-type] 

718 mindays_new = self._min_days.get(ln[2], min_days_default) 

719 

720 # if the new urgency is lower (so the min days are higher), do nothing 

721 if mindays_old <= mindays_new: 

722 continue 

723 

724 # if the package exists in the target suite and it is more recent, do nothing 

725 tsrcv = sources_t.get(ln[0], None) 

726 if tsrcv and apt_pkg.version_compare(tsrcv.version, ln[1]) >= 0: 

727 continue 

728 

729 # if the package doesn't exist in the primary source suite or it is older, do nothing 

730 usrcv = sources_s.get(ln[0], None) 

731 if not usrcv or apt_pkg.version_compare(usrcv.version, ln[1]) < 0: 731 ↛ 732line 731 didn't jump to line 732 because the condition on line 731 was never true

732 continue 

733 

734 # update the urgency for the package 

735 urgencies[ln[0]] = ln[2] 

736 

737 def _write_dates_file(self) -> None: 

738 dates = self._dates 

739 try: 

740 directory = self.state_dir 

741 basename = "age-policy-dates" 

742 old_file = os.path.join(self.suite_info.target_suite.path, "Dates") 

743 except AttributeError: 

744 directory = self.suite_info.target_suite.path 

745 basename = "Dates" 

746 old_file = None 

747 filename = os.path.join(directory, basename) 

748 filename_tmp = os.path.join(directory, f"{basename}_new") 

749 with open(filename_tmp, "w", encoding="utf-8") as fd: 

750 fd.writelines( 

751 f"{pkg} {version} {date}\n" 

752 for pkg, (version, date) in sorted(dates.items()) 

753 ) 

754 os.rename(filename_tmp, filename) 

755 if old_file is not None and os.path.exists(old_file): 755 ↛ 756line 755 didn't jump to line 756 because the condition on line 755 was never true

756 self.logger.info("Removing old age-policy-dates file %s", old_file) 

757 os.unlink(old_file) 

758 

759 

760class RCBugPolicy(AbstractBasePolicy): 

761 """RC bug regression policy for source migrations 

762 

763 The RCBugPolicy will read provided list of RC bugs and block any 

764 source upload that would introduce a *new* RC bug in the target 

765 suite. 

766 

767 The RCBugPolicy's decision is influenced by the following: 

768 

769 State files: 

770 * ${STATE_DIR}/rc-bugs-${SUITE_NAME}: File containing RC bugs for packages in 

771 the given suite (one for both primary source suite and the target sutie is 

772 needed). 

773 - These files need to be updated externally. 

774 """ 

775 

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

777 super().__init__( 

778 "rc-bugs", options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE} 

779 ) 

780 self._bugs_source: dict[str, set[str]] | None = None 

781 self._bugs_target: dict[str, set[str]] | None = None 

782 

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

784 f = simple_policy_hint_parser_function( 

785 IgnoreRCBugHint, lambda x: frozenset(x.split(",")) 

786 ) 

787 hint_parser.register_hint_type(HintType("ignore-rc-bugs", f, min_args=2)) 

788 

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

790 super().initialise(britney) 

791 source_suite = self.suite_info.primary_source_suite 

792 target_suite = self.suite_info.target_suite 

793 fallback_unstable = os.path.join(source_suite.path, "BugsV") 

794 fallback_testing = os.path.join(target_suite.path, "BugsV") 

795 try: 

796 filename_unstable = os.path.join( 

797 self.state_dir, f"rc-bugs-{source_suite.name}" 

798 ) 

799 filename_testing = os.path.join( 

800 self.state_dir, f"rc-bugs-{target_suite.name}" 

801 ) 

802 if ( 802 ↛ 808line 802 didn't jump to line 808

803 not os.path.exists(filename_unstable) 

804 and not os.path.exists(filename_testing) 

805 and os.path.exists(fallback_unstable) 

806 and os.path.exists(fallback_testing) 

807 ): 

808 filename_unstable = fallback_unstable 

809 filename_testing = fallback_testing 

810 except AttributeError: 

811 filename_unstable = fallback_unstable 

812 filename_testing = fallback_testing 

813 self._bugs_source = self._read_bugs(filename_unstable) 

814 self._bugs_target = self._read_bugs(filename_testing) 

815 

816 def apply_src_policy_impl( 

817 self, 

818 rcbugs_info: dict[str, Any], 

819 source_data_tdist: SourcePackage | None, 

820 source_data_srcdist: SourcePackage, 

821 excuse: "Excuse", 

822 ) -> PolicyVerdict: 

823 assert self._bugs_source is not None # for type checking 

824 assert self._bugs_target is not None # for type checking 

825 bugs_t = set() 

826 bugs_s = set() 

827 source_name = excuse.item.package 

828 binaries_s = {x.package_name for x in source_data_srcdist.binaries} 

829 try: 

830 binaries_t = {x.package_name for x in source_data_tdist.binaries} # type: ignore[union-attr] 

831 except AttributeError: 

832 binaries_t = set() 

833 

834 src_key = f"src:{source_name}" 

835 if source_data_tdist and src_key in self._bugs_target: 

836 bugs_t.update(self._bugs_target[src_key]) 

837 if src_key in self._bugs_source: 

838 bugs_s.update(self._bugs_source[src_key]) 

839 

840 for pkg in binaries_s: 

841 if pkg in self._bugs_source: 

842 bugs_s |= self._bugs_source[pkg] 

843 for pkg in binaries_t: 

844 if pkg in self._bugs_target: 

845 bugs_t |= self._bugs_target[pkg] 

846 

847 # The bts seems to support filing source bugs against a binary of the 

848 # same name if that binary isn't built by any source. An example is bug 

849 # 820347 against Package: juce (in the live-2016-04-11 test). Add those 

850 # bugs too. 

851 if ( 

852 source_name not in (binaries_s | binaries_t) 

853 and source_name 

854 not in { 

855 x.package_name 

856 for x in self.suite_info.primary_source_suite.all_binaries_in_suite.keys() 

857 } 

858 and source_name 

859 not in { 

860 x.package_name 

861 for x in self.suite_info.target_suite.all_binaries_in_suite.keys() 

862 } 

863 ): 

864 if source_name in self._bugs_source: 

865 bugs_s |= self._bugs_source[source_name] 

866 if source_name in self._bugs_target: 866 ↛ 867line 866 didn't jump to line 867 because the condition on line 866 was never true

867 bugs_t |= self._bugs_target[source_name] 

868 

869 # If a package is not in the target suite, it has no RC bugs per 

870 # definition. Unfortunately, it seems that the live-data is 

871 # not always accurate (e.g. live-2011-12-13 suggests that 

872 # obdgpslogger had the same bug in testing and unstable, 

873 # but obdgpslogger was not in testing at that time). 

874 # - For the curious, obdgpslogger was removed on that day 

875 # and the BTS probably had not caught up with that fact. 

876 # (https://tracker.debian.org/news/415935) 

877 assert not bugs_t or source_data_tdist, ( 

878 "%s had bugs in the target suite but is not present" % source_name 

879 ) 

880 

881 verdict = PolicyVerdict.PASS 

882 

883 assert self.hints is not None 

884 for hint in self.hints.search( 

885 "ignore-rc-bugs", 

886 package=source_name, 

887 version=source_data_srcdist.version, 

888 ): 

889 ignore_hint = cast(IgnoreRCBugHint, hint) 

890 ignored_bugs = ignore_hint.ignored_rcbugs 

891 

892 # Only handle one hint for now 

893 if "ignored-bugs" in rcbugs_info: 

894 self.logger.info( 

895 "Ignoring ignore-rc-bugs hint from %s on %s due to another hint from %s", 

896 ignore_hint.user, 

897 source_name, 

898 rcbugs_info["ignored-bugs"]["issued-by"], 

899 ) 

900 continue 

901 if not ignored_bugs.isdisjoint(bugs_s): 901 ↛ 910line 901 didn't jump to line 910 because the condition on line 901 was always true

902 bugs_s -= ignored_bugs 

903 bugs_t -= ignored_bugs 

904 rcbugs_info["ignored-bugs"] = { 

905 "bugs": sorted(ignored_bugs), 

906 "issued-by": ignore_hint.user, 

907 } 

908 verdict = PolicyVerdict.PASS_HINTED 

909 else: 

910 self.logger.info( 

911 "Ignoring ignore-rc-bugs hint from %s on %s as none of %s affect the package", 

912 ignore_hint.user, 

913 source_name, 

914 ignored_bugs, 

915 ) 

916 

917 rcbugs_info["shared-bugs"] = sorted(bugs_s & bugs_t) 

918 rcbugs_info["unique-source-bugs"] = sorted(bugs_s - bugs_t) 

919 rcbugs_info["unique-target-bugs"] = sorted(bugs_t - bugs_s) 

920 

921 # update excuse 

922 new_bugs = rcbugs_info["unique-source-bugs"] 

923 old_bugs = rcbugs_info["unique-target-bugs"] 

924 excuse.setbugs(old_bugs, new_bugs) 

925 

926 if new_bugs: 

927 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

928 excuse.add_verdict_info( 

929 verdict, 

930 "Updating %s would introduce bugs in %s: %s" 

931 % ( 

932 source_name, 

933 self.suite_info.target_suite.name, 

934 ", ".join( 

935 f'<a href="https://bugs.debian.org/{quote(a)}">#{a}</a>' 

936 for a in new_bugs 

937 ), 

938 ), 

939 ) 

940 

941 if old_bugs: 

942 excuse.addinfo( 

943 "Updating %s will fix bugs in %s: %s" 

944 % ( 

945 source_name, 

946 self.suite_info.target_suite.name, 

947 ", ".join( 

948 f'<a href="https://bugs.debian.org/{quote(a)}">#{a}</a>' 

949 for a in old_bugs 

950 ), 

951 ) 

952 ) 

953 

954 return verdict 

955 

956 def _read_bugs(self, filename: str) -> dict[str, set[str]]: 

957 """Read the release critical bug summary from the specified file 

958 

959 The file contains rows with the format: 

960 

961 <package-name> <bug number>[,<bug number>...] 

962 

963 The method returns a dictionary where the key is the binary package 

964 name and the value is the list of open RC bugs for it. 

965 """ 

966 bugs: dict[str, set[str]] = {} 

967 self.logger.info("Loading RC bugs data from %s", filename) 

968 with open(filename, encoding="ascii") as f: 

969 for line in f: 

970 ln = line.split() 

971 if len(ln) != 2: # pragma: no cover 

972 self.logger.warning("Malformed line found in line %s", line) 

973 continue 

974 pkg = ln[0] 

975 if pkg not in bugs: 

976 bugs[pkg] = set() 

977 bugs[pkg].update(ln[1].split(",")) 

978 return bugs 

979 

980 

981class PiupartsState(Enum): 

982 FAIL = auto() 

983 PASS = auto() 

984 WAITING = auto() 

985 UNKNOWN = auto() 

986 

987 @staticmethod 

988 def from_str(val: str) -> "PiupartsState": 

989 match val: 

990 case "F": 

991 return PiupartsState.FAIL 

992 case "P": 

993 return PiupartsState.PASS 

994 case "W": 994 ↛ 996line 994 didn't jump to line 996 because the pattern on line 994 always matched

995 return PiupartsState.WAITING 

996 case "X": 

997 return PiupartsState.UNKNOWN 

998 case _: 

999 raise ValueError(f"Invalid piuparts state {val}") 

1000 

1001 

1002class PiupartsResult(StrEnum): 

1003 PASS = "pass" 

1004 REGRESSION = "regression" 

1005 FAILED = "failed" 

1006 WAITING_FOR_TESTS = "waiting-for-test-results" 

1007 CANNOT_BE_TESTED = "cannot-be-tested" 

1008 

1009 

1010class PiupartsPolicy(AbstractBasePolicy): 

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

1012 super().__init__( 

1013 "piuparts", options, suite_info, {SuiteClass.PRIMARY_SOURCE_SUITE} 

1014 ) 

1015 self._piuparts_source: dict[str, tuple[PiupartsState, str]] | None = None 

1016 self._piuparts_target: dict[str, PiupartsState] | None = None 

1017 

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

1019 hint_parser.register_hint_type(HintType("ignore-piuparts")) 

1020 

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

1022 super().initialise(britney) 

1023 source_suite = self.suite_info.primary_source_suite 

1024 target_suite = self.suite_info.target_suite 

1025 try: 

1026 filename_unstable = os.path.join( 

1027 self.state_dir, f"piuparts-summary-{source_suite.name}.json" 

1028 ) 

1029 filename_testing = os.path.join( 

1030 self.state_dir, f"piuparts-summary-{target_suite.name}.json" 

1031 ) 

1032 except AttributeError as e: # pragma: no cover 

1033 raise RuntimeError( 

1034 "Please set STATE_DIR in the britney configuration" 

1035 ) from e 

1036 self._piuparts_source = self._read_piuparts_summary(filename_unstable) 

1037 self._piuparts_target = self._read_piuparts_summary_without_url( 

1038 filename_testing 

1039 ) 

1040 

1041 def apply_src_policy_impl( 

1042 self, 

1043 piuparts_info: dict[str, Any], 

1044 source_data_tdist: SourcePackage | None, 

1045 source_data_srcdist: SourcePackage, 

1046 excuse: "Excuse", 

1047 ) -> PolicyVerdict: 

1048 assert self._piuparts_source is not None # for type checking 

1049 assert self._piuparts_target is not None # for type checking 

1050 source_name = excuse.item.package 

1051 

1052 if source_name in self._piuparts_target: 

1053 testing_state = self._piuparts_target[source_name] 

1054 else: 

1055 testing_state = PiupartsState.UNKNOWN 

1056 url: str | None 

1057 if source_name in self._piuparts_source: 

1058 unstable_state, url = self._piuparts_source[source_name] 

1059 else: 

1060 unstable_state = PiupartsState.UNKNOWN 

1061 url = None 

1062 url_html = "(no link yet)" 

1063 if url is not None: 

1064 url_html = '<a href="{0}">{0}</a>'.format(url) 

1065 

1066 match unstable_state: 

1067 case PiupartsState.PASS: 

1068 # Not a regression 

1069 msg = f"Piuparts tested OK - {url_html}" 

1070 result = PolicyVerdict.PASS 

1071 piuparts_info["test-results"] = PiupartsResult.PASS 

1072 case PiupartsState.FAIL if testing_state is not PiupartsState.FAIL: 

1073 piuparts_info["test-results"] = PiupartsResult.REGRESSION 

1074 msg = f"Piuparts regression - {url_html}" 

1075 result = PolicyVerdict.REJECTED_PERMANENTLY 

1076 case PiupartsState.FAIL: 

1077 piuparts_info["test-results"] = PiupartsResult.FAILED 

1078 msg = f"Piuparts failure (not a regression) - {url_html}" 

1079 result = PolicyVerdict.PASS 

1080 case PiupartsState.WAITING: 

1081 msg = f"Piuparts check waiting for test results - {url_html}" 

1082 result = PolicyVerdict.REJECTED_TEMPORARILY 

1083 piuparts_info["test-results"] = PiupartsResult.WAITING_FOR_TESTS 

1084 case _: 

1085 msg = f"Piuparts can't test {source_name} (not a blocker) - {url_html}" 

1086 piuparts_info["test-results"] = PiupartsResult.CANNOT_BE_TESTED 

1087 result = PolicyVerdict.PASS 

1088 

1089 if url is not None: 

1090 piuparts_info["piuparts-test-url"] = url 

1091 if result.is_rejected: 

1092 excuse.add_verdict_info(result, msg) 

1093 else: 

1094 excuse.addinfo(msg) 

1095 

1096 if result.is_rejected: 

1097 assert self.hints is not None 

1098 if ( 

1099 ignore_hint := self.hints.search_first( 

1100 "ignore-piuparts", 

1101 package=source_name, 

1102 version=source_data_srcdist.version, 

1103 ) 

1104 ) is not None: 

1105 piuparts_info["ignored-piuparts"] = {"issued-by": ignore_hint.user} 

1106 result = PolicyVerdict.PASS_HINTED 

1107 excuse.addinfo( 

1108 f"Piuparts issue ignored as requested by {ignore_hint.user}" 

1109 ) 

1110 

1111 return result 

1112 

1113 def _read_piuparts_summary_gen( 

1114 self, filename: str 

1115 ) -> Iterator[tuple[str, PiupartsState, str]]: 

1116 self.logger.info("Loading piuparts report from %s", filename) 

1117 with open(filename) as fd: 1117 ↛ exitline 1117 didn't return from function '_read_piuparts_summary_gen' because the return on line 1119 wasn't executed

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

1119 return 

1120 data = json.load(fd) 

1121 try: 

1122 if ( 

1123 data["_id"] != "Piuparts Package Test Results Summary" 

1124 or data["_version"] != "1.0" 

1125 ): # pragma: no cover 

1126 raise ValueError( 

1127 f"Piuparts results in {filename} does not have the correct ID or version" 

1128 ) 

1129 except KeyError as e: # pragma: no cover 

1130 raise ValueError( 

1131 f"Piuparts results in {filename} is missing id or version field" 

1132 ) from e 

1133 for source, suite_data in data["packages"].items(): 

1134 if len(suite_data) != 1: # pragma: no cover 

1135 raise ValueError( 

1136 f"Piuparts results in {filename}, the source {source} does not have " 

1137 "exactly one result set" 

1138 ) 

1139 item = next(iter(suite_data.values())) 

1140 state, _, url = item 

1141 yield (source, PiupartsState.from_str(state), url) 

1142 

1143 def _read_piuparts_summary( 

1144 self, filename: str 

1145 ) -> dict[str, tuple[PiupartsState, str]]: 

1146 return { 

1147 source: (state, url) 

1148 for (source, state, url) in self._read_piuparts_summary_gen(filename) 

1149 } 

1150 

1151 def _read_piuparts_summary_without_url( 

1152 self, filename: str 

1153 ) -> dict[str, PiupartsState]: 

1154 return { 

1155 source: state 

1156 for (source, state, _) in self._read_piuparts_summary_gen(filename) 

1157 } 

1158 

1159 

1160class DependsPolicy(AbstractBasePolicy): 

1161 pkg_universe: "BinaryPackageUniverse" 

1162 broken_packages: frozenset["BinaryPackageId"] 

1163 all_binaries: dict["BinaryPackageId", "BinaryPackage"] 

1164 allow_uninst: dict[str, set[str | None]] 

1165 

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

1167 super().__init__( 

1168 "depends", 

1169 options, 

1170 suite_info, 

1171 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

1172 ApplySrcPolicy.RUN_ON_EVERY_ARCH_ONLY, 

1173 ) 

1174 self.nobreakall_arches = None 

1175 self.new_arches = None 

1176 self.break_arches = None 

1177 

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

1179 super().initialise(britney) 

1180 self.pkg_universe = britney.pkg_universe 

1181 self.broken_packages = self.pkg_universe.broken_packages 

1182 self.all_binaries = britney.all_binaries 

1183 self.nobreakall_arches = self.options.nobreakall_arches 

1184 self.new_arches = self.options.new_arches 

1185 self.break_arches = self.options.break_arches 

1186 self.allow_uninst = britney.allow_uninst 

1187 

1188 def apply_srcarch_policy_impl( 

1189 self, 

1190 deps_info: dict[str, Any], 

1191 arch: str, 

1192 source_data_tdist: SourcePackage | None, 

1193 source_data_srcdist: SourcePackage, 

1194 excuse: "Excuse", 

1195 ) -> PolicyVerdict: 

1196 verdict = PolicyVerdict.PASS 

1197 

1198 assert self.break_arches is not None 

1199 assert self.new_arches is not None 

1200 if arch in self.break_arches or arch in self.new_arches: 

1201 # we don't check these in the policy (TODO - for now?) 

1202 return verdict 

1203 

1204 item = excuse.item 

1205 source_suite = item.suite 

1206 target_suite = self.suite_info.target_suite 

1207 

1208 packages_s_a = source_suite.binaries[arch] 

1209 packages_t_a = target_suite.binaries[arch] 

1210 

1211 my_bins = sorted(filter_out_faux_gen(excuse.packages[arch])) 

1212 

1213 arch_all_installable = set() 

1214 arch_arch_installable = set() 

1215 consider_it_regression = True 

1216 

1217 for pkg_id in my_bins: 

1218 pkg_name = pkg_id.package_name 

1219 binary_u = packages_s_a[pkg_name] 

1220 pkg_arch = binary_u.architecture 

1221 

1222 # in some cases, we want to track the uninstallability of a 

1223 # package (because the autopkgtest policy uses this), but we still 

1224 # want to allow the package to be uninstallable 

1225 skip_dep_check = False 

1226 

1227 if binary_u.source_version != source_data_srcdist.version: 

1228 # don't check cruft in unstable 

1229 continue 

1230 

1231 if item.architecture != "source" and pkg_arch == "all": 

1232 # we don't care about the existing arch: all binaries when 

1233 # checking a binNMU item, because the arch: all binaries won't 

1234 # migrate anyway 

1235 skip_dep_check = True 

1236 

1237 if pkg_arch == "all" and arch not in self.nobreakall_arches: 

1238 skip_dep_check = True 

1239 

1240 if pkg_name in self.allow_uninst[arch]: 1240 ↛ 1243line 1240 didn't jump to line 1243 because the condition on line 1240 was never true

1241 # this binary is allowed to become uninstallable, so we don't 

1242 # need to check anything 

1243 skip_dep_check = True 

1244 

1245 if pkg_name in packages_t_a: 

1246 oldbin = packages_t_a[pkg_name] 

1247 if not target_suite.is_installable(oldbin.pkg_id): 

1248 # as the current binary in testing is already 

1249 # uninstallable, the newer version is allowed to be 

1250 # uninstallable as well, so we don't need to check 

1251 # anything 

1252 skip_dep_check = True 

1253 consider_it_regression = False 

1254 

1255 if pkg_id in self.broken_packages: 

1256 if pkg_arch == "all": 

1257 arch_all_installable.add(False) 

1258 else: 

1259 arch_arch_installable.add(False) 

1260 # dependencies can't be satisfied by all the known binaries - 

1261 # this certainly won't work... 

1262 excuse.add_unsatisfiable_on_arch(arch) 

1263 if skip_dep_check: 

1264 # ...but if the binary is allowed to become uninstallable, 

1265 # we don't care 

1266 # we still want the binary to be listed as uninstallable, 

1267 continue 

1268 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

1269 if pkg_name.endswith("-faux-build-depends"): 1269 ↛ 1270line 1269 didn't jump to line 1270 because the condition on line 1269 was never true

1270 name = pkg_name.removesuffix("-faux-build-depends") 

1271 excuse.add_verdict_info( 

1272 verdict, 

1273 f"src:{name} has unsatisfiable build dependency", 

1274 ) 

1275 else: 

1276 excuse.add_verdict_info( 

1277 verdict, f"{pkg_name}/{arch} has unsatisfiable dependency" 

1278 ) 

1279 excuse.addreason("depends") 

1280 else: 

1281 if pkg_arch == "all": 

1282 arch_all_installable.add(True) 

1283 else: 

1284 arch_arch_installable.add(True) 

1285 

1286 if skip_dep_check: 

1287 continue 

1288 

1289 deps = self.pkg_universe.dependencies_of(pkg_id) 

1290 

1291 for dep in deps: 

1292 # dep is a list of packages, each of which satisfy the 

1293 # dependency 

1294 

1295 if not dep: 

1296 continue 

1297 is_ok = False 

1298 needed_for_dep = set() 

1299 

1300 for alternative in dep: 

1301 if target_suite.is_pkg_in_the_suite(alternative): 

1302 # dep can be satisfied in testing - ok 

1303 is_ok = True 

1304 elif alternative in my_bins: 

1305 # can be satisfied by binary from same item: will be 

1306 # ok if item migrates 

1307 is_ok = True 

1308 else: 

1309 needed_for_dep.add(alternative) 

1310 

1311 if not is_ok: 

1312 spec = DependencySpec(DependencyType.DEPENDS, arch) 

1313 excuse.add_package_depends(spec, needed_for_dep) 

1314 

1315 # The autopkgtest policy needs delicate trade offs for 

1316 # non-installability. The current choice (considering source 

1317 # migration and only binaries built by the version of the 

1318 # source): 

1319 # 

1320 # * Run autopkgtest if all arch:$arch binaries are installable 

1321 # (but some or all arch:all binaries are not) 

1322 # 

1323 # * Don't schedule nor wait for not installable arch:all only package 

1324 # on ! NOBREAKALL_ARCHES 

1325 # 

1326 # * Run autopkgtest if installability isn't a regression (there are (or 

1327 # rather, should) not be a lot of packages in this state, and most 

1328 # likely they'll just fail quickly) 

1329 # 

1330 # * Don't schedule, but wait otherwise 

1331 if arch_arch_installable == {True} and False in arch_all_installable: 

1332 deps_info.setdefault("autopkgtest_run_anyways", []).append(arch) 

1333 elif ( 

1334 arch not in self.nobreakall_arches 

1335 and not arch_arch_installable 

1336 and False in arch_all_installable 

1337 ): 

1338 deps_info.setdefault("arch_all_not_installable", []).append(arch) 

1339 elif not consider_it_regression: 

1340 deps_info.setdefault("autopkgtest_run_anyways", []).append(arch) 

1341 

1342 return verdict 

1343 

1344 

1345@unique 

1346class BuildDepResult(IntEnum): 

1347 # relation is satisfied in target 

1348 OK = 1 

1349 # relation can be satisfied by other packages in source 

1350 DEPENDS = 2 

1351 # relation cannot be satisfied 

1352 FAILED = 3 

1353 

1354 

1355class BuildDependsPolicy(AbstractBasePolicy): 

1356 

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

1358 super().__init__( 

1359 "build-depends", 

1360 options, 

1361 suite_info, 

1362 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

1363 ) 

1364 self._all_buildarch: list[str] = [] 

1365 

1366 parse_option(options, "all_buildarch") 

1367 

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

1369 super().initialise(britney) 

1370 if self.options.all_buildarch: 

1371 self._all_buildarch = SuiteContentLoader.config_str_as_list( 

1372 self.options.all_buildarch, [] 

1373 ) 

1374 

1375 def apply_src_policy_impl( 

1376 self, 

1377 build_deps_info: dict[str, Any], 

1378 source_data_tdist: SourcePackage | None, 

1379 source_data_srcdist: SourcePackage, 

1380 excuse: "Excuse", 

1381 get_dependency_solvers: GetDependencySolversProto = get_dependency_solvers, 

1382 ) -> PolicyVerdict: 

1383 verdict = PolicyVerdict.PASS 

1384 

1385 # analyze the dependency fields (if present) 

1386 if deps := source_data_srcdist.build_deps_arch: 

1387 v = self._check_build_deps( 

1388 deps, 

1389 DependencyType.BUILD_DEPENDS, 

1390 build_deps_info, 

1391 source_data_srcdist, 

1392 excuse, 

1393 get_dependency_solvers=get_dependency_solvers, 

1394 ) 

1395 verdict = PolicyVerdict.worst_of(verdict, v) 

1396 

1397 if ideps := source_data_srcdist.build_deps_indep: 

1398 v = self._check_build_deps( 

1399 ideps, 

1400 DependencyType.BUILD_DEPENDS_INDEP, 

1401 build_deps_info, 

1402 source_data_srcdist, 

1403 excuse, 

1404 get_dependency_solvers=get_dependency_solvers, 

1405 ) 

1406 verdict = PolicyVerdict.worst_of(verdict, v) 

1407 

1408 return verdict 

1409 

1410 def _get_check_archs( 

1411 self, archs: Container[str], dep_type: DependencyType 

1412 ) -> list[str]: 

1413 oos = self.options.outofsync_arches 

1414 

1415 if dep_type is DependencyType.BUILD_DEPENDS: 

1416 return [ 

1417 arch 

1418 for arch in self.options.architectures 

1419 if arch in archs and arch not in oos 

1420 ] 

1421 

1422 # first try the all buildarch 

1423 checkarchs = list(self._all_buildarch) 

1424 # then try the architectures where this source has arch specific 

1425 # binaries (in the order of the architecture config file) 

1426 checkarchs.extend( 

1427 arch 

1428 for arch in self.options.architectures 

1429 if arch in archs and arch not in checkarchs 

1430 ) 

1431 # then try all other architectures 

1432 checkarchs.extend( 

1433 arch for arch in self.options.architectures if arch not in checkarchs 

1434 ) 

1435 

1436 # and drop OUTOFSYNC_ARCHES 

1437 return [arch for arch in checkarchs if arch not in oos] 

1438 

1439 def _add_info_for_arch( 

1440 self, 

1441 arch: str, 

1442 excuses_info: dict[str, list[str]], 

1443 blockers: dict[str, set[BinaryPackageId]], 

1444 results: dict[str, BuildDepResult], 

1445 dep_type: DependencyType, 

1446 excuse: "Excuse", 

1447 verdict: PolicyVerdict, 

1448 ) -> PolicyVerdict: 

1449 if arch in blockers: 

1450 packages = blockers[arch] 

1451 

1452 # for the solving packages, update the excuse to add the dependencies 

1453 for p in packages: 

1454 if arch not in self.options.break_arches: 1454 ↛ 1453line 1454 didn't jump to line 1453 because the condition on line 1454 was always true

1455 spec = DependencySpec(dep_type, arch) 

1456 excuse.add_package_depends(spec, {p}) 

1457 

1458 if arch in results and results[arch] is BuildDepResult.FAILED: 

1459 verdict = PolicyVerdict.worst_of( 

1460 verdict, PolicyVerdict.REJECTED_PERMANENTLY 

1461 ) 

1462 

1463 if arch in excuses_info: 

1464 for excuse_text in excuses_info[arch]: 

1465 if verdict.is_rejected: 1465 ↛ 1468line 1465 didn't jump to line 1468 because the condition on line 1465 was always true

1466 excuse.add_verdict_info(verdict, excuse_text) 

1467 else: 

1468 excuse.addinfo(excuse_text) 

1469 

1470 return verdict 

1471 

1472 def _check_build_deps( 

1473 self, 

1474 deps: str, 

1475 dep_type: DependencyType, 

1476 build_deps_info: dict[str, Any], 

1477 source_data_srcdist: SourcePackage, 

1478 excuse: "Excuse", 

1479 get_dependency_solvers: GetDependencySolversProto = get_dependency_solvers, 

1480 ) -> PolicyVerdict: 

1481 verdict = PolicyVerdict.PASS 

1482 any_arch_ok = dep_type is DependencyType.BUILD_DEPENDS_INDEP 

1483 

1484 britney = self.britney 

1485 

1486 # local copies for better performance 

1487 parse_src_depends = apt_pkg.parse_src_depends 

1488 

1489 source_name = excuse.item.package 

1490 source_suite = excuse.item.suite 

1491 target_suite = self.suite_info.target_suite 

1492 binaries_s = source_suite.binaries 

1493 provides_s = source_suite.provides_table 

1494 binaries_t = target_suite.binaries 

1495 provides_t = target_suite.provides_table 

1496 unsat_bd: dict[str, list[str]] = {} 

1497 relevant_archs: set[str] = { 

1498 binary.architecture 

1499 for binary in filter_out_faux_gen(source_data_srcdist.binaries) 

1500 if britney.all_binaries[binary].architecture != "all" 

1501 } 

1502 

1503 excuses_info: dict[str, list[str]] = defaultdict(list) 

1504 blockers: dict[str, set[BinaryPackageId]] = defaultdict(set) 

1505 arch_results = {} 

1506 result_archs = defaultdict(list) 

1507 bestresult = BuildDepResult.FAILED 

1508 check_archs = self._get_check_archs(relevant_archs, dep_type) 

1509 if not check_archs: 

1510 # when the arch list is empty, we check the b-d on any arch, instead of all archs 

1511 # this happens for Build-Depens on a source package that only produces arch: all binaries 

1512 any_arch_ok = True 

1513 check_archs = self._get_check_archs( 

1514 self.options.architectures, DependencyType.BUILD_DEPENDS_INDEP 

1515 ) 

1516 

1517 for arch in check_archs: 

1518 # retrieve the binary package from the specified suite and arch 

1519 binaries_s_a = binaries_s[arch] 

1520 provides_s_a = provides_s[arch] 

1521 binaries_t_a = binaries_t[arch] 

1522 provides_t_a = provides_t[arch] 

1523 arch_results[arch] = BuildDepResult.OK 

1524 # for every dependency block (formed as conjunction of disjunction) 

1525 for block_txt in deps.split(","): 

1526 block_list = parse_src_depends(block_txt, False, arch) 

1527 # Unlike regular dependencies, some clauses of the Build-Depends(-Arch|-Indep) can be 

1528 # filtered out by (e.g.) architecture restrictions. We need to cope with this while 

1529 # keeping block_txt and block aligned. 

1530 if not block_list: 

1531 # Relation is not relevant for this architecture. 

1532 continue 

1533 block = block_list[0] 

1534 # if the block is satisfied in the target suite, then skip the block 

1535 if get_dependency_solvers( 

1536 block, binaries_t_a, provides_t_a, build_depends=True 

1537 ): 

1538 # Satisfied in the target suite; all ok. 

1539 continue 

1540 

1541 # check if the block can be satisfied in the source suite, and list the solving packages 

1542 packages = get_dependency_solvers( 

1543 block, binaries_s_a, provides_s_a, build_depends=True 

1544 ) 

1545 

1546 # if the dependency can be satisfied by the same source package, skip the block: 

1547 # obviously both binary packages will enter the target suite together 

1548 if any(source_name == p.source for p in packages): 1548 ↛ 1549line 1548 didn't jump to line 1549 because the condition on line 1548 was never true

1549 continue 

1550 

1551 # if no package can satisfy the dependency, add this information to the excuse 

1552 if not packages: 

1553 excuses_info[arch].append( 

1554 "%s unsatisfiable %s on %s: %s" 

1555 % (source_name, dep_type, arch, block_txt.strip()) 

1556 ) 

1557 if arch not in unsat_bd: 1557 ↛ 1559line 1557 didn't jump to line 1559 because the condition on line 1557 was always true

1558 unsat_bd[arch] = [] 

1559 unsat_bd[arch].append(block_txt.strip()) 

1560 arch_results[arch] = BuildDepResult.FAILED 

1561 continue 

1562 

1563 blockers[arch].update(p.pkg_id for p in packages) 

1564 if arch_results[arch] < BuildDepResult.DEPENDS: 

1565 arch_results[arch] = BuildDepResult.DEPENDS 

1566 

1567 if any_arch_ok: 

1568 if arch_results[arch] < bestresult: 

1569 bestresult = arch_results[arch] 

1570 result_archs[arch_results[arch]].append(arch) 

1571 if bestresult is BuildDepResult.OK: 

1572 # we found an architecture where the b-deps-indep are 

1573 # satisfied in the target suite, so we can stop 

1574 break 

1575 

1576 if any_arch_ok: 

1577 arch = result_archs[bestresult][0] 

1578 excuse.add_detailed_info(f"Checking {dep_type.get_description()} on {arch}") 

1579 key = "check-%s-on-arch" % dep_type.get_reason() 

1580 build_deps_info[key] = arch 

1581 verdict = self._add_info_for_arch( 

1582 arch, 

1583 excuses_info, 

1584 blockers, 

1585 arch_results, 

1586 dep_type, 

1587 excuse, 

1588 verdict, 

1589 ) 

1590 

1591 else: 

1592 for arch in check_archs: 

1593 verdict = self._add_info_for_arch( 

1594 arch, 

1595 excuses_info, 

1596 blockers, 

1597 arch_results, 

1598 dep_type, 

1599 excuse, 

1600 verdict, 

1601 ) 

1602 

1603 if unsat_bd: 

1604 build_deps_info["unsatisfiable-arch-build-depends"] = unsat_bd 

1605 

1606 return verdict 

1607 

1608 

1609class BuiltUsingPolicy(AbstractBasePolicy): 

1610 """Built-Using policy 

1611 

1612 Binaries that incorporate (part of) another source package must list these 

1613 sources under 'Built-Using'. 

1614 

1615 This policy checks if the corresponding sources are available in the 

1616 target suite. If they are not, but they are candidates for migration, a 

1617 dependency is added. 

1618 

1619 If the binary incorporates a newer version of a source, that is not (yet) 

1620 a candidate, we don't want to accept that binary. A rebuild later in the 

1621 primary suite wouldn't fix the issue, because that would incorporate the 

1622 newer version again. 

1623 

1624 If the binary incorporates an older version of the source, a newer version 

1625 will be accepted as a replacement. We assume that this can be fixed by 

1626 rebuilding the binary at some point during the development cycle. 

1627 

1628 Requiring exact version of the source would not be useful in practice. A 

1629 newer upload of that source wouldn't be blocked by this policy, so the 

1630 built-using would be outdated anyway. 

1631 

1632 """ 

1633 

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

1635 super().__init__( 

1636 "built-using", 

1637 options, 

1638 suite_info, 

1639 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

1640 ApplySrcPolicy.RUN_ON_EVERY_ARCH_ONLY, 

1641 ) 

1642 

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

1644 super().initialise(britney) 

1645 

1646 def apply_srcarch_policy_impl( 

1647 self, 

1648 build_deps_info: dict[str, Any], 

1649 arch: str, 

1650 source_data_tdist: SourcePackage | None, 

1651 source_data_srcdist: SourcePackage, 

1652 excuse: "Excuse", 

1653 ) -> PolicyVerdict: 

1654 verdict = PolicyVerdict.PASS 

1655 

1656 source_suite = excuse.item.suite 

1657 target_suite = self.suite_info.target_suite 

1658 binaries_s = source_suite.binaries 

1659 

1660 def check_bu_in_suite( 

1661 bu_source: str, bu_version: str, source_suite: Suite 

1662 ) -> bool: 

1663 if bu_source not in source_suite.sources: 

1664 return False 

1665 s_source = source_suite.sources[bu_source] 

1666 s_ver = s_source.version 

1667 if apt_pkg.version_compare(s_ver, bu_version) >= 0: 

1668 dep = PackageId(bu_source, s_ver, "source") 

1669 if arch in self.options.break_arches: 

1670 excuse.add_detailed_info( 

1671 "Ignoring Built-Using for %s/%s on %s" 

1672 % (pkg_name, arch, dep.uvname) 

1673 ) 

1674 else: 

1675 spec = DependencySpec(DependencyType.BUILT_USING, arch) 

1676 excuse.add_package_depends(spec, {dep}) 

1677 excuse.add_detailed_info( 

1678 f"{pkg_name}/{arch} has Built-Using on {dep.uvname}" 

1679 ) 

1680 return True 

1681 

1682 return False 

1683 

1684 for pkg_id in sorted( 

1685 x 

1686 for x in filter_out_faux_gen(source_data_srcdist.binaries) 

1687 if x.architecture == arch 

1688 ): 

1689 pkg_name = pkg_id.package_name 

1690 

1691 # retrieve the testing (if present) and unstable corresponding binary packages 

1692 binary_s = binaries_s[arch][pkg_name] 

1693 if binary_s.builtusing is None: 

1694 continue 

1695 

1696 for bu in binary_s.builtusing: 

1697 bu_source = bu[0] 

1698 bu_version = bu[1] 

1699 found = False 

1700 if bu_source in target_suite.sources: 

1701 t_source = target_suite.sources[bu_source] 

1702 t_ver = t_source.version 

1703 if apt_pkg.version_compare(t_ver, bu_version) >= 0: 

1704 found = True 

1705 

1706 if not found: 

1707 found = check_bu_in_suite(bu_source, bu_version, source_suite) 

1708 

1709 if not found and source_suite.suite_class.is_additional_source: 

1710 found = check_bu_in_suite( 

1711 bu_source, bu_version, self.suite_info.primary_source_suite 

1712 ) 

1713 

1714 if not found: 

1715 if arch in self.options.break_arches: 

1716 excuse.add_detailed_info( 

1717 "Ignoring unsatisfiable Built-Using for %s/%s on %s %s" 

1718 % (pkg_name, arch, bu_source, bu_version) 

1719 ) 

1720 else: 

1721 verdict = PolicyVerdict.worst_of( 

1722 verdict, PolicyVerdict.REJECTED_PERMANENTLY 

1723 ) 

1724 excuse.add_verdict_info( 

1725 verdict, 

1726 "%s/%s has unsatisfiable Built-Using on %s %s" 

1727 % (pkg_name, arch, bu_source, bu_version), 

1728 ) 

1729 

1730 return verdict 

1731 

1732 

1733class BlockPolicy(AbstractBasePolicy): 

1734 BLOCK_HINT_REGEX = re.compile("^(un)?(block-?.*)$") 

1735 

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

1737 super().__init__( 

1738 "block", 

1739 options, 

1740 suite_info, 

1741 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

1742 ) 

1743 self._blockall: dict[str | None, Hint] = {} 

1744 

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

1746 super().initialise(britney) 

1747 assert self.hints is not None 

1748 for hint in self.hints.search(type="block-all"): 

1749 self._blockall[hint.package] = hint 

1750 

1751 self._key_packages: frozenset[str] = frozenset() 

1752 if "key" in self._blockall: 

1753 self._key_packages = self._read_key_packages() 

1754 

1755 def _read_key_packages(self) -> frozenset[str]: 

1756 """Read the list of key packages 

1757 

1758 The file contains data in the yaml format : 

1759 

1760 - reason: <something> 

1761 source: <package> 

1762 

1763 The method returns a list of all key packages. 

1764 """ 

1765 filename = os.path.join(self.state_dir, "key_packages.yaml") 

1766 self.logger.info("Loading key packages from %s", filename) 

1767 if os.path.exists(filename): 1767 ↛ 1772line 1767 didn't jump to line 1772 because the condition on line 1767 was always true

1768 with open(filename) as f: 

1769 data = yaml.safe_load(f) 

1770 key_packages = frozenset(item["source"] for item in data) 

1771 else: 

1772 self.logger.error( 

1773 "Britney was asked to block key packages, " 

1774 + "but no key_packages.yaml file was found." 

1775 ) 

1776 sys.exit(1) 

1777 

1778 return key_packages 

1779 

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

1781 # block related hints are currently defined in hint.py 

1782 pass 

1783 

1784 def _check_blocked( 

1785 self, arch: str, version: str, excuse: "Excuse" 

1786 ) -> PolicyVerdict: 

1787 verdict = PolicyVerdict.PASS 

1788 blocked = {} 

1789 unblocked = {} 

1790 block_info = {} 

1791 source_suite = excuse.item.suite 

1792 suite_name = source_suite.name 

1793 src = excuse.item.package 

1794 is_primary = source_suite.suite_class is SuiteClass.PRIMARY_SOURCE_SUITE 

1795 

1796 tooltip = ( 

1797 f"please contact {self.options.distribution}-release if update is needed" 

1798 ) 

1799 

1800 assert self.hints is not None 

1801 mismatches = False 

1802 r = self.BLOCK_HINT_REGEX 

1803 for hint in self.hints.search(package=src): 

1804 m = r.match(hint.type) 

1805 if m: 

1806 if m.group(1) == "un": 

1807 assert hint.suite is not None 

1808 if ( 

1809 hint.version != version 

1810 or hint.suite.name != suite_name 

1811 or (hint.architecture != arch and hint.architecture != "source") 

1812 ): 

1813 self.logger.info( 

1814 "hint mismatch: %s %s %s", version, arch, suite_name 

1815 ) 

1816 mismatches = True 

1817 else: 

1818 unblocked[m.group(2)] = hint.user 

1819 excuse.add_hint(hint) 

1820 else: 

1821 # block(-*) hint: only accepts a source, so this will 

1822 # always match 

1823 blocked[m.group(2)] = hint.user 

1824 excuse.add_hint(hint) 

1825 

1826 if "block" not in blocked and is_primary: 

1827 # if there is a specific block hint for this package, we don't 

1828 # check for the general hints 

1829 

1830 if self.options.distribution == "debian": 1830 ↛ 1834line 1830 didn't jump to line 1834 because the condition on line 1830 was always true

1831 url = "https://release.debian.org/testing/freeze_policy.html" 

1832 tooltip = f'Follow the <a href="{url}">freeze policy</a> when applying for an unblock' 

1833 

1834 if "source" in self._blockall: 

1835 blocked["block"] = self._blockall["source"].user 

1836 excuse.add_hint(self._blockall["source"]) 

1837 elif ( 

1838 "new-source" in self._blockall 

1839 and src not in self.suite_info.target_suite.sources 

1840 ): 

1841 blocked["block"] = self._blockall["new-source"].user 

1842 excuse.add_hint(self._blockall["new-source"]) 

1843 # no tooltip: new sources will probably not be accepted anyway 

1844 block_info["block"] = "blocked by {}: is not in {}".format( 

1845 self._blockall["new-source"].user, 

1846 self.suite_info.target_suite.name, 

1847 ) 

1848 elif "key" in self._blockall and src in self._key_packages: 

1849 blocked["block"] = self._blockall["key"].user 

1850 excuse.add_hint(self._blockall["key"]) 

1851 block_info["block"] = "blocked by {}: is a key package ({})".format( 

1852 self._blockall["key"].user, 

1853 tooltip, 

1854 ) 

1855 elif "no-autopkgtest" in self._blockall: 

1856 if excuse.autopkgtest_results == {"PASS"}: 

1857 if not blocked: 1857 ↛ 1883line 1857 didn't jump to line 1883 because the condition on line 1857 was always true

1858 excuse.addinfo("not blocked: has successful autopkgtest") 

1859 else: 

1860 blocked["block"] = self._blockall["no-autopkgtest"].user 

1861 excuse.add_hint(self._blockall["no-autopkgtest"]) 

1862 if not excuse.autopkgtest_results: 

1863 block_info["block"] = ( 

1864 "blocked by %s: does not have autopkgtest (%s)" 

1865 % ( 

1866 self._blockall["no-autopkgtest"].user, 

1867 tooltip, 

1868 ) 

1869 ) 

1870 else: 

1871 block_info["block"] = ( 

1872 "blocked by %s: autopkgtest not fully successful (%s)" 

1873 % ( 

1874 self._blockall["no-autopkgtest"].user, 

1875 tooltip, 

1876 ) 

1877 ) 

1878 

1879 elif not is_primary: 

1880 blocked["block"] = suite_name 

1881 excuse.needs_approval = True 

1882 

1883 for block_cmd in blocked: 

1884 unblock_cmd = "un" + block_cmd 

1885 if block_cmd in unblocked: 

1886 if is_primary or block_cmd == "block-udeb": 

1887 excuse.addinfo( 

1888 "Ignoring %s request by %s, due to %s request by %s" 

1889 % ( 

1890 block_cmd, 

1891 blocked[block_cmd], 

1892 unblock_cmd, 

1893 unblocked[block_cmd], 

1894 ) 

1895 ) 

1896 else: 

1897 excuse.addinfo("Approved by %s" % (unblocked[block_cmd])) 

1898 else: 

1899 verdict = PolicyVerdict.REJECTED_NEEDS_APPROVAL 

1900 if is_primary or block_cmd == "block-udeb": 

1901 # redirect people to d-i RM for udeb things: 

1902 if block_cmd == "block-udeb": 

1903 tooltip = "please contact the d-i release manager if an update is needed" 

1904 if block_cmd in block_info: 

1905 info = block_info[block_cmd] 

1906 else: 

1907 info = ( 

1908 "Not touching package due to {} request by {} ({})".format( 

1909 block_cmd, 

1910 blocked[block_cmd], 

1911 tooltip, 

1912 ) 

1913 ) 

1914 excuse.add_verdict_info(verdict, info) 

1915 else: 

1916 excuse.add_verdict_info(verdict, "NEEDS APPROVAL BY RM") 

1917 excuse.addreason("block") 

1918 if mismatches: 

1919 excuse.add_detailed_info( 

1920 f"Some hints for {src} do not match this item" 

1921 ) 

1922 return verdict 

1923 

1924 def apply_src_policy_impl( 

1925 self, 

1926 block_info: dict[str, Any], 

1927 source_data_tdist: SourcePackage | None, 

1928 source_data_srcdist: SourcePackage, 

1929 excuse: "Excuse", 

1930 ) -> PolicyVerdict: 

1931 return self._check_blocked("source", source_data_srcdist.version, excuse) 

1932 

1933 def apply_srcarch_policy_impl( 

1934 self, 

1935 block_info: dict[str, Any], 

1936 arch: str, 

1937 source_data_tdist: SourcePackage | None, 

1938 source_data_srcdist: SourcePackage, 

1939 excuse: "Excuse", 

1940 ) -> PolicyVerdict: 

1941 return self._check_blocked(arch, source_data_srcdist.version, excuse) 

1942 

1943 

1944class BuiltOnBuilddPolicy(AbstractBasePolicy): 

1945 

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

1947 super().__init__( 

1948 "builtonbuildd", 

1949 options, 

1950 suite_info, 

1951 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

1952 ApplySrcPolicy.RUN_ON_EVERY_ARCH_ONLY, 

1953 ) 

1954 self._signer_info: dict[str, Any] = {} 

1955 

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

1957 hint_parser.register_hint_type( 

1958 HintType( 

1959 "allow-archall-maintainer-upload", 

1960 versioned=HintAnnotate.FORBIDDEN, 

1961 ) 

1962 ) 

1963 

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

1965 super().initialise(britney) 

1966 try: 

1967 filename_signerinfo = os.path.join(self.state_dir, "signers.json") 

1968 except AttributeError as e: # pragma: no cover 

1969 raise RuntimeError( 

1970 "Please set STATE_DIR in the britney configuration" 

1971 ) from e 

1972 self._signer_info = self._read_signerinfo(filename_signerinfo) 

1973 

1974 def apply_srcarch_policy_impl( 

1975 self, 

1976 buildd_info: dict[str, Any], 

1977 arch: str, 

1978 source_data_tdist: SourcePackage | None, 

1979 source_data_srcdist: SourcePackage, 

1980 excuse: "Excuse", 

1981 ) -> PolicyVerdict: 

1982 verdict = PolicyVerdict.PASS 

1983 signers = self._signer_info 

1984 

1985 if "signed-by" not in buildd_info: 

1986 buildd_info["signed-by"] = {} 

1987 

1988 item = excuse.item 

1989 source_suite = item.suite 

1990 

1991 # we use the source component, because a binary in contrib can 

1992 # belong to a source in main 

1993 component = get_component(source_data_srcdist.section) 

1994 

1995 packages_s_a = source_suite.binaries[arch] 

1996 assert self.hints is not None 

1997 

1998 for pkg_id in sorted( 

1999 x 

2000 for x in filter_out_faux_gen(source_data_srcdist.binaries) 

2001 if x.architecture == arch 

2002 ): 

2003 pkg_name = pkg_id.package_name 

2004 binary_u = packages_s_a[pkg_name] 

2005 pkg_arch = binary_u.architecture 

2006 

2007 if binary_u.source_version != source_data_srcdist.version: 2007 ↛ 2008line 2007 didn't jump to line 2008 because the condition on line 2007 was never true

2008 continue 

2009 

2010 if item.architecture != "source" and pkg_arch == "all": 

2011 # we don't care about the existing arch: all binaries when 

2012 # checking a binNMU item, because the arch: all binaries won't 

2013 # migrate anyway 

2014 continue 

2015 

2016 signer = None 

2017 uid = None 

2018 uidinfo = "" 

2019 buildd_ok = False 

2020 failure_verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2021 try: 

2022 signer = signers[pkg_name][pkg_id.version][pkg_arch] 

2023 if signer["buildd"]: 

2024 buildd_ok = True 

2025 uid = signer["uid"] 

2026 uidinfo = f"arch {pkg_arch} binaries uploaded by {uid}" 

2027 except KeyError: 

2028 self.logger.info( 

2029 "signer info for %s %s (%s) on %s not found", 

2030 pkg_name, 

2031 binary_u.version, 

2032 pkg_arch, 

2033 arch, 

2034 ) 

2035 uidinfo = f"upload info for arch {pkg_arch} binaries not found" 

2036 failure_verdict = PolicyVerdict.REJECTED_CANNOT_DETERMINE_IF_PERMANENT 

2037 if not buildd_ok: 

2038 if component != "main": 

2039 if not buildd_ok and pkg_arch not in buildd_info["signed-by"]: 2039 ↛ 2043line 2039 didn't jump to line 2043 because the condition on line 2039 was always true

2040 excuse.add_detailed_info( 

2041 f"{uidinfo}, but package in {component}" 

2042 ) 

2043 buildd_ok = True 

2044 elif pkg_arch == "all": 

2045 if ( 

2046 allow_hint := self.hints.search_first( 

2047 "allow-archall-maintainer-upload", package=item.package 

2048 ) 

2049 ) is not None: 

2050 buildd_ok = True 

2051 verdict = PolicyVerdict.worst_of( 

2052 verdict, PolicyVerdict.PASS_HINTED 

2053 ) 

2054 if pkg_arch not in buildd_info["signed-by"]: 

2055 excuse.addinfo( 

2056 f"{uidinfo}, but whitelisted by {allow_hint.user}" 

2057 ) 

2058 if not buildd_ok: 

2059 verdict = failure_verdict 

2060 if pkg_arch not in buildd_info["signed-by"]: 

2061 if pkg_arch == "all": 

2062 uidinfo += ( 

2063 ", a new source-only upload is needed to allow migration" 

2064 ) 

2065 excuse.add_verdict_info(verdict, f"Not built on buildd: {uidinfo}") 

2066 

2067 if ( 2067 ↛ 2071line 2067 didn't jump to line 2071

2068 pkg_arch in buildd_info["signed-by"] 

2069 and buildd_info["signed-by"][pkg_arch] != uid 

2070 ): 

2071 self.logger.info( 

2072 "signer mismatch for %s (%s %s) on %s: %s, while %s already listed", 

2073 pkg_name, 

2074 binary_u.source, 

2075 binary_u.source_version, 

2076 pkg_arch, 

2077 uid, 

2078 buildd_info["signed-by"][pkg_arch], 

2079 ) 

2080 

2081 buildd_info["signed-by"][pkg_arch] = uid 

2082 

2083 return verdict 

2084 

2085 def _read_signerinfo(self, filename: str) -> dict[str, Any]: 

2086 signerinfo: dict[str, Any] = {} 

2087 self.logger.info("Loading signer info from %s", filename) 

2088 with open(filename) as fd: 2088 ↛ exitline 2088 didn't return from function '_read_signerinfo' because the return on line 2090 wasn't executed

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

2090 return signerinfo 

2091 signerinfo = json.load(fd) 

2092 

2093 return signerinfo 

2094 

2095 

2096class ImplicitDependencyPolicy(AbstractBasePolicy): 

2097 """Implicit Dependency policy 

2098 

2099 Upgrading a package pkg-a can break the installability of a package pkg-b. 

2100 A newer version (or the removal) of pkg-b might fix the issue. In that 

2101 case, pkg-a has an 'implicit dependency' on pkg-b, because pkg-a can only 

2102 migrate if pkg-b also migrates. 

2103 

2104 This policy tries to discover a few common cases, and adds the relevant 

2105 info to the excuses. If another item is needed to fix the 

2106 uninstallability, a dependency is added. If no newer item can fix it, this 

2107 excuse will be blocked. 

2108 

2109 Note that the migration step will check the installability of every 

2110 package, so this policy doesn't need to handle every corner case. It 

2111 must, however, make sure that no excuse is unnecessarily blocked. 

2112 

2113 Some cases that should be detected by this policy: 

2114 

2115 * pkg-a is upgraded from 1.0-1 to 2.0-1, while 

2116 pkg-b has "Depends: pkg-a (<< 2.0)" 

2117 This typically happens if pkg-b has a strict dependency on pkg-a because 

2118 it uses some non-stable internal interface (examples are glibc, 

2119 binutils, python3-defaults, ...) 

2120 

2121 * pkg-a is upgraded from 1.0-1 to 2.0-1, and 

2122 pkg-a 1.0-1 has "Provides: provides-1", 

2123 pkg-a 2.0-1 has "Provides: provides-2", 

2124 pkg-b has "Depends: provides-1" 

2125 This typically happens when pkg-a has an interface that changes between 

2126 versions, and a virtual package is used to identify the version of this 

2127 interface (e.g. perl-api-x.y) 

2128 

2129 """ 

2130 

2131 _pkg_universe: "BinaryPackageUniverse" 

2132 _all_binaries: dict["BinaryPackageId", "BinaryPackage"] 

2133 _allow_uninst: dict[str, set[str | None]] 

2134 _nobreakall_arches: list[str] 

2135 

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

2137 super().__init__( 

2138 "implicit-deps", 

2139 options, 

2140 suite_info, 

2141 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

2142 ApplySrcPolicy.RUN_ON_EVERY_ARCH_ONLY, 

2143 ) 

2144 

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

2146 super().initialise(britney) 

2147 self._pkg_universe = britney.pkg_universe 

2148 self._all_binaries = britney.all_binaries 

2149 self._smooth_updates = britney.options.smooth_updates 

2150 self._nobreakall_arches = self.options.nobreakall_arches 

2151 self._new_arches = self.options.new_arches 

2152 self._break_arches = self.options.break_arches 

2153 self._allow_uninst = britney.allow_uninst 

2154 self._outofsync_arches = self.options.outofsync_arches 

2155 

2156 def can_be_removed(self, pkg: BinaryPackage) -> bool: 

2157 src = pkg.source 

2158 target_suite = self.suite_info.target_suite 

2159 

2160 # TODO these conditions shouldn't be hardcoded here 

2161 # ideally, we would be able to look up excuses to see if the removal 

2162 # is in there, but in the current flow, this policy is called before 

2163 # all possible excuses exist, so there is no list for us to check 

2164 

2165 if src not in self.suite_info.primary_source_suite.sources: 

2166 # source for pkg not in unstable: candidate for removal 

2167 return True 

2168 

2169 source_t = target_suite.sources[src] 

2170 assert self.hints is not None 

2171 if self.hints.has_hint("remove", package=src, version=source_t.version): 

2172 # removal hint for the source in testing: candidate for removal 

2173 return True 

2174 

2175 if target_suite.is_cruft(pkg): 

2176 # if pkg is cruft in testing, removal will be tried 

2177 return True 

2178 

2179 # the case were the newer version of the source no longer includes the 

2180 # binary (or includes a cruft version of the binary) will be handled 

2181 # separately (in that case there might be an implicit dependency on 

2182 # the newer source) 

2183 

2184 return False 

2185 

2186 def should_skip_rdep( 

2187 self, pkg: BinaryPackage, source_name: str, myarch: str 

2188 ) -> bool: 

2189 target_suite = self.suite_info.target_suite 

2190 

2191 if not target_suite.is_pkg_in_the_suite(pkg.pkg_id): 

2192 # it is not in the target suite, migration cannot break anything 

2193 return True 

2194 

2195 if pkg.source == source_name: 

2196 # if it is built from the same source, it will be upgraded 

2197 # with the source 

2198 return True 

2199 

2200 if self.can_be_removed(pkg): 

2201 # could potentially be removed, so if that happens, it won't be 

2202 # broken 

2203 return True 

2204 

2205 if pkg.architecture == "all" and myarch not in self._nobreakall_arches: 

2206 # arch all on non nobreakarch is allowed to become uninstallable 

2207 return True 

2208 

2209 if pkg.pkg_id.package_name in self._allow_uninst[myarch]: 

2210 # there is a hint to allow this binary to become uninstallable 

2211 return True 

2212 

2213 if not target_suite.is_installable(pkg.pkg_id): 

2214 # it is already uninstallable in the target suite, migration 

2215 # cannot break anything 

2216 return True 

2217 

2218 return False 

2219 

2220 def breaks_installability( 

2221 self, 

2222 pkg_id_t: BinaryPackageId, 

2223 pkg_id_s: BinaryPackageId | None, 

2224 pkg_to_check: BinaryPackageId, 

2225 ) -> bool: 

2226 """ 

2227 Check if upgrading pkg_id_t to pkg_id_s breaks the installability of 

2228 pkg_to_check. 

2229 

2230 To check if removing pkg_id_t breaks pkg_to_check, set pkg_id_s to 

2231 None. 

2232 """ 

2233 

2234 pkg_universe = self._pkg_universe 

2235 negative_deps = pkg_universe.negative_dependencies_of(pkg_to_check) 

2236 

2237 for dep in pkg_universe.dependencies_of(pkg_to_check): 

2238 if pkg_id_t not in dep: 

2239 # this depends doesn't have pkg_id_t as alternative, so 

2240 # upgrading pkg_id_t cannot break this dependency clause 

2241 continue 

2242 

2243 # We check all the alternatives for this dependency, to find one 

2244 # that can satisfy it when pkg_id_t is upgraded to pkg_id_s 

2245 found_alternative = False 

2246 for d in dep: 

2247 if d in negative_deps: 

2248 # If this alternative dependency conflicts with 

2249 # pkg_to_check, it cannot be used to satisfy the 

2250 # dependency. 

2251 # This commonly happens when breaks are added to pkg_id_s. 

2252 continue 

2253 

2254 if d.package_name != pkg_id_t.package_name: 

2255 # a binary different from pkg_id_t can satisfy the dep, so 

2256 # upgrading pkg_id_t won't break this dependency 

2257 found_alternative = True 

2258 break 

2259 

2260 if d != pkg_id_s: 

2261 # We want to know the impact of the upgrade of 

2262 # pkg_id_t to pkg_id_s. If pkg_id_s migrates to the 

2263 # target suite, any other version of this binary will 

2264 # not be there, so it cannot satisfy this dependency. 

2265 # This includes pkg_id_t, but also other versions. 

2266 continue 

2267 

2268 # pkg_id_s can satisfy the dep 

2269 found_alternative = True 

2270 break 

2271 

2272 if not found_alternative: 

2273 return True 

2274 return False 

2275 

2276 def check_upgrade( 

2277 self, 

2278 pkg_id_t: BinaryPackageId, 

2279 pkg_id_s: BinaryPackageId | None, 

2280 source_name: str, 

2281 myarch: str, 

2282 broken_binaries: set[str], 

2283 excuse: "Excuse", 

2284 ) -> PolicyVerdict: 

2285 verdict = PolicyVerdict.PASS 

2286 

2287 pkg_universe = self._pkg_universe 

2288 all_binaries = self._all_binaries 

2289 

2290 # check all rdeps of the package in testing 

2291 rdeps_t = pkg_universe.reverse_dependencies_of(pkg_id_t) 

2292 

2293 for rdep_pkg in sorted(rdeps_t): 

2294 rdep_p = all_binaries[rdep_pkg] 

2295 

2296 # check some cases where the rdep won't become uninstallable, or 

2297 # where we don't care if it does 

2298 if self.should_skip_rdep(rdep_p, source_name, myarch): 

2299 continue 

2300 

2301 if not self.breaks_installability(pkg_id_t, pkg_id_s, rdep_pkg): 

2302 # if upgrading pkg_id_t to pkg_id_s doesn't break rdep_pkg, 

2303 # there is no implicit dependency 

2304 continue 

2305 

2306 # The upgrade breaks the installability of the rdep. We need to 

2307 # find out if there is a newer version of the rdep that solves the 

2308 # uninstallability. If that is the case, there is an implicit 

2309 # dependency. If not, the upgrade will fail. 

2310 

2311 # check source versions 

2312 good_newer_versions = set() 

2313 for npkg, suite in find_newer_binaries( 

2314 self.suite_info, rdep_p, add_source_for_dropped_bin=True 

2315 ): 

2316 if npkg.architecture == "source": 

2317 # When a newer version of the source package doesn't have 

2318 # the binary, we get the source as 'newer version'. In 

2319 # this case, the binary will not be uninstallable if the 

2320 # newer source migrates, because it is no longer there. 

2321 good_newer_versions.add(npkg) 

2322 continue 

2323 assert isinstance(npkg, BinaryPackageId) 

2324 if not self.breaks_installability(pkg_id_t, pkg_id_s, npkg): 

2325 good_newer_versions.add(npkg) 

2326 

2327 if good_newer_versions: 

2328 spec = DependencySpec(DependencyType.IMPLICIT_DEPENDENCY, myarch) 

2329 excuse.add_package_depends(spec, good_newer_versions) 

2330 else: 

2331 # no good newer versions: no possible solution 

2332 broken_binaries.add(rdep_pkg.name) 

2333 if pkg_id_s: 

2334 action = "migrating {} to {}".format( 

2335 pkg_id_s.name, 

2336 self.suite_info.target_suite.name, 

2337 ) 

2338 else: 

2339 action = "removing {} from {}".format( 

2340 pkg_id_t.name, 

2341 self.suite_info.target_suite.name, 

2342 ) 

2343 if rdep_pkg.package_name.endswith("-faux-build-depends"): 

2344 name = rdep_pkg.package_name.removesuffix("-faux-build-depends") 

2345 info = f'{action} makes Build-Depends of src:<a href="#{name}">{name}</a> uninstallable' 

2346 else: 

2347 info = '{0} makes <a href="#{1}">{1}</a> uninstallable'.format( 

2348 action, rdep_pkg.name 

2349 ) 

2350 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2351 excuse.add_verdict_info(verdict, info) 

2352 

2353 return verdict 

2354 

2355 def apply_srcarch_policy_impl( 

2356 self, 

2357 implicit_dep_info: dict[str, Any], 

2358 arch: str, 

2359 source_data_tdist: SourcePackage | None, 

2360 source_data_srcdist: SourcePackage, 

2361 excuse: "Excuse", 

2362 ) -> PolicyVerdict: 

2363 verdict = PolicyVerdict.PASS 

2364 

2365 if not source_data_tdist: 

2366 # this item is not currently in testing: no implicit dependency 

2367 return verdict 

2368 

2369 if excuse.hasreason("missingbuild"): 

2370 # if the build is missing, the policy would treat this as if the 

2371 # binaries would be removed, which would give incorrect (and 

2372 # confusing) info 

2373 info = "missing build, not checking implicit dependencies on %s" % (arch) 

2374 excuse.add_detailed_info(info) 

2375 return verdict 

2376 

2377 source_suite = excuse.item.suite 

2378 source_name = excuse.item.package 

2379 target_suite = self.suite_info.target_suite 

2380 all_binaries = self._all_binaries 

2381 

2382 # we check all binaries for this excuse that are currently in testing 

2383 relevant_binaries = sorted( 

2384 x 

2385 for x in source_data_tdist.binaries 

2386 if (arch == "source" or x.architecture == arch) 

2387 and x.package_name in target_suite.binaries[x.architecture] 

2388 and x.architecture not in self._new_arches 

2389 and x.architecture not in self._break_arches 

2390 and x.architecture not in self._outofsync_arches 

2391 ) 

2392 

2393 broken_binaries: set[str] = set() 

2394 

2395 assert self.hints is not None 

2396 for pkg_id_t in relevant_binaries: 

2397 mypkg = pkg_id_t.package_name 

2398 myarch = pkg_id_t.architecture 

2399 binaries_t_a = target_suite.binaries[myarch] 

2400 binaries_s_a = source_suite.binaries[myarch] 

2401 

2402 if target_suite.is_cruft(all_binaries[pkg_id_t]): 

2403 # this binary is cruft in testing: it will stay around as long 

2404 # as necessary to satisfy dependencies, so we don't need to 

2405 # care 

2406 continue 

2407 

2408 if mypkg in binaries_s_a: 

2409 mybin = binaries_s_a[mypkg] 

2410 pkg_id_s: Optional["BinaryPackageId"] = mybin.pkg_id 

2411 if mybin.source != source_name: 

2412 # hijack: this is too complicated to check, so we ignore 

2413 # it (the migration code will check the installability 

2414 # later anyway) 

2415 pass 

2416 elif mybin.source_version != source_data_srcdist.version: 

2417 # cruft in source suite: pretend the binary doesn't exist 

2418 pkg_id_s = None 

2419 elif pkg_id_t == pkg_id_s: 

2420 # same binary (probably arch: all from a binNMU): 

2421 # 'upgrading' doesn't change anything, for this binary, so 

2422 # it won't break anything 

2423 continue 

2424 else: 

2425 pkg_id_s = None 

2426 

2427 if not pkg_id_s and is_smooth_update_allowed( 

2428 binaries_t_a[mypkg], self._smooth_updates, self.hints 

2429 ): 

2430 # the binary isn't in the new version (or is cruft there), and 

2431 # smooth updates are allowed: the binary can stay around if 

2432 # that is necessary to satisfy dependencies, so we don't need 

2433 # to check it 

2434 continue 

2435 

2436 if ( 

2437 not pkg_id_s 

2438 and source_data_tdist.version == source_data_srcdist.version 

2439 and source_suite.suite_class is SuiteClass.ADDITIONAL_SOURCE_SUITE 

2440 and binaries_t_a[mypkg].architecture == "all" 

2441 ): 

2442 # we're very probably migrating a binNMU built in tpu where the arch:all 

2443 # binaries were not copied to it as that's not needed. This policy could 

2444 # needlessly block. 

2445 continue 

2446 

2447 v = self.check_upgrade( 

2448 pkg_id_t, pkg_id_s, source_name, myarch, broken_binaries, excuse 

2449 ) 

2450 verdict = PolicyVerdict.worst_of(verdict, v) 

2451 

2452 # each arch is processed separately, so if we already have info from 

2453 # other archs, we need to merge the info from this arch 

2454 broken_old = set(implicit_dep_info.get("broken-binaries", [])) 

2455 implicit_dep_info["broken-binaries"] = sorted(broken_old | broken_binaries) 

2456 

2457 return verdict 

2458 

2459 

2460class ReverseRemovalPolicy(AbstractBasePolicy): 

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

2462 super().__init__( 

2463 "reverseremoval", 

2464 options, 

2465 suite_info, 

2466 {SuiteClass.PRIMARY_SOURCE_SUITE, SuiteClass.ADDITIONAL_SOURCE_SUITE}, 

2467 ) 

2468 

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

2470 hint_parser.register_hint_type(HintType("ignore-reverse-remove")) 

2471 

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

2473 super().initialise(britney) 

2474 

2475 pkg_universe = britney.pkg_universe 

2476 source_suites = britney.suite_info.source_suites 

2477 target_suite = britney.suite_info.target_suite 

2478 

2479 # Build set of the sources of reverse (Build-) Depends 

2480 assert self.hints is not None 

2481 

2482 rev_bin: dict[BinaryPackageId, set[str]] = defaultdict(set) 

2483 for hint in self.hints.search("remove"): 

2484 for item in hint.packages: 

2485 # I think we don't need to look at the target suite 

2486 for src_suite in source_suites: 

2487 try: 

2488 # Explicitly not running filter_out_faux here 

2489 my_bins = set(src_suite.sources[item.uvname].binaries) 

2490 except KeyError: 

2491 continue 

2492 compute_reverse_tree(pkg_universe, my_bins) 

2493 for this_bin in my_bins: 

2494 rev_bin.setdefault(this_bin, set()).add(item.uvname) 

2495 

2496 rev_src: dict[str, set[str]] = defaultdict(set) 

2497 for bin_pkg, reasons in rev_bin.items(): 

2498 # If the pkg is in the target suite, there's nothing this 

2499 # policy wants to do. 

2500 if target_suite.is_pkg_in_the_suite(bin_pkg): 

2501 continue 

2502 that_bin = britney.all_binaries[bin_pkg] 

2503 bin_src = that_bin.source + "/" + that_bin.source_version 

2504 rev_src.setdefault(bin_src, set()).update(reasons) 

2505 self._block_src_for_rm_hint = rev_src 

2506 

2507 def apply_src_policy_impl( 

2508 self, 

2509 rev_remove_info: dict[str, Any], 

2510 source_data_tdist: SourcePackage | None, 

2511 source_data_srcdist: SourcePackage, 

2512 excuse: "Excuse", 

2513 ) -> PolicyVerdict: 

2514 verdict = PolicyVerdict.PASS 

2515 

2516 item = excuse.item 

2517 if item.name in self._block_src_for_rm_hint: 

2518 reason = ", ".join(sorted(self._block_src_for_rm_hint[item.name])) 

2519 assert self.hints is not None 

2520 excuse.addreason("reverseremoval") 

2521 if ( 

2522 ignore_hint := self.hints.search_first( 

2523 "ignore-reverse-remove", package=item.uvname, version=item.version 

2524 ) 

2525 ) is not None: 

2526 excuse.addreason("ignore-reverse-remove") 

2527 excuse.addinfo( 

2528 "Should block migration because of remove hint for %s, but forced by %s" 

2529 % (reason, ignore_hint.user) 

2530 ) 

2531 verdict = PolicyVerdict.PASS_HINTED 

2532 else: 

2533 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2534 excuse.add_verdict_info( 

2535 verdict, "Remove hint for (transitive) dependency: %s" % reason 

2536 ) 

2537 

2538 return verdict 

2539 

2540 

2541class ReproducibleState(Enum): 

2542 BAD = auto() 

2543 FAIL = auto() 

2544 GOOD = auto() 

2545 UNKNOWN = auto() 

2546 

2547 @staticmethod 

2548 def from_str(val: str | None) -> "ReproducibleState": 

2549 match val: 

2550 case "BAD": 

2551 return ReproducibleState.BAD 

2552 case "FAIL": 2552 ↛ 2553line 2552 didn't jump to line 2553 because the pattern on line 2552 never matched

2553 return ReproducibleState.FAIL 

2554 case "GOOD": 

2555 return ReproducibleState.GOOD 

2556 case "UNKNOWN" | None: 2556 ↛ 2558line 2556 didn't jump to line 2558 because the pattern on line 2556 always matched

2557 return ReproducibleState.UNKNOWN 

2558 case _: 

2559 raise ValueError(f"Invalid reproducability state f{str}") 

2560 

2561 

2562@dataclass(slots=True, frozen=True) 

2563class ReproducibleData: 

2564 state: ReproducibleState 

2565 build_id: str | None = field(default=None, kw_only=True) 

2566 diffoscope_log_id: str | None = field(default=None, kw_only=True) 

2567 artifact_id: str | None = field(default=None, kw_only=True) 

2568 

2569 

2570class ReproduciblePolicy(AbstractBasePolicy): 

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

2572 super().__init__( 

2573 "reproducible", 

2574 options, 

2575 suite_info, 

2576 {SuiteClass.PRIMARY_SOURCE_SUITE}, 

2577 ApplySrcPolicy.RUN_ON_EVERY_ARCH_ONLY, 

2578 ) 

2579 self._reproducible: dict[str, dict[tuple[str, str], ReproducibleData]] = {} 

2580 self._components: tuple[str, ...] = () 

2581 

2582 # Default values for this policy's options 

2583 parse_option(options, "repro_success_bounty", default=0, to_int=True) 

2584 parse_option(options, "repro_regression_penalty", default=0, to_int=True) 

2585 parse_option(options, "repro_log_url") 

2586 parse_option(options, "repro_excuse_url") 

2587 parse_option(options, "repro_retry_url") 

2588 parse_option(options, "repro_components") 

2589 

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

2591 hint_parser.register_hint_type( 

2592 HintType( 

2593 "ignore-reproducible-src", 

2594 versioned=HintAnnotate.OPTIONAL, 

2595 architectured=HintAnnotate.OPTIONAL, 

2596 ) 

2597 ) 

2598 hint_parser.register_hint_type( 

2599 HintType( 

2600 "ignore-reproducible", 

2601 versioned=HintAnnotate.OPTIONAL, 

2602 architectured=HintAnnotate.OPTIONAL, 

2603 ) 

2604 ) 

2605 

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

2607 super().initialise(britney) 

2608 summary = self._reproducible 

2609 

2610 valid_release_names = { 

2611 suite.codename 

2612 for suite in chain( 

2613 (britney.suite_info.target_suite,), 

2614 britney.suite_info.source_suites, 

2615 ) 

2616 } | { 

2617 suite.name 

2618 for suite in chain( 

2619 (britney.suite_info.target_suite,), 

2620 britney.suite_info.source_suites, 

2621 ) 

2622 } 

2623 

2624 assert hasattr( 

2625 self, "state_dir" 

2626 ), "Please set STATE_DIR in the britney configuration" 

2627 assert ( 

2628 self.options.repro_components 

2629 ), "Please set REPRO_COMPONENTS in the britney configuration" 

2630 self._components = tuple(self.options.repro_components.split()) 

2631 

2632 for file in os.listdir(self.state_dir): 

2633 if not file.startswith("reproducible-") or not file.endswith(".json"): 

2634 continue 

2635 filename = os.path.join(self.state_dir, file) 

2636 

2637 self.logger.info("Loading reproducibility report from %s", filename) 

2638 with open(filename) as fd: 

2639 if os.fstat(fd.fileno()).st_size < 1: 

2640 continue 

2641 data = json.load(fd) 

2642 

2643 for result in data["records"]: 

2644 if ( 2644 ↛ 2648line 2644 didn't jump to line 2648 because the condition on line 2644 was never true

2645 release := result.get("release") 

2646 ) is not None and release not in valid_release_names: 

2647 # tests do not have a release set 

2648 continue 

2649 

2650 state = ReproducibleState.from_str(result.get("status")) 

2651 repo = { 

2652 key: value 

2653 for key, value in result.items() 

2654 if key in ("build_id", "diffoscope_log_id", "artifact_id") 

2655 } 

2656 

2657 summary.setdefault(result["architecture"], {})[ 

2658 (result["name"], result["version"]) 

2659 ] = ReproducibleData(state, **repo) 

2660 

2661 def _lookup_data( 

2662 self, package_name: str, version: str, arch: str 

2663 ) -> tuple[ReproducibleData, str] | None: 

2664 key = (package_name, version) 

2665 if (repo := self._reproducible[arch].get(key)) is not None: 

2666 return repo, arch 

2667 

2668 repo = self._reproducible["all"].get(key) 

2669 return (repo, "all") if repo is not None else None 

2670 

2671 def _format_link(self, bpid: BinaryPackageId, arch: str) -> str: 

2672 data = self._lookup_data(bpid.package_name, bpid.version, arch) 

2673 assert data is not None 

2674 repo, arch = data 

2675 if repo.diffoscope_log_id and (diff_id := repo.artifact_id): 2675 ↛ 2676line 2675 didn't jump to line 2676 because the condition on line 2675 was never true

2676 endpoint = f"artifacts/{diff_id}/diffoscope" 

2677 else: 

2678 endpoint = "log" 

2679 url = self.options.repro_log_url.format( 

2680 arch=arch, build_id=repo.build_id, endpoint=endpoint 

2681 ) 

2682 return f'<a href="{url}">{bpid.package_name}</a>' 

2683 

2684 def _create_link_to_log(self, arch: str, failed_bpids: set[BinaryPackageId]) -> str: 

2685 if not self.options.repro_log_url: 2685 ↛ 2686line 2685 didn't jump to line 2686 because the condition on line 2685 was never true

2686 return ": " + ", ".join(bpid.package_name for bpid in sorted(failed_bpids)) 

2687 

2688 return ": " + ", ".join( 

2689 self._format_link(bpid, arch) for bpid in sorted(failed_bpids) 

2690 ) 

2691 

2692 def apply_srcarch_policy_impl( 

2693 self, 

2694 policy_info: dict[str, Any], 

2695 arch: str, 

2696 source_data_tdist: SourcePackage | None, 

2697 source_data_srcdist: SourcePackage, 

2698 excuse: "Excuse", 

2699 ) -> PolicyVerdict: 

2700 verdict = PolicyVerdict.PASS 

2701 eligible_for_bounty = False 

2702 all_hints = [] 

2703 

2704 assert self.hints is not None # Needed for type checking / mypy 

2705 

2706 # we don't want to apply this policy (yet) on binNMUs 

2707 if excuse.item.architecture != "source": 2707 ↛ 2708line 2707 didn't jump to line 2708 because the condition on line 2707 was never true

2708 return verdict 

2709 

2710 # we're not supposed to judge on this arch 

2711 if arch not in self.options.repro_arches: 2711 ↛ 2712line 2711 didn't jump to line 2712 because the condition on line 2711 was never true

2712 return verdict 

2713 

2714 # bail out if this arch has no packages for this source (not build 

2715 # here) 

2716 if arch not in excuse.packages: 2716 ↛ 2717line 2716 didn't jump to line 2717 because the condition on line 2716 was never true

2717 return verdict 

2718 

2719 component = get_component(source_data_srcdist.section) 

2720 

2721 if self._components and component not in self._components: 2721 ↛ 2722line 2721 didn't jump to line 2722 because the condition on line 2721 was never true

2722 self.logger.debug( 

2723 "%s skipping reproducible policy, component %s not requested", 

2724 excuse.name, 

2725 component, 

2726 ) 

2727 return verdict 

2728 

2729 source_name = excuse.item.package 

2730 

2731 if self.options.repro_excuse_url: 

2732 url = self.options.repro_excuse_url.format( 

2733 package=quote(source_name), arch=arch 

2734 ) 

2735 url_html = ' - <a href="%s">info</a>' % url 

2736 # When run on multiple archs, the last one "wins" 

2737 policy_info["status-url"] = url 

2738 else: 

2739 url = None 

2740 url_html = "" 

2741 

2742 if arch not in self._reproducible: 2742 ↛ 2743line 2742 didn't jump to line 2743 because the condition on line 2742 was never true

2743 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

2744 msg = f"No reproducibility data available at all for {arch}" 

2745 excuse.add_verdict_info(verdict, msg) 

2746 return verdict 

2747 if "all" not in self._reproducible: 2747 ↛ 2748line 2747 didn't jump to line 2748 because the condition on line 2747 was never true

2748 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

2749 msg = "No reproducibility data available at all for arch:all" 

2750 excuse.add_verdict_info(verdict, msg) 

2751 return verdict 

2752 

2753 # skip/delay policy until both arch:arch and arch:all builds are done 

2754 if (arch or "all") in excuse.missing_builds: 2754 ↛ 2755line 2754 didn't jump to line 2755 because the condition on line 2754 was never true

2755 self.logger.debug( 

2756 "%s not built for %s or all, skipping reproducible policy", 

2757 excuse.name, 

2758 arch, 

2759 ) 

2760 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

2761 excuse.add_verdict_info( 

2762 verdict, 

2763 f"Reproducibility check deferred on {arch}: missing builds{url_html}", 

2764 ) 

2765 return verdict 

2766 

2767 source_suite_state = "not-unknown" 

2768 failed_bpids: set[BinaryPackageId] = set() 

2769 # The states should either be GOOD/BAD for all binaries, UNKNOWN for all 

2770 # binaries, or missing for all binaries, but let's not assume that. 

2771 # They can be from different components after all. 

2772 bins_src, src_suite_name = binaries_from_source_version( 

2773 source_data_srcdist, self.suite_info 

2774 ) 

2775 for bpid in bins_src: 

2776 if bpid.architecture not in ("all", arch): 2776 ↛ 2777line 2776 didn't jump to line 2777 because the condition on line 2776 was never true

2777 continue 

2778 in_component = True 

2779 for suite in self.suite_info.source_suites: 

2780 if suite.name == src_suite_name and ( 

2781 ( 

2782 component := get_component( 

2783 suite.all_binaries_in_suite[bpid].section 

2784 ) 

2785 ) 

2786 not in self._components 

2787 ): 

2788 self.logger.debug( 

2789 "repro check for %s skipped due to component %s", 

2790 bpid, 

2791 component, 

2792 ) 

2793 in_component = False 

2794 break 

2795 if not in_component: 

2796 # TODO: should we update the excuses text? 

2797 continue 

2798 

2799 if ( 

2800 data := self._lookup_data(bpid.package_name, bpid.version, arch) 

2801 ) is not None: 

2802 pkg_info, _ = data 

2803 self.logger.debug("repro data for %s: %s", bpid, pkg_info.state) 

2804 if pkg_info.state is ReproducibleState.BAD: 

2805 failed_bpids.add(bpid) 

2806 # not changing source_suite_state here on purpose 

2807 elif ( 2807 ↛ 2811line 2807 didn't jump to line 2811

2808 pkg_info.state is ReproducibleState.FAIL 

2809 or pkg_info.state is ReproducibleState.UNKNOWN 

2810 ): 

2811 source_suite_state = "unknown" 

2812 else: 

2813 self.logger.debug("No repro data found for %s", bpid) 

2814 # but maybe it's hinted (e.g. at the time of writing 

2815 # reproduce.debian.net has a bug where udebs go missing) 

2816 if ( 

2817 bpid_hints := self.hints.search_first( 

2818 "ignore-reproducible", 

2819 package=bpid.package_name, 

2820 version=bpid.version, 

2821 architecture=bpid.architecture, 

2822 ) 

2823 ) is not None: 

2824 all_hints.append(bpid_hints) 

2825 self.logger.debug("repro: hint found for %s: %s", source_name, bpid) 

2826 else: 

2827 source_suite_state = "unknown" 

2828 break 

2829 

2830 if source_suite_state == "not-unknown": 

2831 source_suite_state = "known" 

2832 

2833 excuse_info = [] 

2834 if source_suite_state == "unknown": 

2835 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

2836 excuse_info.append( 

2837 f"Reproducibility check waiting for results on {arch}{url_html}" 

2838 ) 

2839 policy_info.setdefault("state", {}).setdefault(arch, "unavailable") 

2840 elif failed_bpids: 

2841 ignored_bpids: set[BinaryPackageId] = set() 

2842 if source_data_tdist is None: 2842 ↛ 2843line 2842 didn't jump to line 2843 because the condition on line 2842 was never true

2843 target_suite_state = "new" 

2844 else: 

2845 target_suite_state = "reproducible" 

2846 for bpid in failed_bpids: 

2847 pkg_name = bpid.package_name 

2848 for bpid_t in filter_out_faux_gen(source_data_tdist.binaries): 

2849 if bpid_t.architecture not in ("all", arch): 2849 ↛ 2850line 2849 didn't jump to line 2850 because the condition on line 2849 was never true

2850 continue 

2851 if pkg_name != bpid_t.package_name: 2851 ↛ 2852line 2851 didn't jump to line 2852 because the condition on line 2851 was never true

2852 continue 

2853 if ( 2853 ↛ 2868line 2853 didn't jump to line 2868 because the condition on line 2853 was always true

2854 data := self._lookup_data(pkg_name, bpid_t.version, arch) 

2855 ) is not None: 

2856 pkg_info, _ = data 

2857 self.logger.debug( 

2858 "testing repro data for %s: %s", bpid_t, pkg_info.state 

2859 ) 

2860 if pkg_info.state is ReproducibleState.BAD: 

2861 ignored_bpids.add(bpid) 

2862 elif ( 2862 ↛ 2866line 2862 didn't jump to line 2866

2863 pkg_info.state is ReproducibleState.FAIL 

2864 or pkg_info.state is ReproducibleState.UNKNOWN 

2865 ): 

2866 target_suite_state = "unknown" 

2867 else: 

2868 self.logger.debug( 

2869 "No testing repro data found for %s", bpid_t 

2870 ) 

2871 # This shouldn't happen as for the past migration 

2872 # to have been allowed, there should be data. 

2873 target_suite_state = "unknown" 

2874 break 

2875 

2876 # Reminder: code here is part of the non-reproducibile source-suite branch 

2877 if target_suite_state == "new": 2877 ↛ 2878line 2877 didn't jump to line 2878 because the condition on line 2877 was never true

2878 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2879 excuse_info.append( 

2880 f"New but not reproduced on {arch}{url_html}" 

2881 f"{self._create_link_to_log(arch, failed_bpids)}" 

2882 ) 

2883 policy_info.setdefault("state", {}).setdefault( 

2884 arch, "new but not reproducible" 

2885 ) 

2886 elif target_suite_state == "unknown": 2886 ↛ 2888line 2886 didn't jump to line 2888 because the condition on line 2886 was never true

2887 # Shouldn't happen after initial bootstrap once blocking 

2888 verdict = PolicyVerdict.REJECTED_TEMPORARILY 

2889 excuse_info.append( 

2890 f"Reproducibility check failed and now waiting for reference " 

2891 f"results on {arch}{url_html}" 

2892 f"{self._create_link_to_log(arch, failed_bpids)}" 

2893 ) 

2894 policy_info.setdefault("state", {}).setdefault( 

2895 arch, "waiting for reference" 

2896 ) 

2897 elif failed_bpids <= ignored_bpids: 

2898 # For the forseeable future we want to prevent regressions, one day 

2899 # we might want to even block these. 

2900 # verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2901 excuse_info.append( 

2902 f"Not reproduced on {arch} (not a regression)" 

2903 f"{self._create_link_to_log(arch, failed_bpids)}" 

2904 ) 

2905 policy_info.setdefault("state", {}).setdefault(arch, "not reproducible") 

2906 else: 

2907 verdict = PolicyVerdict.REJECTED_PERMANENTLY 

2908 excuse_info.append( 

2909 f"Reproducibility regression on {arch}" 

2910 f"{self._create_link_to_log(arch, failed_bpids - ignored_bpids)}" 

2911 ) 

2912 policy_info.setdefault("state", {}).setdefault(arch, "regression") 

2913 

2914 # non-reproducible source-suite cases are handled above, so here we 

2915 # handle the last of the source-suite cases 

2916 else: 

2917 excuse_info.append(f"Reproduced on {arch}{url_html}") 

2918 policy_info.setdefault("state", {}).setdefault(arch, "reproducible") 

2919 eligible_for_bounty = True 

2920 

2921 if verdict.is_rejected: 

2922 for hint_arch in ("source", arch): 

2923 if ( 

2924 ignore_hint := self.hints.search_first( 

2925 "ignore-reproducible-src", 

2926 package=source_name, 

2927 version=source_data_srcdist.version, 

2928 architecture=hint_arch, 

2929 ) 

2930 ) is not None: 

2931 # one hint is enough, take the first one encountered 

2932 verdict = PolicyVerdict.PASS_HINTED 

2933 policy_info.setdefault("hints", {}).setdefault(arch, []).append( 

2934 f"{ignore_hint.user}: {str(ignore_hint)}" 

2935 ) 

2936 if hint_arch == arch: 2936 ↛ 2939line 2936 didn't jump to line 2939 because the condition on line 2936 was always true

2937 on_arch = f" on {arch}" 

2938 else: 

2939 on_arch = "" 

2940 excuse_info.append( 

2941 f"Reproducibility issues ignored for src:{ignore_hint.package}" 

2942 f"{on_arch} as requested by {ignore_hint.user}" 

2943 ) 

2944 break 

2945 

2946 if verdict.is_rejected: 

2947 if source_suite_state == "known": 

2948 check_bpids = failed_bpids - ignored_bpids 

2949 else: 

2950 # Let's not wait for results if all binaries have a hint 

2951 check_bpids = filter_out_faux(source_data_srcdist.binaries) 

2952 missed_bpids = set() 

2953 

2954 for bpid in check_bpids: 

2955 if ( 

2956 bpid_hint := self.hints.search_first( 

2957 "ignore-reproducible", 

2958 package=bpid.package_name, 

2959 version=bpid.version, 

2960 architecture=bpid.architecture, 

2961 ) 

2962 ) is not None: 

2963 # one hint per binary is enough 

2964 all_hints.append(bpid_hint) 

2965 self.logger.debug( 

2966 "repro: hint found for %s: %s", source_name, bpid 

2967 ) 

2968 else: 

2969 missed_bpids.add(bpid) 

2970 

2971 if not missed_bpids: 

2972 verdict = PolicyVerdict.PASS_HINTED 

2973 for hint in all_hints: 

2974 policy_info.setdefault("hints", {}).setdefault(arch, []).append( 

2975 hint.user + ": " + str(hint) 

2976 ) 

2977 # TODO: we're going to print this for arch:all binaries on each arch 

2978 excuse_info.append( 

2979 f"Reproducibility issues ignored for {hint.package} on {arch} as " 

2980 f"requested by {hint.user}" 

2981 ) 

2982 elif all_hints: 2982 ↛ 2983line 2982 didn't jump to line 2983 because the condition on line 2982 was never true

2983 self.logger.info( 

2984 "repro: binary hints for %s ignored as they don't cover these binaries %s", 

2985 source_name, 

2986 missed_bpids, 

2987 ) 

2988 

2989 # A binary without results got hinted 

2990 if not verdict.is_rejected and all_hints: 

2991 for hint in all_hints: 

2992 excuse_info.append( 

2993 f"Reproducibility unknown for {hint.package} but ignored on {arch} as " 

2994 f"requested by {hint.user}" 

2995 ) 

2996 

2997 if self.options.repro_success_bounty and eligible_for_bounty: 2997 ↛ 2998line 2997 didn't jump to line 2998 because the condition on line 2997 was never true

2998 excuse.add_bounty("reproducibility", self.options.repro_success_bounty) 

2999 

3000 if verdict.is_rejected and self.options.repro_regression_penalty: 3000 ↛ 3002line 3000 didn't jump to line 3002 because the condition on line 3000 was never true

3001 # With a non-zero penalty, we shouldn't block on this policy 

3002 verdict = PolicyVerdict.PASS 

3003 if self.options.repro_regression_penalty > 0: 

3004 excuse.add_penalty( 

3005 "reproducibility", self.options.repro_regression_penalty 

3006 ) 

3007 

3008 for msg in excuse_info: 

3009 if verdict.is_rejected: 

3010 excuse.add_verdict_info(verdict, msg) 

3011 else: 

3012 excuse.addinfo(msg) 

3013 

3014 return verdict