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    /// A run wrote a story's oracle before its first code task, nobody
22    /// having reviewed it.
23    Run,
24}
25
26impl Producer {
27    /// The acceptance source a run reports for it.
28    #[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    /// Its recorded name.
39    #[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/// Where a task's visible acceptance set came from.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum AcceptanceSource {
54    /// Written by the acceptance author at the run's checkpoint.
55    Authored,
56    /// Frozen in review.
57    Review,
58    /// A bug's confirmed reproductions.
59    BugConfirmation,
60    /// Tests the person named for a bug.
61    PersonTest,
62}
63
64/// One visible test of the set.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
66pub struct VisibleTest {
67    /// Project-relative path.
68    pub path: String,
69    /// `sha256:<hex>` of its content.
70    pub sha256: String,
71    /// The scenario or statement ids it cites; never empty.
72    pub cites: Vec<String>,
73}
74
75/// A sealed held-out set: kept in the state directory, never shown.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
77pub struct SealedRef {
78    /// The requirement it is keyed by.
79    pub key: String,
80    /// The manifest's hash.
81    pub manifest_sha256: String,
82    /// Files in it.
83    pub count: usize,
84}
85
86/// Who decided, through what, when.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
88pub struct Decided {
89    /// The person.
90    pub by: String,
91    /// The interface.
92    pub via: Via,
93    /// Unix seconds.
94    pub at: u64,
95}
96
97/// Acceptance tests a person decided before the run.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
99pub struct PreFrozenAcceptance {
100    /// The unit, as the adapter names it.
101    pub unit: String,
102    /// The requirement key (`US1`, `FR-003`, `bug:<slug>`).
103    pub requirement: String,
104    /// The wording it was decided on: a different current digest lapses it.
105    pub wording_digest: String,
106    /// The decision's digest (a review's approval, a bug's confirmation).
107    pub approval_digest: String,
108    /// The visible tests.
109    pub visible: Vec<VisibleTest>,
110    /// The held-out set, unless `[acceptance] held_out = false`.
111    pub held_out_sealed: Option<SealedRef>,
112    /// Who produced it.
113    pub producer: Producer,
114    /// The decision.
115    pub decided: Decided,
116}
117
118impl PreFrozenAcceptance {
119    /// Whether it is well formed: a visible test at least, each citing
120    /// something.
121    ///
122    /// # Errors
123    ///
124    /// What is missing.
125    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    /// Whether it still stands for the wording as it reads now ([`current`]).
136    #[must_use]
137    pub fn is_current(&self, wording_now: &str) -> bool {
138        current(self, wording_now)
139    }
140
141    /// The `Acceptance:` trailer: one form for every producer;
142    /// `manifest` is the visible set's digest.
143    #[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/// Whether `record` still stands for the wording as it reads now.
160#[must_use]
161pub fn current(record: &PreFrozenAcceptance, wording_now: &str) -> bool {
162    record.wording_digest == wording_now
163}
164
165/// The text a visible set's manifest digest is over: `path=sha256` lines,
166/// sorted by path.
167#[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}