Skip to content

Commit 45ae17f

Browse files
authored
Adds safety case tracing facility in copper's macros (#1075)
* Adds safety case tracing facility in copper's macros * This changed the public API so changing it. * missing new public API spot
1 parent bf2967c commit 45ae17f

13 files changed

Lines changed: 568 additions & 75 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ erased-serde = { version = "0.4", default-features = false, features = [
193193
] }
194194
serde_derive = { version = "1.0", features = ["default"] }
195195
pyo3 = { version = "0.28", default-features = false, features = ["macros"] }
196+
inventory = "0.3"
196197

197198
# External CLI
198199
clap = { version = "4.5", default-features = false, features = [

api/v1/cu29-derive.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub proc macro cu29_derive::bundle_resources!()
33
pub proc macro cu29_derive::#[copper_runtime]
44
pub proc macro cu29_derive::gen_cumsgs!()
55
pub proc macro cu29_derive::resources!()
6+
pub proc macro cu29_derive::#[safety_case]

api/v1/cu29.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub use cu29::replay
2727
pub use cu29::resource
2828
pub use cu29::resources
2929
pub use cu29::rx_channels
30+
pub use cu29::safety_case
3031
pub use cu29::simulation
3132
pub use cu29::tx_channels
3233
pub use cu29::units
@@ -100,6 +101,7 @@ pub use cu29::prelude::observed_encode_bytes
100101
pub use cu29::prelude::output_msg
101102
pub use cu29::prelude::record_observed_encode_bytes
102103
pub use cu29::prelude::rx_channels
104+
pub use cu29::prelude::safety_case
103105
pub use cu29::prelude::to_value
104106
pub use cu29::prelude::tx_channels
105107
pub use cu29::prelude::units
@@ -108,6 +110,8 @@ pub macro cu29::prelude::defmt_debug!
108110
pub macro cu29::prelude::defmt_error!
109111
pub macro cu29::prelude::defmt_info!
110112
pub macro cu29::prelude::defmt_warn!
113+
pub macro cu29::prelude::safety_check!
114+
pub macro cu29::prelude::safety_check_eq!
111115
pub mod cu29::rtsan
112116
pub struct cu29::rtsan::ScopedDisabler
113117
pub struct cu29::rtsan::ScopedSanitizeRealtime
@@ -121,3 +125,5 @@ pub macro cu29::defmt_debug!
121125
pub macro cu29::defmt_error!
122126
pub macro cu29::defmt_info!
123127
pub macro cu29::defmt_warn!
128+
pub macro cu29::safety_check!
129+
pub macro cu29::safety_check_eq!

core/cu29/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ bevy_reflect_derive = { workspace = true, optional = true }
3232
bincode = { workspace = true }
3333
serde = { workspace = true }
3434
serde_derive = { workspace = true }
35+
inventory = { workspace = true, optional = true }
36+
serde_json = { workspace = true, optional = true, default-features = true }
3537

3638
# only std
3739
rayon = { workspace = true, optional = true }
@@ -82,3 +84,4 @@ std = [
8284
"cu29-value/std",
8385
]
8486
rtsan = ["dep:rtsan-standalone", "cu29-derive/rtsan"]
87+
safety-ids = ["std", "dep:inventory", "dep:serde_json"]

core/cu29/src/lib.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
//! - `high-precision-limiter`: std-only hybrid sleep/spin loop limiter for tighter `rate_target_hz` cadence
3030
//! - `async-cl-io`: offload CopperList serialization/logging to a dedicated std thread
3131
//! - `parallel-rt`: prepare the runtime for a future multi-threaded deterministic executor
32+
//! - `safety-ids`: std-only safety-case metadata collection and JSON export helpers
3233
//!
3334
//! ## Concepts behind Copper
3435
//!
@@ -52,7 +53,7 @@
5253
//!
5354
//! ## V1 API status
5455
//!
55-
//! The V1 public contract is defined in `docs/v1-api-surface.md`. The prelude is the
56+
//! The V1 public contract is defined in `doc/v1-api-surface.md`. The prelude is the
5657
//! canonical application import surface; lower-level modules remain addressable by
5758
//! module path when needed, but are not implicitly part of the prelude contract.
5859
//!
@@ -66,7 +67,7 @@ compile_error!("feature `parallel-rt` requires `std`");
6667
#[cfg(not(feature = "std"))]
6768
extern crate alloc;
6869

69-
pub use cu29_derive::{bundle_resources, resources};
70+
pub use cu29_derive::{bundle_resources, resources, safety_case};
7071
pub use cu29_runtime::app;
7172
pub use cu29_runtime::config;
7273
pub use cu29_runtime::context;
@@ -100,6 +101,8 @@ pub use cu29_runtime::rx_channels;
100101
#[cfg(feature = "std")]
101102
pub use cu29_runtime::simulation;
102103
pub use cu29_runtime::tx_channels;
104+
#[cfg(feature = "safety-ids")]
105+
pub mod safety;
103106

104107
#[cfg(feature = "rtsan")]
105108
pub mod rtsan {
@@ -207,6 +210,20 @@ macro_rules! defmt_error {
207210
($($tt:tt)*) => {{}};
208211
}
209212

213+
#[macro_export]
214+
macro_rules! safety_check {
215+
($check_id:literal, $requirement_id:literal, $description:literal, $condition:expr $(,)?) => {
216+
assert!($condition, "{}", $description);
217+
};
218+
}
219+
220+
#[macro_export]
221+
macro_rules! safety_check_eq {
222+
($check_id:literal, $requirement_id:literal, $description:literal, $left:expr, $right:expr $(,)?) => {
223+
assert_eq!($left, $right, "{}", $description);
224+
};
225+
}
226+
210227
/// Canonical imports for Copper applications.
211228
///
212229
/// This module intentionally re-exports each stable application-facing group once.
@@ -217,6 +234,7 @@ pub mod prelude {
217234
#[cfg(feature = "units")]
218235
pub use crate::units;
219236
pub use crate::{defmt_debug, defmt_error, defmt_info, defmt_warn};
237+
pub use crate::{safety_case, safety_check, safety_check_eq};
220238
#[cfg(feature = "reflect")]
221239
pub use bevy_reflect_derive::Reflect;
222240
#[cfg(feature = "signal-handler")]

core/cu29/src/safety.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use serde::Serialize;
2+
use std::fs;
3+
use std::path::Path;
4+
5+
pub use inventory;
6+
7+
#[derive(Debug, Clone, Copy, Serialize)]
8+
pub struct SafetyCheckRef {
9+
pub check_id: &'static str,
10+
pub requirement_id: &'static str,
11+
pub description: &'static str,
12+
pub kind: &'static str,
13+
}
14+
15+
#[derive(Debug, Clone, Copy, Serialize)]
16+
pub struct SafetyCaseRef {
17+
pub package: &'static str,
18+
pub case_id: &'static str,
19+
pub function: &'static str,
20+
pub module_path: &'static str,
21+
pub file: &'static str,
22+
pub checks: &'static [SafetyCheckRef],
23+
}
24+
25+
inventory::collect!(SafetyCaseRef);
26+
27+
#[derive(Debug, Clone, Serialize)]
28+
pub struct PackageSafetyIndex {
29+
pub package: String,
30+
pub cases: Vec<CollectedSafetyCase>,
31+
}
32+
33+
#[derive(Debug, Clone, Serialize)]
34+
pub struct CollectedSafetyCase {
35+
pub package: String,
36+
pub case_id: String,
37+
pub function: String,
38+
pub test_name: String,
39+
pub file: String,
40+
pub checks: Vec<CollectedSafetyCheck>,
41+
}
42+
43+
#[derive(Debug, Clone, Serialize)]
44+
pub struct CollectedSafetyCheck {
45+
pub check_id: String,
46+
pub requirement_id: String,
47+
pub description: String,
48+
pub kind: String,
49+
}
50+
51+
pub fn collect_package_index(package: &str) -> PackageSafetyIndex {
52+
let mut cases: Vec<_> = inventory::iter::<SafetyCaseRef>
53+
.into_iter()
54+
.filter(|case_ref| case_ref.package == package)
55+
.map(|case_ref| CollectedSafetyCase {
56+
package: case_ref.package.to_string(),
57+
case_id: case_ref.case_id.to_string(),
58+
function: case_ref.function.to_string(),
59+
test_name: libtest_name(case_ref.module_path, case_ref.function),
60+
file: case_ref.file.to_string(),
61+
checks: case_ref
62+
.checks
63+
.iter()
64+
.map(|check| CollectedSafetyCheck {
65+
check_id: check.check_id.to_string(),
66+
requirement_id: check.requirement_id.to_string(),
67+
description: check.description.to_string(),
68+
kind: check.kind.to_string(),
69+
})
70+
.collect(),
71+
})
72+
.collect();
73+
74+
cases.sort_by(|left, right| left.case_id.cmp(&right.case_id));
75+
for case in &mut cases {
76+
case.checks
77+
.sort_by(|left, right| left.check_id.cmp(&right.check_id));
78+
}
79+
80+
PackageSafetyIndex {
81+
package: package.to_string(),
82+
cases,
83+
}
84+
}
85+
86+
pub fn write_package_index_json(
87+
path: impl AsRef<Path>,
88+
index: &PackageSafetyIndex,
89+
) -> std::io::Result<()> {
90+
let path = path.as_ref();
91+
if let Some(parent) = path.parent() {
92+
fs::create_dir_all(parent)?;
93+
}
94+
let json = serde_json::to_vec_pretty(index).expect("safety index should serialize");
95+
fs::write(path, json)
96+
}
97+
98+
fn libtest_name(module_path: &str, function: &str) -> String {
99+
let mut segments = module_path.split("::");
100+
let _crate_name = segments.next();
101+
let rest: Vec<_> = segments.collect();
102+
if rest.is_empty() {
103+
function.to_string()
104+
} else {
105+
format!("{}::{}", rest.join("::"), function)
106+
}
107+
}

0 commit comments

Comments
 (0)