assorted_debian_utils/
excuses.rs

1// Copyright 2021-2022 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Helpers to handle `excuses.yaml` for testing migration
5//!
6//! This module provides helpers to deserialize [excuses.yaml](https://release.debian.org/britney/excuses.yaml)
7//! with [serde]. Note however, that this module only handles a biased selection of fields.
8
9use std::{collections::HashMap, fmt::Formatter, io};
10
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Deserializer, de};
13use smallvec::SmallVec;
14
15use crate::{
16    architectures::Architecture, archive::Component, package::PackageName, utils::DateTimeVisitor,
17    version::PackageVersion,
18};
19
20/// Deserialize a datetime string into a `DateTime<Utc>`
21fn deserialize_datetime<'de, D>(deserializer: D) -> std::result::Result<DateTime<Utc>, D::Error>
22where
23    D: Deserializer<'de>,
24{
25    deserializer.deserialize_str(DateTimeVisitor("%Y-%m-%d %H:%M:%S%.f%:z"))
26}
27
28/// Deserialize a version or '-' as `PackageVersion` or `None`
29fn deserialize_version<'de, D>(
30    deserializer: D,
31) -> std::result::Result<Option<PackageVersion>, D::Error>
32where
33    D: Deserializer<'de>,
34{
35    #[derive(Debug)]
36    struct Visitor;
37
38    impl de::Visitor<'_> for Visitor {
39        type Value = Option<PackageVersion>;
40
41        fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
42            write!(formatter, "a package version or '-'")
43        }
44
45        fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
46        where
47            E: de::Error,
48        {
49            if s == "-" {
50                Ok(None)
51            } else {
52                PackageVersion::try_from(s)
53                    .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(s), &self))
54                    .map(Some)
55            }
56        }
57    }
58
59    deserializer.deserialize_str(Visitor)
60}
61
62/// The excuses.
63#[derive(Debug, PartialEq, Eq, Deserialize)]
64#[serde(rename_all = "kebab-case")]
65pub struct Excuses {
66    /// Date of the run that produced `excuses.yaml`
67    #[serde(deserialize_with = "deserialize_datetime")]
68    pub generated_date: DateTime<Utc>,
69    /// All excuse items
70    ///
71    /// While not every excuses item relates to a source package, the field is still named that way in `excuses.yaml`
72    pub sources: Vec<ExcusesItem>,
73}
74
75/// A policy's verdict
76#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
77pub enum Verdict {
78    /// Policy passed
79    #[serde(rename = "PASS")]
80    Pass,
81    /// Policy passed due to a hint
82    #[serde(rename = "PASS_HINTED")]
83    PassHinted,
84    /// Rejected due to a block hint or because the upload requires explicit approval (e.g.,
85    /// uploads to proposed-updates or testing-proposed-updates)
86    #[serde(rename = "REJECTED_NEEDS_APPROVAL")]
87    RejectedNeedsApproval,
88    /// Rejected tu to a permanent issue
89    #[serde(rename = "REJECTED_PERMANENTLY")]
90    RejectedPermanently,
91    /// Rejected due to a transient issue
92    #[serde(rename = "REJECTED_TEMPORARILY")]
93    RejectedTemporarily,
94    /// Rejected, but not able to determine if the issue is transient
95    #[serde(rename = "REJECTED_CANNOT_DETERMINE_IF_PERMANENT")]
96    RejectedCannotDetermineIfPermanent,
97    /// Reject due to another blocking item.
98    #[serde(rename = "REJECTED_BLOCKED_BY_ANOTHER_ITEM")]
99    RejectedBlockedByAnotherItem,
100    /// Reject due to another blocking item.
101    #[serde(rename = "REJECTED_WAITING_FOR_ANOTHER_ITEM")]
102    RejectedWaitingForAnotherItem,
103}
104
105/// Age policy info
106#[derive(Debug, PartialEq, Eq, Deserialize)]
107#[serde(rename_all = "kebab-case")]
108pub struct AgeInfo {
109    /// The required age
110    pub age_requirement: u32,
111    /// The current age
112    pub current_age: u32,
113    /// The verdict
114    pub verdict: Verdict,
115}
116
117/// Catch-all policy info
118#[derive(Debug, PartialEq, Eq, Deserialize)]
119#[serde(rename_all = "kebab-case")]
120pub struct UnspecfiedPolicyInfo {
121    /// The verdict
122    pub verdict: Verdict,
123}
124
125/// Built-on-buildd policy info
126#[derive(Debug, PartialEq, Eq, Deserialize)]
127#[serde(rename_all = "kebab-case")]
128pub struct BuiltOnBuildd {
129    /// The signers for each architecture
130    pub signed_by: HashMap<Architecture, Option<String>>,
131    /// The verdict
132    pub verdict: Verdict,
133}
134
135/// Collected policy infos
136#[derive(Debug, PartialEq, Eq, Deserialize)]
137#[serde(rename_all = "kebab-case")]
138pub struct PolicyInfo {
139    /// The age policy
140    pub age: Option<AgeInfo>,
141    /// The buildt-on-buildd policy
142    pub builtonbuildd: Option<BuiltOnBuildd>,
143    /// The autopkgtest porlicy
144    pub autopkgtest: Option<UnspecfiedPolicyInfo>,
145    /// All remaining policies
146    #[serde(flatten)]
147    pub extras: HashMap<String, UnspecfiedPolicyInfo>,
148    /*
149        autopkgtest: Option<UnspecfiedPolicyInfo>,
150        block: Option<UnspecfiedPolicyInfo>,
151        build_depends: Option<UnspecfiedPolicyInfo>,
152        built_using:  Option<UnspecfiedPolicyInfo>,
153        depends: Option<UnspecfiedPolicyInfo>,
154        piuparts: Option<UnspecfiedPolicyInfo>,
155        rc_bugs: Option<UnspecfiedPolicyInfo>,
156    */
157}
158
159/// List of missing builds
160#[derive(Debug, PartialEq, Eq, Deserialize)]
161#[serde(rename_all = "kebab-case")]
162pub struct MissingBuilds {
163    /// Architectures where builds are missing
164    // 16 is arbitrary, but is large enough to hold all current release architectures
165    pub on_architectures: SmallVec<[Architecture; 16]>,
166}
167
168/// A source package's excuses
169#[derive(Debug, PartialEq, Eq, Deserialize)]
170#[serde(rename_all = "kebab-case")]
171pub struct ExcusesItem {
172    /// Maintainer of the package
173    pub maintainer: Option<String>,
174    /// The item is a candidate for migration
175    pub is_candidate: bool,
176    /// Version in the source suite, i.e., the version to migrate
177    ///
178    /// If the value is `None`, the package is being removed.
179    #[serde(deserialize_with = "deserialize_version")]
180    pub new_version: Option<PackageVersion>,
181    /// Version in the target suite
182    ///
183    /// If the value is `None`, the package is not yet available in the target suite.
184    #[serde(deserialize_with = "deserialize_version")]
185    pub old_version: Option<PackageVersion>,
186    /// Migration item name
187    pub item_name: String,
188    /// Source package name
189    pub source: PackageName,
190    /// Migration is blocked by another package
191    pub invalidated_by_other_package: Option<bool>,
192    /// Component of the source package
193    pub component: Option<Component>,
194    /// Missing builds
195    pub missing_builds: Option<MissingBuilds>,
196    /// Policy info
197    #[serde(rename = "policy_info")]
198    pub policy_info: Option<PolicyInfo>,
199    /// The excuses
200    pub excuses: Vec<String>,
201    /// Combined verdict
202    pub migration_policy_verdict: Verdict,
203}
204
205impl ExcusesItem {
206    /// Excuses item refers to package removal
207    pub fn is_removal(&self) -> bool {
208        self.new_version.is_none()
209    }
210
211    /// Excuses item refers to a binNMU
212    pub fn is_binnmu(&self) -> bool {
213        self.new_version == self.old_version
214    }
215
216    /// Get architecture of the binNMU or `None`
217    pub fn binnmu_arch(&self) -> Option<Architecture> {
218        self.item_name.split_once('/').map(|(_, arch)| {
219            arch.split_once('_')
220                .map_or(arch, |(arch, _)| arch)
221                .try_into()
222                .unwrap()
223        })
224    }
225
226    /// Excuses item refers to an item in (stable) proposed-updates
227    pub fn is_from_pu(&self) -> bool {
228        self.item_name.ends_with("_pu")
229    }
230
231    /// Excuses item refers to an item in testing-proposed-updates
232    pub fn is_from_tpu(&self) -> bool {
233        self.item_name.ends_with("_tpu")
234    }
235}
236
237/// Result type
238pub type Result<T> = serde_yaml::Result<T>;
239
240/// Read excuses from a reader
241pub fn from_reader(reader: impl io::Read) -> Result<Excuses> {
242    serde_yaml::from_reader(reader)
243}
244
245/// Read excuses from a string
246pub fn from_str(data: &str) -> Result<Excuses> {
247    serde_yaml::from_str(data)
248}