git-ents.gitmain
⌘K
foforge
result.rs165 lines · 6.0 KB · rusthistorycomment on this file
1//! The Result entity and its status taxonomy.
2//!
3//! Spec coverage: `model.result-taxonomy`, `model.result-identity`.
4
5use ents_attrs as ents;
6use facet::Facet;
7use gix_hash::ObjectId;
8
9/// The fixed set of outcomes a recorded result may carry.
10///
11/// `model.result-taxonomy` fixes this taxonomy at exactly three values;
12/// unlike `ents-forge`'s `Issue::state`, which is intentionally open, this
13/// is a closed enum precisely because the spec closes it. *When* each
14/// status is written, and when nothing is written at all, is run
15/// semantics specified by `effect.result-taxonomy` and owned by
16/// `ents-effect` (phase 5) — this type only names the three values.
17///
18/// # Examples
19///
20/// ```
21/// use ents_model::Status;
22///
23/// let (id, store) = facet_git_tree::serialize(&Status::Pass).expect("serialize");
24/// let back: Status = facet_git_tree::deserialize(&id, &store).expect("deserialize");
25/// assert_eq!(back, Status::Pass);
26/// ```
27// @relation(model.result-taxonomy, meta-ref.typed-tree, model.extensibility, scope=file)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Facet)]
29#[repr(u8)]
30pub enum Status {
31 /// The effect ran and succeeded.
32 Pass,
33 /// The effect ran and reported failure.
34 Fail,
35 /// The effect could not complete a run (as distinct from completing
36 /// and reporting failure).
37 Error,
38}
39
40impl std::fmt::Display for Status {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.write_str(match self {
43 Self::Pass => "pass",
44 Self::Fail => "fail",
45 Self::Error => "error",
46 })
47 }
48}
49
50/// A recorded result: the outcome of running one effect against one
51/// commit, living at `refs/meta/results/<effect>/<short-oid>`
52/// (`namespace::result_ref`) or the self-run mirror.
53///
54/// `model.result-identity` requires the result to carry the effect's name
55/// and the full oid of the commit the run judged *as tree fields*, from
56/// which the refname's `<effect>` and `<short-oid>` segments derive
57/// (`meta-ref.identity-binding`): the gate recomputes the refname from
58/// these fields and refuses a mismatch, so a signed `pass` cannot be
59/// replayed as the result of a different effect or commit — a result means
60/// something with the refname stripped away. The composite key freezes the
61/// genesis tree by identity, so this struct evolves additively only.
62///
63/// `target` is stored as a raw 20-byte SHA-1 array, the same
64/// `facet-git-tree`-native oid representation [`crate::Redaction`] uses;
65/// [`ResultRecord::new`] and [`ResultRecord::target`] keep the public API
66/// in gitoxide's own type. The fields are not parent edges: a result ref's
67/// parents stay prior states of the same result, and a result never
68/// retains the judged commit's ancestry the way a pin does
69/// (`model.result-identity`, `model.review-pin`).
70///
71/// # Examples
72///
73/// ```
74/// use ents_model::{ResultRecord, Status};
75///
76/// let target = gix_hash::ObjectId::null(gix_hash::Kind::Sha1);
77/// let result = ResultRecord::new("unit", target, Status::Pass);
78/// assert_eq!(result.effect, "unit");
79/// assert_eq!(result.target(), target);
80///
81/// let (id, store) = facet_git_tree::serialize(&result).expect("serialize");
82/// let back: ResultRecord = facet_git_tree::deserialize(&id, &store).expect("deserialize");
83/// assert_eq!(back, result);
84/// ```
85// @relation(model.result-identity, meta-ref.identity-binding, meta-ref.typed-tree, model.extensibility, scope=file)
86#[derive(Debug, Clone, PartialEq, Eq, Facet)]
87pub struct ResultRecord {
88 /// The name of the effect this result records — binds the refname's
89 /// `<effect>` segment (`model.result-identity`).
90 #[facet(ents::skip)]
91 pub effect: String,
92 /// The full oid of the commit the run judged, as a raw 20-byte SHA-1
93 /// array — binds the refname's `<short-oid>` segment
94 /// (`model.result-identity`).
95 #[facet(ents::skip)]
96 target: [u8; 20],
97 /// The run's outcome.
98 #[facet(ents::head)]
99 pub status: Status,
100}
101
102impl ResultRecord {
103 /// Record `status` for `effect` against the commit `target`.
104 #[must_use]
105 pub fn new(effect: impl Into<String>, target: ObjectId, status: Status) -> Self {
106 let mut bytes = [0u8; 20];
107 bytes.copy_from_slice(target.as_slice());
108 Self {
109 effect: effect.into(),
110 target: bytes,
111 status,
112 }
113 }
114
115 /// The oid of the commit this result judged.
116 #[must_use]
117 pub fn target(&self) -> ObjectId {
118 ObjectId::from_bytes_or_panic(&self.target)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 #![allow(clippy::expect_used, reason = "unit test")]
125
126 use facet_git_tree::{deserialize, serialize};
127 use rstest::rstest;
128
129 use super::*;
130
131 #[rstest]
132 #[case::pass(Status::Pass, "pass")]
133 #[case::fail(Status::Fail, "fail")]
134 #[case::error(Status::Error, "error")]
135 // @relation(model.result-taxonomy, scope=function, role=Verifies)
136 fn status_displays_lowercase(#[case] status: Status, #[case] expected: &str) {
137 assert_eq!(status.to_string(), expected);
138 }
139
140 #[rstest]
141 #[case::pass(Status::Pass)]
142 #[case::fail(Status::Fail)]
143 #[case::error(Status::Error)]
144 // @relation(model.result-taxonomy, meta-ref.typed-tree, scope=function, role=Verifies)
145 fn every_taxonomy_value_round_trips(#[case] status: Status) {
146 let (id, store) = serialize(&status).expect("serialize");
147 let back: Status = deserialize(&id, &store).expect("deserialize");
148 assert_eq!(back, status);
149 }
150
151 #[rstest]
152 #[case::pass(Status::Pass)]
153 #[case::fail(Status::Fail)]
154 #[case::error(Status::Error)]
155 // @relation(model.result-identity, meta-ref.typed-tree, scope=function, role=Verifies)
156 fn result_round_trips_and_preserves_effect_and_target(#[case] status: Status) {
157 let target = ObjectId::from_bytes_or_panic(&[9u8; 20]);
158 let result = ResultRecord::new("unit", target, status);
159 let (id, store) = serialize(&result).expect("serialize");
160 let back: ResultRecord = deserialize(&id, &store).expect("deserialize");
161 assert_eq!(back, result);
162 assert_eq!(back.effect, "unit");
163 assert_eq!(back.target(), target);
164 }
165}