1use crate::{
2 constants::WASM_PAGE_SIZE_BYTES,
3 ledger::{AllocationLedger, AllocationRecord},
4 physical::CommitSnapshotDiagnostic,
5 policy::PolicyIdentity,
6};
7use serde::Serialize;
8
9#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
19pub struct DiagnosticExport {
20 pub current_generation: u64,
22 pub records: Vec<DiagnosticRecord>,
24}
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
34pub struct DiagnosticRuntimeBinding {
35 pub policy_identity: PolicyIdentity,
37 pub allocation_pool: crate::MemoryAllocationPool,
39}
40
41impl DiagnosticRuntimeBinding {
42 #[must_use]
44 pub const fn new(
45 policy_identity: PolicyIdentity,
46 allocation_pool: crate::MemoryAllocationPool,
47 ) -> Self {
48 Self {
49 policy_identity,
50 allocation_pool,
51 }
52 }
53}
54
55#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
68pub struct MemoryRuntimeDoctorReport {
69 pub bootstrapped: bool,
71 pub tested_policy_identity: Result<PolicyIdentity, DiagnosticFailure>,
73 pub established_bootstrap_binding: Option<DiagnosticRuntimeBinding>,
75 pub bootstrap_binding: DiagnosticCheck,
78 pub stable_cell: DiagnosticStableCell,
80 pub commit_recovery: Option<CommitSnapshotDiagnostic>,
82 pub ledger: Option<DiagnosticExport>,
84 pub requests: Vec<crate::MemoryRequest>,
86 pub allocation_pool: crate::MemoryAllocationPool,
88 pub validation: DiagnosticCheck,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
99pub enum DiagnosticCode {
100 #[serde(rename = "stable_cell")]
102 StableCell,
103 #[serde(rename = "unsupported_format")]
105 UnsupportedFormat,
106 #[serde(rename = "ledger_recovery")]
108 LedgerRecovery,
109 #[serde(rename = "allocation_validation")]
111 AllocationValidation,
112 #[serde(rename = "policy_identity")]
114 PolicyIdentity,
115 #[serde(rename = "runtime_binding")]
117 RuntimeBinding,
118}
119
120#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
127pub struct DiagnosticFailure {
128 pub code: DiagnosticCode,
130 pub message: String,
132}
133
134impl DiagnosticFailure {
135 #[must_use]
137 pub fn new(code: DiagnosticCode, message: impl Into<String>) -> Self {
138 Self {
139 code,
140 message: message.into(),
141 }
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
152pub struct DiagnosticStableCell {
153 pub status: DiagnosticStableCellStatus,
155 pub memory_size: DiagnosticMemorySize,
157}
158
159impl DiagnosticStableCell {
160 #[must_use]
162 pub const fn new(
163 status: DiagnosticStableCellStatus,
164 memory_size: DiagnosticMemorySize,
165 ) -> Self {
166 Self {
167 status,
168 memory_size,
169 }
170 }
171}
172
173#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
180pub enum DiagnosticStableCellStatus {
181 Empty,
183 Readable,
185 Corrupt {
188 failure: DiagnosticFailure,
190 },
191}
192
193#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
200pub enum DiagnosticCheck {
201 NotRun {
203 code: DiagnosticCode,
205 message: String,
207 },
208 Passed,
210 Failed {
212 code: DiagnosticCode,
214 message: String,
216 },
217}
218
219impl DiagnosticCheck {
220 #[must_use]
222 pub const fn passed() -> Self {
223 Self::Passed
224 }
225
226 #[must_use]
228 pub fn failed(code: DiagnosticCode, message: impl Into<String>) -> Self {
229 Self::Failed {
230 code,
231 message: message.into(),
232 }
233 }
234
235 #[must_use]
237 pub fn not_run(code: DiagnosticCode, message: impl Into<String>) -> Self {
238 Self::NotRun {
239 code,
240 message: message.into(),
241 }
242 }
243}
244
245impl DiagnosticExport {
246 #[must_use]
252 pub fn from_ledger(ledger: &AllocationLedger) -> Self {
253 Self::from_records(ledger.current_generation, ledger.records.iter().cloned())
254 }
255
256 pub(crate) fn from_owned_ledger(ledger: AllocationLedger) -> Self {
259 Self::from_records(ledger.current_generation, ledger.records.into_iter())
260 }
261
262 fn from_records(
263 current_generation: u64,
264 records: impl Iterator<Item = AllocationRecord>,
265 ) -> Self {
266 Self {
267 current_generation,
268 records: records
269 .map(|allocation| DiagnosticRecord {
270 allocation,
271 memory_size: None,
272 })
273 .collect(),
274 }
275 }
276}
277
278#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
283pub struct DiagnosticRecord {
284 pub allocation: AllocationRecord,
286 #[serde(skip_serializing_if = "Option::is_none")]
291 pub memory_size: Option<DiagnosticMemorySize>,
292}
293
294#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
301pub struct DiagnosticMemorySize {
302 pub wasm_pages: u64,
304}
305
306impl DiagnosticMemorySize {
307 #[must_use]
309 pub const fn from_wasm_pages(wasm_pages: u64) -> Self {
310 Self { wasm_pages }
311 }
312
313 #[must_use]
315 pub const fn bytes(self) -> u64 {
316 self.wasm_pages.saturating_mul(WASM_PAGE_SIZE_BYTES)
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use crate::{AllocationDeclaration, MemoryManagerSlot, ledger::AllocationRecord};
324
325 #[test]
326 fn diagnostic_export_copies_ledger_records() {
327 let declaration = AllocationDeclaration::new(
328 "app.users.v1",
329 MemoryManagerSlot::new(100).expect("usable slot"),
330 )
331 .expect("declaration");
332 let ledger = AllocationLedger {
333 current_generation: 3,
334 records: vec![AllocationRecord::active(&declaration)],
335 };
336
337 let export = DiagnosticExport::from_ledger(&ledger);
338
339 assert_eq!(export.current_generation, 3);
340 assert_eq!(export.records.len(), 1);
341 assert_eq!(export.records[0].memory_size, None);
342
343 assert_eq!(export, DiagnosticExport::from_owned_ledger(ledger));
344 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
345
346 assert_eq!(
347 wire,
348 serde_json::json!({
349 "current_generation": 3,
350 "records": [{"allocation": {"stable_key": "app.users.v1", "slot": 100, "state": "Active"}}]
351 })
352 );
353 }
354
355 #[test]
356 fn diagnostic_outcome_states_serialize() {
357 let stable_cell = DiagnosticStableCell::new(
358 DiagnosticStableCellStatus::Corrupt {
359 failure: DiagnosticFailure::new(
360 DiagnosticCode::StableCell,
361 "bad stable-cell record",
362 ),
363 },
364 DiagnosticMemorySize::from_wasm_pages(1),
365 );
366 let allocation_pool = crate::MemoryAllocationPool::new(Vec::new(), Vec::new()).unwrap();
367 let check = DiagnosticCheck::failed(
368 DiagnosticCode::AllocationValidation,
369 "duplicate declaration",
370 );
371
372 assert_eq!(
373 serde_json::to_value(DiagnosticCheck::passed()).unwrap(),
374 "Passed"
375 );
376 assert_eq!(
377 serde_json::to_value(check).unwrap(),
378 serde_json::json!({
379 "Failed": {"code": "allocation_validation", "message": "duplicate declaration"}
380 })
381 );
382 assert_eq!(
383 serde_json::to_value(stable_cell).unwrap(),
384 serde_json::json!({
385 "status": {"Corrupt": {"failure": {"code": "stable_cell", "message": "bad stable-cell record"}}},
386 "memory_size": {"wasm_pages": 1}
387 })
388 );
389 assert_eq!(
390 serde_json::to_value(allocation_pool).unwrap(),
391 serde_json::json!({
392 "authorities": [], "excluded_ids": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
393 })
394 );
395 }
396
397 #[test]
398 fn diagnostic_codes_have_stable_wire_names() {
399 let cases = [
400 (DiagnosticCode::StableCell, "stable_cell"),
401 (DiagnosticCode::UnsupportedFormat, "unsupported_format"),
402 (DiagnosticCode::LedgerRecovery, "ledger_recovery"),
403 (
404 DiagnosticCode::AllocationValidation,
405 "allocation_validation",
406 ),
407 (DiagnosticCode::PolicyIdentity, "policy_identity"),
408 (DiagnosticCode::RuntimeBinding, "runtime_binding"),
409 ];
410
411 for (code, expected) in cases {
412 assert_eq!(
413 crate::test_cbor::to_value(code).expect("diagnostic code value"),
414 crate::test_cbor::Value::Text(expected.to_string())
415 );
416 }
417 }
418
419 #[test]
420 fn diagnostic_export_can_include_memory_sizes() {
421 let declaration = AllocationDeclaration::new(
422 "app.users.v1",
423 MemoryManagerSlot::new(100).expect("usable slot"),
424 )
425 .expect("declaration");
426 let ledger = AllocationLedger {
427 current_generation: 3,
428 records: vec![AllocationRecord::active(&declaration)],
429 };
430
431 let mut export = DiagnosticExport::from_ledger(&ledger);
432 export.records[0].memory_size = Some(DiagnosticMemorySize::from_wasm_pages(2));
433
434 assert_eq!(
435 export.records[0].memory_size,
436 Some(DiagnosticMemorySize { wasm_pages: 2 })
437 );
438 let wire = serde_json::to_value(&export).expect("diagnostic JSON");
439 assert_eq!(
440 wire["records"][0]["memory_size"],
441 serde_json::json!({"wasm_pages": 2})
442 );
443 }
444}