Skip to main content

speccoder_core/
acceptance.rs

1//! Acceptance tests decided before the run: one record for every producer
2//! — a review that froze the oracle and a bug's confirmed
3//! reproduction. The fix's checkpoint reuses the record instead
4//! of calling the acceptance author; a change of the wording it was decided
5//! on lapses it.
6
7use serde::{Deserialize, Serialize};
8
9use crate::actions::Via;
10
11/// Who produced a pre-frozen set.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum Producer {
15    /// A person froze the oracle in review.
16    Review,
17    /// A person confirmed a bug's authored reproductions.
18    BugConfirmation,
19    /// A person named the bug's reproduction tests (`--test`).
20    PersonTest,
21}
22
23impl Producer {
24    /// The acceptance source a run reports for it.
25    #[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    /// Its recorded name.
35    #[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/// Where a task's visible acceptance set came from.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
47#[serde(rename_all = "snake_case")]
48pub enum AcceptanceSource {
49    /// Written by the acceptance author at the run's checkpoint.
50    Authored,
51    /// Frozen in review.
52    Review,
53    /// A bug's confirmed reproductions.
54    BugConfirmation,
55    /// Tests the person named for a bug.
56    PersonTest,
57}
58
59/// One visible test of the set.
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
61pub struct VisibleTest {
62    /// Project-relative path.
63    pub path: String,
64    /// `sha256:<hex>` of its content.
65    pub sha256: String,
66    /// The scenario or statement ids it cites; never empty.
67    pub cites: Vec<String>,
68}
69
70/// A sealed held-out set: kept in the state directory, never shown.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
72pub struct SealedRef {
73    /// The requirement it is keyed by.
74    pub key: String,
75    /// The manifest's hash.
76    pub manifest_sha256: String,
77    /// Files in it.
78    pub count: usize,
79}
80
81/// Who decided, through what, when.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
83pub struct Decided {
84    /// The person.
85    pub by: String,
86    /// The interface.
87    pub via: Via,
88    /// Unix seconds.
89    pub at: u64,
90}
91
92/// Acceptance tests a person decided before the run.
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
94pub struct PreFrozenAcceptance {
95    /// The unit, as the adapter names it.
96    pub unit: String,
97    /// The requirement key (`US1`, `FR-003`, `bug:<slug>`).
98    pub requirement: String,
99    /// The wording it was decided on: a different current digest lapses it.
100    pub wording_digest: String,
101    /// The decision's digest (a review's approval, a bug's confirmation).
102    pub approval_digest: String,
103    /// The visible tests.
104    pub visible: Vec<VisibleTest>,
105    /// The held-out set, unless `[acceptance] held_out = false`.
106    pub held_out_sealed: Option<SealedRef>,
107    /// Who produced it.
108    pub producer: Producer,
109    /// The decision.
110    pub decided: Decided,
111}
112
113impl PreFrozenAcceptance {
114    /// Whether it is well formed: a visible test at least, each citing
115    /// something.
116    ///
117    /// # Errors
118    ///
119    /// What is missing.
120    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    /// Whether it still stands for the wording as it reads now ([`current`]).
131    #[must_use]
132    pub fn is_current(&self, wording_now: &str) -> bool {
133        current(self, wording_now)
134    }
135
136    /// The `Acceptance:` trailer: one form for every producer;
137    /// `manifest` is the visible set's digest.
138    #[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/// Whether `record` still stands for the wording as it reads now.
155#[must_use]
156pub fn current(record: &PreFrozenAcceptance, wording_now: &str) -> bool {
157    record.wording_digest == wording_now
158}
159
160/// The text a visible set's manifest digest is over: `path=sha256` lines,
161/// sorted by path.
162#[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}