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 Run,
24}
25
26impl Producer {
27 #[must_use]
29 pub const fn source(self) -> AcceptanceSource {
30 match self {
31 Self::Review => AcceptanceSource::Review,
32 Self::BugConfirmation => AcceptanceSource::BugConfirmation,
33 Self::PersonTest => AcceptanceSource::PersonTest,
34 Self::Run => AcceptanceSource::Authored,
35 }
36 }
37
38 #[must_use]
40 pub const fn as_str(self) -> &'static str {
41 match self {
42 Self::Review => "review",
43 Self::BugConfirmation => "bug_confirmation",
44 Self::PersonTest => "person_test",
45 Self::Run => "run",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum AcceptanceSource {
54 Authored,
56 Review,
58 BugConfirmation,
60 PersonTest,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66pub struct VisibleTest {
67 pub path: String,
69 pub sha256: String,
71 pub cites: Vec<String>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
77pub struct SealedRef {
78 pub key: String,
80 pub manifest_sha256: String,
82 pub count: usize,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
88pub struct Decided {
89 pub by: String,
91 pub via: Via,
93 pub at: u64,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
99pub struct PreFrozenAcceptance {
100 pub unit: String,
102 pub requirement: String,
104 pub wording_digest: String,
106 pub approval_digest: String,
108 pub visible: Vec<VisibleTest>,
110 pub held_out_sealed: Option<SealedRef>,
112 pub producer: Producer,
114 pub decided: Decided,
116}
117
118impl PreFrozenAcceptance {
119 pub fn validate(&self) -> Result<(), String> {
126 if self.visible.is_empty() {
127 return Err("a pre-frozen set needs at least one visible test".to_owned());
128 }
129 if let Some(t) = self.visible.iter().find(|t| t.cites.is_empty()) {
130 return Err(format!("{} cites nothing", t.path));
131 }
132 Ok(())
133 }
134
135 #[must_use]
137 pub fn is_current(&self, wording_now: &str) -> bool {
138 current(self, wording_now)
139 }
140
141 #[must_use]
144 pub fn trailer(&self, manifest: &str) -> String {
145 format!(
146 "Acceptance: {} manifest={manifest} approval={} wording={} by={}\n",
147 self.producer.as_str(),
148 self.approval_digest,
149 self.wording_digest,
150 self.decided
151 .by
152 .split_whitespace()
153 .collect::<Vec<_>>()
154 .join(" ")
155 )
156 }
157}
158
159#[must_use]
161pub fn current(record: &PreFrozenAcceptance, wording_now: &str) -> bool {
162 record.wording_digest == wording_now
163}
164
165#[must_use]
168pub fn manifest_text(visible: &[VisibleTest]) -> String {
169 let mut lines: Vec<String> = visible
170 .iter()
171 .map(|t| format!("{}={}", t.path, t.sha256))
172 .collect();
173 lines.sort();
174 lines.join("\n")
175}
176
177#[cfg(test)]
178mod tests {
179 use proptest::prelude::*;
180
181 use super::*;
182
183 fn record(wording: &str) -> PreFrozenAcceptance {
184 PreFrozenAcceptance {
185 unit: "007-bugfixes".to_owned(),
186 requirement: "FR-003".to_owned(),
187 wording_digest: wording.to_owned(),
188 approval_digest: "sha256:a".to_owned(),
189 visible: vec![VisibleTest {
190 path: "tests/repro_e1.rs".to_owned(),
191 sha256: "sha256:t".to_owned(),
192 cites: vec!["E1".to_owned()],
193 }],
194 held_out_sealed: None,
195 producer: Producer::BugConfirmation,
196 decided: Decided {
197 by: "Ada".to_owned(),
198 via: Via::Terminal,
199 at: 1,
200 },
201 }
202 }
203
204 proptest! {
205 #[test]
206 fn any_wording_change_lapses_it(a in "[a-z0-9:]{1,20}", b in "[a-z0-9:]{1,20}") {
207 let r = record(&a);
208 prop_assert!(current(&r, &a));
209 prop_assert_eq!(current(&r, &b), a == b);
210 prop_assert_eq!(r.is_current(&b), a == b);
211 }
212 }
213
214 #[test]
215 fn the_trailer_has_one_form_for_every_producer() {
216 let r = record("sha256:w");
217 assert_eq!(
218 r.trailer("sha256:m"),
219 "Acceptance: bug_confirmation manifest=sha256:m approval=sha256:a wording=sha256:w by=Ada\n"
220 );
221 assert!(r.validate().is_ok());
222 let mut empty = r;
223 empty.visible[0].cites.clear();
224 assert!(empty.validate().is_err());
225 assert_eq!(Producer::PersonTest.source(), AcceptanceSource::PersonTest);
226 }
227}