1use serde::{Deserialize, Serialize};
18
19use crate::protocol_state_storage::{
20 ProtocolStateError, ProtocolStateStorage, MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES,
21};
22
23pub const CONFORMANCE_KEY_TYPE: &str = "storage_conformance_probe";
28
29pub const CONFORMANCE_KEY_TYPE_OTHER: &str = "storage_conformance_probe_other";
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ConformanceFailure {
35 pub check: String,
37 pub detail: String,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ConformanceReport {
44 pub passed: Vec<String>,
46 pub failures: Vec<ConformanceFailure>,
48}
49
50impl ConformanceReport {
51 pub fn is_green(&self) -> bool {
53 self.failures.is_empty()
54 }
55
56 pub fn summary(&self) -> String {
58 if self.is_green() {
59 format!("{} checks passed", self.passed.len())
60 } else {
61 let names: Vec<&str> = self
62 .failures
63 .iter()
64 .map(|failure| failure.check.as_str())
65 .collect();
66 format!(
67 "{} of {} checks failed: {}",
68 self.failures.len(),
69 self.passed.len() + self.failures.len(),
70 names.join(", ")
71 )
72 }
73 }
74
75 fn pass(&mut self, check: &str) {
76 self.passed.push(check.to_string());
77 }
78
79 fn fail(&mut self, check: &str, detail: impl Into<String>) {
80 self.failures.push(ConformanceFailure {
81 check: check.to_string(),
82 detail: detail.into(),
83 });
84 }
85
86 fn check(&mut self, name: &str, outcome: std::result::Result<(), String>) {
87 match outcome {
88 Ok(()) => self.pass(name),
89 Err(detail) => self.fail(name, detail),
90 }
91 }
92}
93
94fn expect_load(
95 storage: &dyn ProtocolStateStorage,
96 key_id: &str,
97 expected: &[u8],
98) -> std::result::Result<(), String> {
99 match storage.load(CONFORMANCE_KEY_TYPE, key_id) {
100 Ok(Some(actual)) if actual == expected => Ok(()),
101 Ok(Some(actual)) => Err(format!(
102 "loaded {} bytes, expected {} (contents differ)",
103 actual.len(),
104 expected.len()
105 )),
106 Ok(None) => Err("stored value loaded back as missing".to_string()),
107 Err(err) => Err(format!("load failed: {err}")),
108 }
109}
110
111pub fn run(storage: &dyn ProtocolStateStorage) -> ConformanceReport {
117 let mut report = ConformanceReport {
118 passed: Vec::new(),
119 failures: Vec::new(),
120 };
121
122 cleanup(storage);
125
126 let outcome = storage
128 .store(CONFORMANCE_KEY_TYPE, "basic", b"hello")
129 .map_err(|err| format!("store failed: {err}"))
130 .and_then(|()| expect_load(storage, "basic", b"hello"));
131 report.check("store_then_load", outcome);
132
133 let binary: Vec<u8> = (0u8..=255).collect();
137 let outcome = storage
138 .store(CONFORMANCE_KEY_TYPE, "binary", &binary)
139 .map_err(|err| format!("store failed: {err}"))
140 .and_then(|()| expect_load(storage, "binary", &binary));
141 report.check("binary_values_round_trip", outcome);
142
143 let outcome = storage
145 .store(CONFORMANCE_KEY_TYPE, "empty", b"")
146 .map_err(|err| format!("store failed: {err}"))
147 .and_then(|()| expect_load(storage, "empty", b""));
148 report.check("empty_value_is_not_missing", outcome);
149
150 let outcome = storage
152 .store(CONFORMANCE_KEY_TYPE, "overwrite", b"first")
153 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE, "overwrite", b"second"))
154 .map_err(|err| format!("store failed: {err}"))
155 .and_then(|()| expect_load(storage, "overwrite", b"second"));
156 report.check("store_overwrites", outcome);
157
158 let outcome = match storage.load(CONFORMANCE_KEY_TYPE, "never_written") {
160 Ok(None) => Ok(()),
161 Ok(Some(_)) => Err("a key that was never written returned a value".to_string()),
162 Err(ProtocolStateError::NotFound(_)) => {
163 Err("absent keys must load as Ok(None), not NotFound".to_string())
164 }
165 Err(err) => Err(format!("load of an absent key failed: {err}")),
166 };
167 report.check("absent_key_loads_as_none", outcome);
168
169 let outcome = storage
174 .store(CONFORMANCE_KEY_TYPE, "deletable", b"x")
175 .and_then(|()| storage.delete(CONFORMANCE_KEY_TYPE, "deletable"))
176 .map_err(|err| format!("store/delete failed: {err}"))
177 .and_then(|()| match storage.load(CONFORMANCE_KEY_TYPE, "deletable") {
178 Ok(None) => Ok(()),
179 Ok(Some(_)) => Err("value survived a delete".to_string()),
180 Err(err) => Err(format!("load after delete failed: {err}")),
181 })
182 .and_then(
183 |()| match storage.delete(CONFORMANCE_KEY_TYPE, "deletable") {
184 Ok(()) => Ok(()),
185 Err(err) => Err(format!("second delete must succeed, got: {err}")),
186 },
187 );
188 report.check("delete_removes_and_is_idempotent", outcome);
189
190 let outcome = storage
194 .store(CONFORMANCE_KEY_TYPE, "shared_id", b"mine")
195 .and_then(|()| storage.store(CONFORMANCE_KEY_TYPE_OTHER, "shared_id", b"theirs"))
196 .map_err(|err| format!("store failed: {err}"))
197 .and_then(|()| expect_load(storage, "shared_id", b"mine"))
198 .and_then(
199 |()| match storage.load(CONFORMANCE_KEY_TYPE_OTHER, "shared_id") {
200 Ok(Some(value)) if value == b"theirs" => Ok(()),
201 Ok(_) => Err("the same key id in two key types collided".to_string()),
202 Err(err) => Err(format!("load failed: {err}")),
203 },
204 );
205 report.check("key_types_are_separate_namespaces", outcome);
206
207 let outcome = (|| -> std::result::Result<(), String> {
209 let keys = storage
210 .list_keys(CONFORMANCE_KEY_TYPE)
211 .map_err(|err| format!("list_keys failed: {err}"))?;
212 for expected in ["basic", "binary", "empty", "overwrite", "shared_id"] {
213 if !keys.iter().any(|key| key == expected) {
214 return Err(format!("list_keys omitted {expected:?}"));
215 }
216 }
217 if keys.iter().any(|key| key == "deletable") {
218 return Err("list_keys named a deleted record".to_string());
219 }
220 let other = storage
221 .list_keys(CONFORMANCE_KEY_TYPE_OTHER)
222 .map_err(|err| format!("list_keys failed: {err}"))?;
223 if other.len() != 1 || other.first().map(String::as_str) != Some("shared_id") {
224 return Err(format!("list_keys leaked across key types: {other:?}"));
225 }
226 Ok(())
227 })();
228 report.check("list_keys_is_accurate_and_scoped", outcome);
229
230 let outcome = match storage.list_keys("storage_conformance_probe_unused") {
232 Ok(keys) if keys.is_empty() => Ok(()),
233 Ok(keys) => Err(format!("an unused key type listed {} keys", keys.len())),
234 Err(err) => Err(format!("list_keys on an unused key type failed: {err}")),
235 };
236 report.check("unused_key_type_lists_empty", outcome);
237
238 let composed = "space-1/doc.name_2/000000000000002a";
243 let outcome = storage
244 .store(CONFORMANCE_KEY_TYPE, composed, b"composed")
245 .map_err(|err| format!("store with a composed key failed: {err}"))
246 .and_then(|()| expect_load(storage, composed, b"composed"))
247 .and_then(|()| {
248 let keys = storage
249 .list_keys(CONFORMANCE_KEY_TYPE)
250 .map_err(|err| format!("list_keys failed: {err}"))?;
251 if keys.iter().any(|key| key == composed) {
252 Ok(())
253 } else {
254 Err("list_keys did not return a composed key verbatim".to_string())
255 }
256 });
257 report.check("composed_key_ids_round_trip", outcome);
258
259 let long_key = format!("{}/{}/{:016x}", "s".repeat(128), "d".repeat(128), u64::MAX);
267 let outcome = storage
268 .store(CONFORMANCE_KEY_TYPE, &long_key, b"long")
269 .map_err(|err| format!("store with a long key failed: {err}"))
270 .and_then(|()| expect_load(storage, &long_key, b"long"));
271 report.check("long_key_ids_round_trip", outcome);
272
273 let large = vec![0xa5u8; MAX_PROTOCOL_STATE_RECORD_TRANSFER_BYTES / 2];
277 let outcome = storage
278 .store(CONFORMANCE_KEY_TYPE, "large", &large)
279 .map_err(|err| format!("store of a large record failed: {err}"))
280 .and_then(|()| expect_load(storage, "large", &large));
281 report.check("large_records_round_trip", outcome);
282
283 let outcome = (|| -> std::result::Result<(), String> {
287 cleanup(storage);
288 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
289 let remaining = storage
290 .list_keys(key_type)
291 .map_err(|err| format!("list_keys failed: {err}"))?;
292 if !remaining.is_empty() {
293 return Err(format!(
294 "{} records survived a listed delete in {key_type}",
295 remaining.len()
296 ));
297 }
298 }
299 Ok(())
300 })();
301 report.check("listed_records_can_all_be_deleted", outcome);
302
303 report
304}
305
306fn cleanup(storage: &dyn ProtocolStateStorage) {
308 for key_type in [CONFORMANCE_KEY_TYPE, CONFORMANCE_KEY_TYPE_OTHER] {
309 if let Ok(keys) = storage.list_keys(key_type) {
310 for key in keys {
311 let _ = storage.delete(key_type, &key);
312 }
313 }
314 }
315}
316
317pub fn run_json(storage: &dyn ProtocolStateStorage) -> String {
322 let report = run(storage);
323 serde_json::to_string(&report).unwrap_or_else(|err| {
324 format!(
325 "{{\"passed\":[],\"failures\":[{{\"check\":\"report_serialization\",\"detail\":\"{}\"}}]}}",
326 err.to_string().replace('"', "'")
327 )
328 })
329}