Skip to main content

powerio_capi/
diagnostics.rs

1//! The codes the language boundary itself emits, and the workspace gates.
2//!
3//! `BIND` is the one namespace with no Rust error type behind it: these are the
4//! failures a C entry point detects from an argument's representation alone —
5//! a null pointer, bytes that are not UTF-8, a caught panic. A failure that
6//! needs powerio's own vocabulary to detect (an unknown format name, an index
7//! the handle does not have) belongs to `REQUEST` and comes from the crate that
8//! owns the vocabulary.
9
10pub use powerio_core::{
11    Diagnostic, DiagnosticInfo, DiagnosticSeverity, check_registry, check_scope_ownership,
12};
13
14pub mod codes {
15    powerio_core::diagnostic_codes! {
16        BIND_CAPI_NULL_HANDLE = "BIND.CAPI.NULL_HANDLE", Error,
17            "a handle argument was NULL", category = Data;
18        BIND_CAPI_NULL_ARGUMENT = "BIND.CAPI.NULL_ARGUMENT", Error,
19            "a required non-handle argument was NULL", category = Data;
20        BIND_CAPI_INVALID_UTF8 = "BIND.CAPI.INVALID_UTF8", Error,
21            "a string argument is not valid UTF-8", category = Data;
22        BIND_CAPI_INDEX_OUT_OF_RANGE = "BIND.CAPI.INDEX_OUT_OF_RANGE", Error,
23            "an index argument cannot be converted or is out of range", category = Data;
24        BIND_CAPI_PANIC = "BIND.CAPI.PANIC", Error,
25            "a panic was caught at the boundary and did not cross it", category = Data;
26        BIND_CAPI_INVALID_OPTIONS = "BIND.CAPI.INVALID_OPTIONS", Error,
27            "an options struct declared a size or a field value this build cannot honor",
28            category = Data;
29        EMIT_CAPI_SERIALIZE_FAILED = "EMIT.CAPI.SERIALIZE_FAILED", Error,
30            "a document or table an entry point returns could not be built or serialized",
31            category = Output;
32        BIND_CAPI_UNCODED_FAILURE = "BIND.CAPI.UNCODED_FAILURE", Error,
33            "a library failure reached the boundary carrying no finding of its own",
34            category = Data;
35        REQUEST_CAPI_UNKNOWN_FORMULA = "REQUEST.CAPI.UNKNOWN_FORMULA", Error,
36            "the caller named a branch susceptance formula this surface does not have",
37            category = Request;
38        REQUEST_CAPI_TYPE_MISMATCH = "REQUEST.CAPI.TYPE_MISMATCH", Error,
39            "the value does not have the structural type required by the operation",
40            category = Request;
41        REQUEST_CAPI_QUANTITY_UNKNOWN = "REQUEST.CAPI.QUANTITY_UNKNOWN", Error,
42            "the requested operating point quantity is not defined",
43            category = Request;
44        REQUEST_CAPI_ALLOCATION_UNKNOWN = "REQUEST.CAPI.ALLOCATION_UNKNOWN", Error,
45            "the requested load allocation rule is not defined",
46            category = Request;
47    }
48}
49
50/// Every code this crate declares.
51#[must_use]
52pub fn registry() -> Vec<&'static DiagnosticInfo> {
53    codes::ALL.to_vec()
54}
55
56// The workspace gate. This crate is the only one that depends on all five
57// library crates at once, and the release features CI job builds it with every
58// feature on, so it is the one place a code shared by two crates shows up.
59#[cfg(all(test, feature = "dist", feature = "prob", feature = "matrix"))]
60mod workspace {
61    use super::*;
62
63    fn registries() -> Vec<(&'static str, Vec<&'static DiagnosticInfo>)> {
64        vec![
65            ("powerio-tx", powerio_tx::diagnostics::registry()),
66            ("powerio (stored + transform)", powerio::codes::registry()),
67            #[cfg(feature = "gridfm")]
68            (
69                "powerio (gridfm reader)",
70                powerio::gridfm_codes::ALL.to_vec(),
71            ),
72            ("powerio-dist", powerio_dist::diagnostics::registry()),
73            ("powerio-matrix", powerio_matrix::diagnostics::registry()),
74            ("powerio-prob", powerio_prob::diagnostics::registry()),
75            ("powerio-capi", registry()),
76        ]
77    }
78
79    #[test]
80    fn every_code_in_the_workspace_is_registered_once_and_well_formed() {
81        let all: Vec<&DiagnosticInfo> = registries()
82            .into_iter()
83            .flat_map(|(_, entries)| entries)
84            .collect();
85        let problems = check_registry(all.iter().copied());
86        assert!(problems.is_empty(), "{problems:#?}");
87    }
88
89    #[test]
90    fn no_two_crates_claim_one_scope() {
91        let owned = registries();
92        let borrowed: Vec<(&str, &[&DiagnosticInfo])> = owned
93            .iter()
94            .map(|(name, entries)| (*name, entries.as_slice()))
95            .collect();
96        let problems = check_scope_ownership(&borrowed);
97        assert!(problems.is_empty(), "{problems:#?}");
98    }
99
100    /// Any stable code string the ABI implementation spells inline resolves to
101    /// a registered entry in some workspace registry, so a bare unregistered
102    /// literal cannot reach a `PioError`. The module's own test block may
103    /// fabricate codes and is excluded. No inline strings is also valid: ABI
104    /// code should normally refer to registry entries directly.
105    #[test]
106    fn every_code_string_the_abi_emits_is_registered() {
107        let source = include_str!("lib.rs")
108            .split("#[cfg(test)]")
109            .next()
110            .expect("split yields the leading source");
111        let registered: std::collections::BTreeSet<&str> = registries()
112            .into_iter()
113            .flat_map(|(_, entries)| entries)
114            .map(|entry| entry.code)
115            .collect();
116        for piece in source.split('"').skip(1).step_by(2) {
117            let dotted = piece.split('.').count() >= 3
118                && piece.split('.').all(|segment| {
119                    !segment.is_empty()
120                        && segment.bytes().all(|byte| {
121                            byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_'
122                        })
123                });
124            if dotted {
125                assert!(registered.contains(piece), "`{piece}` is not registered");
126            }
127        }
128    }
129}