1use serde::{Deserialize, Serialize};
8
9use crate::actions::Via;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum Producer {
15 Review,
17 BugConfirmation,
19 PersonTest,
21}
22
23impl Producer {
24 #[must_use]
26 pub const fn source(self) -> AcceptanceSource {
27 match self {
28 Self::Review => AcceptanceSource::Review,
29 Self::BugConfirmation => AcceptanceSource::BugConfirmation,
30 Self::PersonTest => AcceptanceSource::PersonTest,
31 }
32 }
33
34 #[must_use]
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::Review => "review",
39 Self::BugConfirmation => "bug_confirmation",
40 Self::PersonTest => "person_test",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
47#[serde(rename_all = "snake_case")]
48pub enum AcceptanceSource {
49 Authored,
51 Review,
53 BugConfirmation,
55 PersonTest,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
61pub struct VisibleTest {
62 pub path: String,
64 pub sha256: String,
66 pub cites: Vec<String>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
72pub struct SealedRef {
73 pub key: String,
75 pub manifest_sha256: String,
77 pub count: usize,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
83pub struct Decided {
84 pub by: String,
86 pub via: Via,
88 pub at: u64,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
94pub struct PreFrozenAcceptance {
95 pub unit: String,
97 pub requirement: String,
99 pub wording_digest: String,
101 pub approval_digest: String,
103 pub visible: Vec<VisibleTest>,
105 pub held_out_sealed: Option<SealedRef>,
107 pub producer: Producer,
109 pub decided: Decided,
111}
112
113impl PreFrozenAcceptance {
114 pub fn validate(&self) -> Result<(), String> {
121 if self.visible.is_empty() {
122 return Err("a pre-frozen set needs at least one visible test".to_owned());
123 }
124 if let Some(t) = self.visible.iter().find(|t| t.cites.is_empty()) {
125 return Err(format!("{} cites nothing", t.path));
126 }
127 Ok(())
128 }
129
130 #[must_use]
132 pub fn is_current(&self, wording_now: &str) -> bool {
133 current(self, wording_now)
134 }
135
136 #[must_use]
139 pub fn trailer(&self, manifest: &str) -> String {
140 format!(
141 "Acceptance: {} manifest={manifest} approval={} wording={} by={}\n",
142 self.producer.as_str(),
143 self.approval_digest,
144 self.wording_digest,
145 self.decided
146 .by
147 .split_whitespace()
148 .collect::<Vec<_>>()
149 .join(" ")
150 )
151 }
152}
153
154#[must_use]
156pub fn current(record: &PreFrozenAcceptance, wording_now: &str) -> bool {
157 record.wording_digest == wording_now
158}
159
160#[must_use]
163pub fn manifest_text(visible: &[VisibleTest]) -> String {
164 let mut lines: Vec<String> = visible
165 .iter()
166 .map(|t| format!("{}={}", t.path, t.sha256))
167 .collect();
168 lines.sort();
169 lines.join("\n")
170}
171
172#[cfg(test)]
173mod tests {
174 use proptest::prelude::*;
175
176 use super::*;
177
178 fn record(wording: &str) -> PreFrozenAcceptance {
179 PreFrozenAcceptance {
180 unit: "007-bugfixes".to_owned(),
181 requirement: "FR-003".to_owned(),
182 wording_digest: wording.to_owned(),
183 approval_digest: "sha256:a".to_owned(),
184 visible: vec![VisibleTest {
185 path: "tests/repro_e1.rs".to_owned(),
186 sha256: "sha256:t".to_owned(),
187 cites: vec!["E1".to_owned()],
188 }],
189 held_out_sealed: None,
190 producer: Producer::BugConfirmation,
191 decided: Decided {
192 by: "Ada".to_owned(),
193 via: Via::Terminal,
194 at: 1,
195 },
196 }
197 }
198
199 proptest! {
200 #[test]
201 fn any_wording_change_lapses_it(a in "[a-z0-9:]{1,20}", b in "[a-z0-9:]{1,20}") {
202 let r = record(&a);
203 prop_assert!(current(&r, &a));
204 prop_assert_eq!(current(&r, &b), a == b);
205 prop_assert_eq!(r.is_current(&b), a == b);
206 }
207 }
208
209 #[test]
210 fn the_trailer_has_one_form_for_every_producer() {
211 let r = record("sha256:w");
212 assert_eq!(
213 r.trailer("sha256:m"),
214 "Acceptance: bug_confirmation manifest=sha256:m approval=sha256:a wording=sha256:w by=Ada\n"
215 );
216 assert!(r.validate().is_ok());
217 let mut empty = r;
218 empty.visible[0].cites.clear();
219 assert!(empty.validate().is_err());
220 assert_eq!(Producer::PersonTest.source(), AcceptanceSource::PersonTest);
221 }
222}