Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,14 @@
load("@rules_rust//rust:defs.bzl", "rust_proc_macro")
rust_proc_macro(
name = "everestrs-derive",
srcs = ["src/lib.rs"],
edition = "2021",
visibility = ["//visibility:public"],
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
"@everest_framework_crate_index//:proc-macro2",
"@everest_framework_crate_index//:quote",
"@everest_framework_crate_index//:syn",
],
)

View File

@@ -0,0 +1,13 @@
[package]
name = "everestrs-derive"
version = "0.25.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
everestrs-build = { workspace = true }

View File

@@ -0,0 +1,528 @@
use proc_macro::TokenStream;
use quote::quote;
use std::path::PathBuf;
use syn::{parse_macro_input, spanned::Spanned, ItemFn, ItemMod, Type};
/// Attribute macro that wraps an EVerest module's `main` function to control
/// module lifecycle. The user function receives a borrowed `&Module`, ensuring
/// it cannot be dropped prematurely. After the user function returns, the
/// `Module` is dropped deterministically before process exit.
///
/// # Basics
///
/// The basic usage is demonstated below
///
/// ```ignore
/// #[everestrs::main]
/// fn main(module: &Module) {
/// let class = Arc::new(MyModule {});
/// let _publishers = module.start(class.clone(), class.clone());
/// loop { std::thread::sleep(std::time::Duration::from_secs(1)); }
/// }
/// ```
///
/// # Async
///
/// You can also use async for your code. The pattern is quite similar:
///
/// ```ignore
/// #[everestrs::main]
/// #[tokio::main]
/// async fn main(module: &Module) {
/// let class = Arc::new(MyModule {});
/// ...
/// }
#[proc_macro_attribute]
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
if !attr.is_empty() {
let attr2: proc_macro2::TokenStream = attr.into();
return syn::Error::new_spanned(attr2, "#[everestrs::main] takes no arguments")
.to_compile_error()
.into();
}
let input = parse_macro_input!(item as ItemFn);
if let Err(e) = main_validate(&input) {
return e.to_compile_error().into();
}
let sig = &input.sig;
let param = &sig.inputs[0];
let body = &input.block;
let ret = &sig.output;
let maybe_async = &sig.asyncness;
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
let ident = &sig.ident;
let attrs = &input.attrs;
// Extract the parameter name and inner type from `name: &Type`.
let (param_name, inner_ty) = match param {
syn::FnArg::Typed(pat_type) => {
let name = &pat_type.pat;
match pat_type.ty.as_ref() {
Type::Reference(type_ref) => (name, &type_ref.elem),
_ => unreachable!("validated above"),
}
}
_ => unreachable!("validated above"),
};
let expanded = quote! {
#(#attrs)*
#maybe_async fn #ident() #ret {
let #param_name = #inner_ty::new();
let __everest_result = {
#maybe_async fn __everest_main(#param_name: &#inner_ty) #ret
#body
__everest_main(&#param_name) #maybe_await
};
__everest_result
}
};
expanded.into()
}
fn main_validate(input: &ItemFn) -> Result<(), syn::Error> {
if !input.sig.generics.params.is_empty() {
return Err(syn::Error::new(
input.sig.generics.span(),
"#[everestrs::main] does not support generic functions",
));
}
if input.sig.inputs.len() != 1 {
return Err(syn::Error::new(
input.sig.inputs.span(),
"#[everestrs::main] function must have exactly one parameter: `module: &Module`",
));
}
let param = &input.sig.inputs[0];
match param {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(
param.span(),
"#[everestrs::main] function must not take `self`",
));
}
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
Type::Reference(type_ref) => {
if type_ref.mutability.is_some() {
return Err(syn::Error::new(
type_ref.mutability.span(),
"#[everestrs::main] parameter must be a shared reference (`&Module`), not `&mut`",
));
}
}
_ => {
return Err(syn::Error::new(
pat_type.ty.span(),
"#[everestrs::main] parameter must be a reference (e.g. `module: &Module`)",
));
}
},
}
Ok(())
}
struct TestAttr {
config_path: syn::LitStr,
module_name: syn::LitStr,
harness: bool,
}
impl syn::parse::Parse for TestAttr {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let mut config_path = None;
let mut module_name = None;
let mut harness = false;
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
let _: syn::Token![=] = input.parse()?;
match ident.to_string().as_str() {
"config" => {
config_path = Some(input.parse::<syn::LitStr>()?);
}
"module" => {
module_name = Some(input.parse::<syn::LitStr>()?);
}
"harness" => {
harness = input.parse::<syn::LitBool>()?.value;
}
other => {
return Err(syn::Error::new(
ident.span(),
format!(
"unknown attribute `{other}`, expected `config`, `module`, or `harness`"
),
));
}
}
if !input.is_empty() {
let _: syn::Token![,] = input.parse()?;
}
}
let config_path = config_path
.ok_or_else(|| syn::Error::new(input.span(), "missing `config` attribute"))?;
let module_name = module_name
.ok_or_else(|| syn::Error::new(input.span(), "missing `module` attribute"))?;
Ok(Self {
config_path,
module_name,
harness,
})
}
}
/// Attribute macro that creates an EVerest test. It launches the manager with
/// the given config, spawns the module standalone, and then executes the test
/// body. Each test gets a unique MQTT prefix so tests can run in parallel
/// without topic collisions.
///
/// # Basics
///
/// Both `config` and `module` are required.
///
/// It can either be applied inside a module tagged with `everestrs::harness`.
/// In this case all EVerest bindings generation is done by the harness and tests
/// can share generated code.
///
/// ```ignore
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
/// mod my_tests {
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_a(module: &Module) { ... }
///
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_b(module: &Module) { ... }
/// }
/// ```
///
/// Alternatively you can use the test macro to generate the EVerest bindings.
/// In this case the bindings are only accessible in the test itself:
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// fn test_a(module: &Module) { ... }
/// ```
///
/// # Other macros
///
/// The macro can be combined with other commonly used macros, for example
///
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// #[should_panic]
/// fn test_a(module: &Module) {
/// assert!(false);
/// }
/// ```
///
/// You can also combine it with #[tokio::test]. The ordering does not matter
///
/// ```ignore
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// #[tokio::test]
/// async fn my_test(module: &Module) {
/// }
/// ```
///
/// works same as
///
/// ```ignore
/// #[tokio::test]
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
/// async fn my_test(module: &Module) {
/// }
/// ```
#[proc_macro_attribute]
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
let test_attr = parse_macro_input!(attr as TestAttr);
let input = parse_macro_input!(item as ItemFn);
// If config is provided, generate harness + test in a wrapping module.
// If not, just emit the test function (assumes harness is on an enclosing mod).
match test_impl(&test_attr, input) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
fn test_validate(input: &ItemFn) -> Result<(), syn::Error> {
if !input.sig.generics.params.is_empty() {
return Err(syn::Error::new(
input.sig.generics.span(),
"#[everestrs::test] does not support generic functions",
));
}
if input.sig.inputs.len() != 1 {
return Err(syn::Error::new(
input.sig.inputs.span(),
"#[everestrs::test] function must have exactly one parameter",
));
}
let param = &input.sig.inputs[0];
match param {
syn::FnArg::Receiver(_) => {
return Err(syn::Error::new(
param.span(),
"#[everestrs::test] function must not take `self`",
));
}
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
Type::Reference(type_ref) => {
if type_ref.mutability.is_some() {
return Err(syn::Error::new(
type_ref.mutability.span(),
"#[everestrs::test] parameter must be a shared reference, not `&mut`",
));
}
}
_ => {
return Err(syn::Error::new(
pat_type.ty.span(),
"#[everestrs::test] parameter must be a reference (e.g. `module: &Module`)",
));
}
},
}
Ok(())
}
fn test_impl(attr: &TestAttr, item_fn: ItemFn) -> Result<proc_macro2::TokenStream, syn::Error> {
// Sanity checks.
test_validate(&item_fn)?;
// Forward all attributes from the user function to the generated #[test] fn
// (e.g. #[should_panic], #[ignore], #[allow(...)]).
let attrs = &item_fn.attrs;
let sig = &item_fn.sig;
let ident = &sig.ident;
let param = &sig.inputs[0];
let ret = &sig.output;
let body = &item_fn.block;
// Check if someone else after us might emit #[test]. `rstest` does
// something similar to prevent reemit this. We match any attribute whose
// last path segment is `test` — covers `#[test]`, `#[tokio::test]`,
// `#[async_std::test]`, etc.
// See https://github.com/la10736/rstest/blob/master/rstest_macros/src/utils.rs#L38
// and https://github.com/la10736/rstest/blob/master/rstest_macros/src/parse/rstest/test_attr.rs#L25
let maybe_test = if attrs.iter().any(|a| {
a.path()
.segments
.last()
.map_or(false, |s| s.ident == "test")
}) {
quote! {}
} else {
quote! { #[test] }
};
let maybe_async = &sig.asyncness;
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
let module = &attr.module_name;
// Extract the parameter name and inner type from `name: &Type`.
let (param_name, inner_ty) = match param {
syn::FnArg::Typed(pat_type) => {
let name = &pat_type.pat;
match pat_type.ty.as_ref() {
Type::Reference(type_ref) => (name, &type_ref.elem),
_ => unreachable!("validated above"),
}
}
_ => unreachable!("validated above"),
};
// Derive the config filename from the attribute. The bazel rule symlinks
// the config file to etc/everest/<basename>.
let config_basename = std::path::Path::new(&attr.config_path.value())
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
// The function we would like to emit
let test_fn = quote! {
#maybe_test
#(#attrs)*
#maybe_async fn #ident() #ret {
let prefix = std::env::current_dir().expect("Failed to get current directory");
let config = prefix.join(format!("etc/everest/{}", #config_basename));
// Each test gets a unique MQTT prefix so tests can run in parallel
// without topic collisions.
let __mqtt_prefix = format!(
"everest_test_{:?}_{:?}/",
std::process::id(),
std::thread::current().id(),
);
// Start the manager, telling it not to spawn the module under test.
// Blocks until the manager signals readiness via --status-fifo.
let _manager = ::everestrs::manager::Manager::start(
&prefix, &config, &[#module], Some(&__mqtt_prefix),
).expect("Failed to start manager");
let args = everestrs::Args {
prefix: prefix.clone(),
module: #module.to_string(),
log_config: prefix.join("etc/everest/default_logging.cfg"),
mqtt_broker_socket_path: None,
mqtt_broker_host: "localhost".to_string(),
mqtt_broker_port: 1883,
mqtt_everest_prefix: __mqtt_prefix,
mqtt_external_prefix: "".to_string(),
};
let #param_name = #inner_ty::new_with_args(args);
let __everest_result = {
#maybe_async fn __everest_test(#param_name: &#inner_ty) #ret
#body
__everest_test(&#param_name) #maybe_await
};
drop(#param_name);
__everest_result
}
};
if attr.harness {
let generated_tokens = generate_harness_tokens(attr)?;
let mod_ident = &item_fn.sig.ident;
Ok(quote! {
mod #mod_ident {
#generated_tokens
#[allow(unused_imports)]
use generated::Module;
#test_fn
}
})
} else {
Ok(test_fn)
}
}
fn resolve_everest_core_roots() -> Vec<PathBuf> {
if let Ok(val) = std::env::var("EVEREST_CORE_ROOT") {
val.split(':').map(PathBuf::from).collect()
} else {
// Fallback: try CARGO_MANIFEST_DIR and walk up to find the repo root.
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
let mut dir = PathBuf::from(&manifest_dir);
// Walk up looking for a directory that contains an "interfaces" subdirectory.
loop {
if dir.join("interfaces").is_dir() {
return vec![dir];
}
if !dir.pop() {
break;
}
}
}
Vec::new()
}
}
/// Core logic: resolves config, builds synthetic manifest, runs codegen.
/// Returns the generated tokens.
fn generate_harness_tokens(attr: &TestAttr) -> Result<proc_macro2::TokenStream, syn::Error> {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map_err(|_| syn::Error::new(attr.config_path.span(), "CARGO_MANIFEST_DIR not set"))?;
let config_path = PathBuf::from(&manifest_dir).join(attr.config_path.value());
let module_instance = attr.module_name.value();
let everest_core_roots = resolve_everest_core_roots();
if everest_core_roots.is_empty() {
return Err(syn::Error::new(
attr.config_path.span(),
"Could not determine everest-core root. Set EVEREST_CORE_ROOT environment variable.",
));
}
let (manifest, _tracked_files) =
everestrs_build::build_test_manifest(&config_path, &module_instance, &everest_core_roots)
.map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Failed to build test manifest: {e}"),
)
})?;
let generated_code = everestrs_build::codegen::emit_manifest(manifest, everest_core_roots)
.map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Code generation failed: {e}"),
)
})?;
syn::parse_str(&generated_code).map_err(|e| {
syn::Error::new(
attr.module_name.span(),
format!("Failed to parse generated code: {e}"),
)
})
}
/// Attribute macro that auto-generates a test counterpart module by reading
/// a config.yaml and deducing interfaces from connected modules' manifests.
///
/// Applied to a `mod` to share generated types across multiple tests and
/// fixtures. For single test functions, use
/// `#[everestrs::test(config = "...", module = "...")]` instead.
///
/// ```ignore
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
/// mod my_tests {
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_a(module: &Module) { ... }
///
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
/// fn test_b(module: &Module) { ... }
/// }
/// ```
#[proc_macro_attribute]
pub fn harness(attr: TokenStream, item: TokenStream) -> TokenStream {
let test_attr = parse_macro_input!(attr as TestAttr);
let item_mod = parse_macro_input!(item as ItemMod);
match harness_impl(&test_attr, item_mod) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
}
}
/// Generates harness code and injects it into a module body.
fn harness_impl(
attr: &TestAttr,
item_mod: syn::ItemMod,
) -> Result<proc_macro2::TokenStream, syn::Error> {
let generated_tokens = generate_harness_tokens(attr)?;
if let Some((_brace, items)) = item_mod.content {
let vis = &item_mod.vis;
let ident = &item_mod.ident;
let attrs = &item_mod.attrs;
Ok(quote! {
#(#attrs)*
#vis mod #ident {
#generated_tokens
#[allow(unused_imports)]
use generated::Module;
#(#items)*
}
})
} else {
Err(syn::Error::new_spanned(
&item_mod,
"#[everestrs::harness] requires a module with a body (not just a declaration)",
))
}
}