1use super::{Context, State, observe};
3use crate::types::{EvalId, RequestId, RequestStatus, RunId, Sha256Digest};
4use serde::{Deserialize, Serialize, ser::SerializeMap};
5const SUMMARY_CHARS: usize = 512;
7const PAGE_SIZE: usize = 20;
9const FINDING_BYTES: usize = 24 * 1024;
11#[derive(Debug, Clone, Copy, Serialize)]
12#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
13pub enum RunVerdict {
14 Green,
15 Red,
16 Incomplete,
17 SignoffsPending,
18 Error,
19 Cancelled,
20}
21impl RunVerdict {
22 fn from_state(state: State) -> Option<Self> {
23 match state {
24 State::Green => Some(Self::Green),
25 State::Red => Some(Self::Red),
26 State::Incomplete => Some(Self::Incomplete),
27 State::SignoffsPending => Some(Self::SignoffsPending),
28 State::Error => Some(Self::Error),
29 State::Cancelled => Some(Self::Cancelled),
30 _ => None,
31 }
32 }
33}
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(try_from = "String", into = "String")]
36pub struct ResultCursor {
37 run: RunId,
38 revision: Sha256Digest,
39 offset: usize,
40}
41impl TryFrom<String> for ResultCursor {
42 type Error = String;
43 fn try_from(value: String) -> Result<Self, String> {
44 let mut parts = value.rsplitn(3, ':');
45 let offset = parts
46 .next()
47 .and_then(|part| part.parse().ok())
48 .ok_or("Invalid result cursor.")?;
49 let revision = parts
50 .next()
51 .ok_or("Invalid result cursor.")?
52 .parse::<Sha256Digest>()
53 .map_err(|_| "Invalid result cursor.")?;
54 let run = parts.next().ok_or("Invalid result cursor.")?.parse()?;
55 Ok(Self {
56 run,
57 revision,
58 offset,
59 })
60 }
61}
62impl From<ResultCursor> for String {
63 fn from(cursor: ResultCursor) -> Self {
64 format!("{}:{}:{}", cursor.run, cursor.revision, cursor.offset)
65 }
66}
67impl std::str::FromStr for ResultCursor {
68 type Err = String;
69 fn from_str(value: &str) -> Result<Self, String> {
70 value.to_owned().try_into()
71 }
72}
73#[derive(Debug, Clone, Serialize)]
74pub struct Finding {
75 pub request: RequestId,
76 pub eval: EvalId,
77 pub state: RequestStatus,
78 pub summary: String,
79}
80#[derive(Debug, Clone, Copy)]
83pub enum ResultState {
84 Preparing,
85 Running,
86 Waiting,
87 Cancelling,
88 Green { satisfied: bool },
89 Red,
90 Incomplete,
91 SignoffsPending,
92 Error,
93 Cancelled,
94}
95impl ResultState {
96 fn from_observed(state: State, satisfied: bool) -> Self {
97 match state {
98 State::Preparing => Self::Preparing,
99 State::Running => Self::Running,
100 State::Waiting => Self::Waiting,
101 State::Cancelling => Self::Cancelling,
102 State::Green => Self::Green { satisfied },
103 State::Red => Self::Red,
104 State::Incomplete => Self::Incomplete,
105 State::SignoffsPending => Self::SignoffsPending,
106 State::Error => Self::Error,
107 State::Cancelled => Self::Cancelled,
108 }
109 }
110 pub fn state(self) -> State {
111 match self {
112 Self::Preparing => State::Preparing,
113 Self::Running => State::Running,
114 Self::Waiting => State::Waiting,
115 Self::Cancelling => State::Cancelling,
116 Self::Green { .. } => State::Green,
117 Self::Red => State::Red,
118 Self::Incomplete => State::Incomplete,
119 Self::SignoffsPending => State::SignoffsPending,
120 Self::Error => State::Error,
121 Self::Cancelled => State::Cancelled,
122 }
123 }
124}
125#[derive(Debug, Clone)]
126pub struct RunResultPage {
127 pub run: RunId,
128 pub phase: ResultState,
129 pub findings: Vec<Finding>,
130 pub next_cursor: Option<ResultCursor>,
131}
132impl RunResultPage {
133 pub fn state(&self) -> State {
134 self.phase.state()
135 }
136 pub fn ready(&self) -> bool {
137 self.state().is_terminal()
138 }
139 pub fn verdict(&self) -> Option<RunVerdict> {
140 RunVerdict::from_state(self.state())
141 }
142 pub fn satisfied(&self) -> bool {
143 matches!(self.phase, ResultState::Green { satisfied: true })
144 }
145}
146impl Serialize for RunResultPage {
147 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
148 let mut output = serializer.serialize_map(None)?;
150 output.serialize_entry("run", &self.run)?;
151 output.serialize_entry("state", &self.state())?;
152 output.serialize_entry("ready", &self.ready())?;
153 output.serialize_entry("verdict", &self.verdict())?;
154 output.serialize_entry("satisfied", &self.satisfied())?;
155 output.serialize_entry("findings", &self.findings)?;
156 output.serialize_entry("next_cursor", &self.next_cursor)?;
157 output.end()
158 }
159}
160pub(super) fn finding(request: &crate::store::Request) -> Finding {
161 let summary = if let Some(error) = request.error() {
162 error.to_owned()
163 } else if let Some(crate::store::ExecutionResult::Runtime(result)) = request.result() {
164 match result.exit_code.value() {
165 Some(code) => format!("Runtime check exited {code}."),
166 None => format!("Runtime check: {}.", request.status()),
167 }
168 } else {
169 request.status().to_string()
170 };
171 Finding {
172 request: request.id.clone(),
173 eval: request.eval_id.clone(),
174 state: request.status(),
175 summary: summary.chars().take(SUMMARY_CHARS).collect(),
176 }
177}
178pub async fn result(
179 context: &Context,
180 id: &RunId,
181 cursor: Option<ResultCursor>,
182) -> Result<RunResultPage, String> {
183 let observation = observe::status(context, id).await?;
184 if !observation.state.is_terminal() {
185 if cursor.is_some() {
186 return Err("A running Run has no result cursor.".into());
187 }
188 return Ok(RunResultPage {
189 run: id.clone(),
190 phase: ResultState::from_observed(observation.state, false),
191 findings: Vec::new(),
192 next_cursor: None,
193 });
194 }
195 let state_path = context.state.clone();
196 let run_id = id.clone();
197 let (view, details) = crate::task::joined(
198 tokio::task::spawn_blocking(move || {
199 let mut reader = crate::store::reader::Reader::default();
200 reader.snapshot(&state_path, |db, cache, state| {
201 cache
202 .selected(db, state, &run_id, None, true)?
203 .ok_or_else(|| crate::store::receipts::Error::Invalid("Run not found.".into()))
204 })
205 })
206 .await,
207 )
208 .map_err(|e| e.to_string())??;
209 let state = observe::state(&view.run, &view.requests);
210 let revision =
211 Sha256Digest::of(&serde_json::to_vec(&(&view, &details)).map_err(|e| e.to_string())?);
212 let offset = if let Some(cursor) = cursor {
213 if cursor.run != *id || cursor.revision != revision {
214 return Err("Expired or foreign result cursor.".into());
215 }
216 cursor.offset
217 } else {
218 0
219 };
220 if offset > view.requests.len() {
221 return Err("Invalid result cursor offset.".into());
222 }
223 let findings = if state.is_terminal() {
224 view.requests
225 .iter()
226 .skip(offset)
227 .take(PAGE_SIZE)
228 .map(finding)
229 .scan(0, |bytes, finding| {
230 let size = serde_json::to_vec(&finding)
231 .expect("typed finding serializes")
232 .len()
233 + 1;
234 *bytes += size;
235 (*bytes <= FINDING_BYTES).then_some(finding)
236 })
237 .collect::<Vec<_>>()
238 } else {
239 Vec::new()
240 };
241 let next = offset + findings.len();
242 let next_cursor = (state.is_terminal() && next < view.requests.len()).then(|| ResultCursor {
243 run: id.clone(),
244 revision,
245 offset: next,
246 });
247 Ok(RunResultPage {
248 run: id.clone(),
249 phase: ResultState::from_observed(
250 state,
251 view.run.validation.snapshot().is_some_and(|validation| {
252 matches!(validation.satisfied, crate::config::Field::Value(true))
253 }),
254 ),
255 findings,
256 next_cursor,
257 })
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use serde_json::json;
264
265 #[test]
266 fn result_phase_is_the_only_source_of_wire_readiness_and_verdict() {
267 let page = |phase| RunResultPage {
268 run: "run-wire".parse().unwrap(),
269 phase,
270 findings: Vec::new(),
271 next_cursor: None,
272 };
273 assert_eq!(
274 serde_json::to_value(page(ResultState::Running)).unwrap(),
275 json!({
276 "run":"run-wire","state":"running","ready":false,"verdict":null,
277 "satisfied":false,"findings":[],"next_cursor":null,
278 })
279 );
280 assert_eq!(
281 serde_json::to_value(page(ResultState::Green { satisfied: true })).unwrap(),
282 json!({
283 "run":"run-wire","state":"green","ready":true,"verdict":"GREEN",
284 "satisfied":true,"findings":[],"next_cursor":null,
285 })
286 );
287 let red = serde_json::to_value(page(ResultState::Red)).unwrap();
288 assert_eq!(red["verdict"], "RED");
289 assert_eq!(red["ready"], true);
290 assert_eq!(red["satisfied"], false);
291 }
292}