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,4 @@
# The default path to the link dependencies file generated by CMake. Overwritten by CMake, this is a fallback for IDEs.
[env.EVEREST_RS_LINK_DEPENDENCIES]
value = "../../../build/everestrs-link-dependencies.txt"
relative = true

View File

@@ -0,0 +1,134 @@
set(CXXBRIDGE_VERSION 1.0.194) # Must be kept in sync with `everestrs/Cargo.toml`
set(CXXBRIDGE_INSTALL_PATH ${CMAKE_CURRENT_BINARY_DIR}/cargo/bin/cxxbridge)
# Checks if the cxxbridge binary at CXXBRIDGE_PATH exists and matches the required version.
# If not, sets VERSION_MATCHES to FALSE.
function(everestrs_check_cxxbridge_version CXXBRIDGE_PATH VERSION_MATCHES)
if(NOT CXXBRIDGE_BINARY OR NOT EXISTS ${CXXBRIDGE_BINARY})
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
endif()
execute_process(
COMMAND
${CXXBRIDGE_BINARY} --version
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
OUTPUT_VARIABLE
CXXBRIDGE_VERSION_OUTPUT
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(CXXBRIDGE_VERSION_OUTPUT MATCHES "cxxbridge ${CXXBRIDGE_VERSION}")
set(${VERSION_MATCHES} TRUE PARENT_SCOPE)
else()
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
endif()
endfunction()
# If cxxbridge is already installed system-wide, check if it's the correct version.
find_program(CXXBRIDGE_BINARY cxxbridge)
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
# Otherwise, check if we previously installed it in our build dir.
if(NOT CXXBRIDGE_INSTALLED)
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
endif()
# Either we never had cxxbridge installed, or the version is incorrect. Install the correct version to our build dir.
if(NOT CXXBRIDGE_INSTALLED)
message(STATUS "Fetching rust cxxbridge cli tool")
execute_process(
COMMAND
${CMAKE_COMMAND} -E env --unset=CARGO_BUILD_TARGET # Ensure we always build for the host
cargo install cxxbridge-cmd --force --version ${CXXBRIDGE_VERSION} --root ${CMAKE_CURRENT_BINARY_DIR}/cargo
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
RESULT_VARIABLE
CARGO_INSTALL_RESULT
OUTPUT_QUIET
ERROR_VARIABLE
CARGO_INSTALL_ERROR
)
if(NOT CARGO_INSTALL_RESULT)
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
else()
message(FATAL_ERROR "cargo install cxxbridge-cmd failed with:\n${CARGO_INSTALL_ERROR}")
endif()
endif()
set(CXXBRIDGE_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR})
set(CXXBRIDGE_HEADER ${CXXBRIDGE_OUTPUT_DIR}/rust/cxx.h)
set(EVERESTRS_FFI_HEADER ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.h)
set(EVERESTRS_FFI_SOURCE ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.cc)
add_custom_command(
OUTPUT
${EVERESTRS_FFI_HEADER}
${EVERESTRS_FFI_SOURCE}
${CXXBRIDGE_HEADER}
COMMAND
${CMAKE_COMMAND} -E make_directory ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src ${CXXBRIDGE_OUTPUT_DIR}/rust
COMMAND
${CXXBRIDGE_BINARY} --header -o ${CXXBRIDGE_HEADER}
COMMAND
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs --header -o ${EVERESTRS_FFI_HEADER}
COMMAND
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs -o ${EVERESTRS_FFI_SOURCE}
DEPENDS
everestrs/src/lib.rs
COMMENT "Generating cxxbridge glue for everestrs"
VERBATIM
DEPENDS
${CXXBRIDGE_BINARY}
)
add_library(everestrs_sys STATIC
${EVERESTRS_FFI_SOURCE}
everestrs/src/everestrs_sys.cpp
)
add_library(everest::everestrs_sys ALIAS everestrs_sys)
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs
)
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs-build
)
target_include_directories(everestrs_sys
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
)
# This is a requirement that linking works on systems enforcing PIE
# FIXME (aw): investicate why this is really necessary
set_property(TARGET everestrs_sys PROPERTY POSITION_INDEPENDENT_CODE ON)
target_link_libraries(everestrs_sys
PRIVATE
everest::framework
everest::log
)
set(EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:everest::everestrs_sys>)
get_target_property(EVERESTRS_LINK_TARGETS everest::everestrs_sys LINK_LIBRARIES)
foreach(link_target IN LISTS EVERESTRS_LINK_TARGETS)
list(APPEND EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:${link_target}>)
endforeach()
set_property(
TARGET
everestrs_sys
PROPERTY
EVERESTRS_LINK_DEPENDENCIES "${EVERESTRS_LINK_DEPENDENCIES}"
)

View File

@@ -0,0 +1,742 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys",
]
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "argh"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240"
dependencies = [
"argh_derive",
"argh_shared",
"rust-fuzzy-search",
]
[[package]]
name = "argh_derive"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "argh_shared"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cc"
version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "clap"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cxx"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
dependencies = [
"cc",
"cxx-build",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
dependencies = [
"cc",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
dependencies = [
"clap",
"codespan-reporting",
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
[[package]]
name = "cxxbridge-macro"
version = "1.0.194"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
dependencies = [
"indexmap",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "everestrs"
version = "0.25.0"
dependencies = [
"clap",
"cxx",
"everestrs-build",
"everestrs-derive",
"log",
"mockall",
"mockall_double",
"nix",
"serde",
"serde_json",
"serde_yaml",
"thiserror",
"tokio",
]
[[package]]
name = "everestrs-build"
version = "0.25.0"
dependencies = [
"anyhow",
"argh",
"convert_case",
"minijinja",
"serde",
"serde_json",
"serde_yaml",
]
[[package]]
name = "everestrs-derive"
version = "0.25.0"
dependencies = [
"everestrs-build",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "fragile"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "link-cplusplus"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
dependencies = [
"cc",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minijinja"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3287d827e6da221ea11aa173c66b82ab69db27a1b177e8439f730b478bf33a7b"
dependencies = [
"serde",
]
[[package]]
name = "mockall"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "mockall_double"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "nix"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rust-fuzzy-search"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scratch"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.52.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
dependencies = [
"pin-project-lite",
"tokio-macros",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -0,0 +1,11 @@
[workspace]
resolver = "2"
members = [
"everestrs",
"everestrs-build",
"everestrs-derive",
]
[workspace.dependencies]
everestrs-build = { path="everestrs-build" }
everestrs-derive = { path="everestrs-derive" }

View File

@@ -0,0 +1,46 @@
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
filegroup(
name = "templates",
srcs = glob(["jinja/**/*"]),
)
rust_library(
name = "everestrs-build",
srcs = glob(["src/**/*.rs"], exclude = ["src/bin/**"]),
deps = [
"@everest_framework_crate_index//:anyhow",
"@everest_framework_crate_index//:argh",
"@everest_framework_crate_index//:convert_case",
"@everest_framework_crate_index//:minijinja",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
],
compile_data = [":templates"],
visibility = ["//visibility:public"],
edition = "2021",
)
rust_binary(
name = "codegen",
srcs = glob(["src/bin/**/*.rs"]),
deps = [
"@everest_framework_crate_index//:anyhow",
"@everest_framework_crate_index//:argh",
"@everest_framework_crate_index//:convert_case",
"@everest_framework_crate_index//:minijinja",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
":everestrs-build",
],
visibility = ["//visibility:public"],
edition = "2021",
)
rust_test(
name = "test",
crate = ":everestrs-build",
edition = "2021",
)

View File

@@ -0,0 +1,15 @@
[package]
name = "everestrs-build"
version = "0.25.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
argh = "0.1.12"
convert_case = "0.6.0"
minijinja = "1.0.8"
serde = "1.0.188"
serde_json = "1.0.107"
serde_yaml = "0.9.25"

View File

@@ -0,0 +1,152 @@
/// {{trait.description | replace("\n", " ")}}
pub(crate) trait {{trait.name | title}}ClientSubscriber: Sync + Send {
{% for var in trait.vars %}
fn on_{{ var.name | snake }}(&self, context: &Context, value: {{ var.data_type.name }});
{% endfor %}
{%- if trait.errors %}
fn on_error_raised(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
fn on_error_cleared(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
{%- endif %}
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) {{trait.name | title}}ClientSubscriber {}
impl {{trait.name | title}}ClientSubscriber for {{trait.name | title}}ClientSubscriber {
{% for var in trait.vars %}
fn on_{{ var.name | snake }}<'a>(&self, context: &Context<'a>, value: {{ var.data_type.name }});
{% endfor %}
{%- if trait.errors %}
fn on_error_raised<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
fn on_error_cleared<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
{%- endif %}
}
}
fn dispatch_variable_to_{{ trait.name | snake }}(
context: &Context,
client_subscriber: &dyn {{trait.name | title}}ClientSubscriber,
name: &str,
value: __serde_json::Value,
) -> ::everestrs::Result<()> {
match name {
{%- for var in trait.vars %}
"{{ var.name }}" => {
let v: {{ var.data_type.name }} = __serde_json::from_value(value)
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize variable `{{ var.name }}`: {e:?})")))?;
client_subscriber.on_{{ var.name | snake }}(context, v);
Ok(())
},
{%- endfor %}
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown variable {other} received.").to_string())),
}
}
fn dispatch_error_to_{{ trait.name | snake }} (
context: &Context,
client_subscriber: &dyn {{ trait.name | title }}ClientSubscriber,
error: ::everestrs::FfiErrorType,
raised: bool
) {
{%- if trait.errors %}
// The type is errors::{{ trait.name | snake }}::Error
let Ok(v) = __serde_yaml::from_str(&error.error_type) else {
everestrs::log::error!("Failed to deserialize error `{}`", error.error_type);
return;
};
let error_type = ::everestrs::ErrorType {
error_type: v,
description: error.description,
message: error.message,
severity: error.severity,
};
if raised {
client_subscriber.on_error_raised(context, error_type);
} else {
client_subscriber.on_error_cleared(context, error_type);
}
{%- endif %}
}
pub(crate) mod __mockall_{{trait.name | snake }}_client {
use super::__serde_json;
use super::types;
use super::errors;
#[derive(Clone)]
pub(crate) struct {{trait.name | title }}ClientPublisher {
pub(super) implementation_id: &'static str,
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
pub(super) index: usize,
}
impl {{trait.name | title }}ClientPublisher {
{%- for cmd in trait.cmds %}
/// {{cmd.description | replace("\n", " ")}}
///
{%- for arg in cmd.arguments %}
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
{%- endfor %}
{% if cmd.result -%}
///
/// Returns: {{cmd.result.description | replace("\n", " ")}}
{% endif -%}
pub(crate) fn {{cmd.name | identifier}}(&self,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}
> {
let args = __serde_json::json!({
{%- for arg in cmd.arguments %}
"{{arg.name}}": {{arg.name | identifier}},
{%- endfor %}
});
let rt = self.runtime.upgrade().ok_or_else(|| {
::everestrs::Error::HandlerException(
"publisher used after Module was dropped".into(),
)
})?;
rt.call_command(self.implementation_id, self.index, "{{ cmd.name }}", &args)
}
{% endfor %}
}
#[cfg(all(feature = "mockall", not(feature = "trait")))]
mockall::mock!{
pub(crate) {{trait.name | title }}ClientPublisher {
{%- for cmd in trait.cmds %}
pub(crate) fn {{cmd.name | identifier}}(&self,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}
>;
{% endfor %}
}
impl Clone for {{trait.name | title }}ClientPublisher {
fn clone(&self) -> Self;
}
}
}
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
pub(crate) use __mockall_{{trait.name | snake }}_client::{{trait.name | title }}ClientPublisher;

View File

@@ -0,0 +1,50 @@
{% for p_config in provided_config %}
/// The configuration for the {{ p_config.name }}.
#[derive(Debug)]
pub(crate) struct {{ p_config.name | title }}Config {
{% for config in p_config.config %}
/// {{ config.description }}
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
{% endfor %}
}
{% endfor %}
/// The configuration for the module. It also contains the config for all other
/// interfaces.
#[derive(Debug)]
pub(crate) struct ModuleConfig {
{% for config in module_config %}
/// {{ config.description }}
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
{% endfor %}
{% for p_config in provided_config %}
/// The config for the `{{ p_config.name }}` interface.
pub(crate) {{ p_config.name }}_config: {{ p_config.name | title }}Config,
{% endfor %}
}
/// Returns the config for the whole module.
impl Module {
pub(crate) fn get_config(&self) -> ModuleConfig {
let raw_config = self.runtime.get_module_configs();
{% for p_config in provided_config %}
let {{ p_config.name }}_config = {{ p_config.name | title }}Config {
{% for config in p_config.config %}
{{ config.name | identifier }}: raw_config.get("{{ p_config.name }}").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
{% endfor %}
};
{% endfor %}
ModuleConfig {
{% for config in module_config %}
{{ config.name | identifier }}: raw_config.get("!module").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
{% endfor %}
{% for p_config in provided_config %}
{{ p_config.name }}_config,
{% endfor %}
}
}
}

View File

@@ -0,0 +1,28 @@
{%- for name, errors in involved_errors | items %}
#[allow(clippy::enum_variant_names)]
pub mod {{ name | snake }} {
use everestrs::serde as __serde;
{%- for error_group in errors %}
/// The error definition of the {{ name }} interface.
/// {{ error_group.error_list.description | replace("\n", " ") }}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub enum {{ error_group.name | title }}Error {
{%- for error_entry in error_group.error_list.errors %}
/// {{ error_entry.description | replace("\n", " ")}}
#[serde(rename = "{{ error_group.name | snake }}/{{ error_entry.name }}")]
{{ error_entry.name | title}},
{%- endfor %}
}
{%- endfor %}
/// All possible errors of the {{ name }} interface.
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
#[serde(untagged)]
pub enum Error {
{%- for error_group in errors %}
{{ error_group.name | title }}({{ error_group.name | title }}Error),
{%- endfor %}
}
}
{%- endfor %}

View File

@@ -0,0 +1,302 @@
mod generated {
#![allow(
clippy::let_unit_value,
clippy::match_single_binding,
clippy::upper_case_acronyms,
clippy::useless_conversion,
clippy::too_many_arguments,
dead_code,
non_camel_case_types,
unused_mut,
unused_variables,
unused_imports,
)]
use everestrs::serde_json as __serde_json;
use everestrs::serde_yaml as __serde_yaml;
pub mod types {
{% include "types" %}
}
pub mod errors {
{% include "errors" %}
}
{% include "config" %}
/// Called when the module receives on_ready from EVerest.
pub(crate) trait OnReadySubscriber: Sync + Send {
fn on_ready(&self, pub_impl: &ModulePublisher);
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) OnReadySubscriber {}
impl OnReadySubscriber for OnReadySubscriber {
fn on_ready(&self, pub_impl: &ModulePublisher);
}
}
{% for trait in provided_interfaces %}
{% include "service" %}
{% endfor %}
{% for trait in required_interfaces %}
{% include "client" %}
{% endfor %}
#[derive(Clone)]
#[cfg_attr(all(test, feature="mockall", not(feature="trait")), derive(Default))]
pub(crate) struct ModulePublisher {
{% for provide in provides %}
pub(crate) {{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
pub(crate) {{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher,
{% elif require.min_connections == require.max_connections %}
pub(crate) {{ require.implementation_id | identifier }}_slots: [{{ require.interface | title}}ClientPublisher; {{require.min_connections}}],
{% else %}
pub(crate) {{ require.implementation_id | identifier }}_slots: Vec<{{require.interface | title}}ClientPublisher>,
{% endif %}
{% endfor %}
}
struct ModuleInner {
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: [::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>; {{require.max_connections}}],
{% else %}
{{ require.implementation_id | identifier }}_slots: Vec<::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>>,
{% endif %}
{% endfor %}
publisher: ModulePublisher,
ready: ::std::sync::Condvar,
ready_flag: ::std::sync::Mutex<bool>,
}
pub(crate) struct Module {
runtime: ::std::pin::Pin<::std::sync::Arc<::everestrs::Runtime>>,
inner: ::std::sync::OnceLock<::std::sync::Arc<ModuleInner>>,
}
/// The context structure.
pub(crate) struct Context<'a> {
pub(crate) publisher: &'a ModulePublisher,
{# TODO(ddo) Clarify the naming. #}
/// The name as in `implementation_id`.
pub name: &'a str,
/// The index of the slot.
pub index: usize,
}
impl Module {
#[must_use]
pub(crate) fn new() -> Self {
let runtime = ::everestrs::Runtime::new();
Self {
runtime,
inner: ::std::sync::OnceLock::new(),
}
}
#[must_use]
pub(crate) fn new_with_args(args: ::everestrs::Args) -> Self {
let runtime = ::everestrs::Runtime::new_with_args(args);
Self {
runtime,
inner: ::std::sync::OnceLock::new(),
}
}
pub(crate) fn start
{% if requires_with_generics %}
<
{% for require in requires %}
{% if require.min_connections != 1 or require.max_connections != 1 %}
{{ require.implementation_id | title }}Callback: FnMut(usize) -> ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% endif %}
{% endfor %}
>
{% endif %}
(
&self,
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
{% else %}
{{ require.implementation_id | identifier }}_cb: {{ require.implementation_id | title }}Callback,
{% endif %}
{% endfor %}
) -> &ModulePublisher {
let runtime = &self.runtime;
let connections = runtime.get_module_connections();
// Publishers hold a Weak<Runtime> so ModuleInner -> publishers ->
// Runtime -> sub_impl -> ModuleInner is not a cycle. Drop of the
// Module deterministically tears down Runtime, ModuleInner, and the
// mock subscribers inside it.
let runtime_weak = ::std::sync::Arc::downgrade(&::std::pin::Pin::into_inner(runtime.clone()));
let inner = self.inner.get_or_init(|| {
::std::sync::Arc::new(ModuleInner {
on_ready,
{% for provide in provides %}
{{ provide.implementation_id | identifier }},
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }},
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn({{ require.implementation_id | identifier }}_cb),
{% else %}
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map({{ require.implementation_id | identifier }}_cb).collect(),
{% endif %}
{% endfor %}
#[cfg(any(not(test), not(feature = "mockall"), feature = "trait"))]
publisher: ModulePublisher {
{% for provide in provides %}
{{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher {
implementation_id: "{{ provide.implementation_id }}",
runtime: runtime_weak.clone(),
},
{% endfor %}
{% for require in requires %}
{% if require.min_connections == 1 and require.max_connections == 1 %}
{{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher {
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: 0,
},
{% elif require.min_connections == require.max_connections %}
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn(|i| {{require.interface | title}}ClientPublisher{
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: i,
}),
{% else %}
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map(|i| {{require.interface | title}}ClientPublisher{
implementation_id: "{{ require.implementation_id }}",
runtime: runtime_weak.clone(),
index: i,
}).collect(),
{% endif %}
{% endfor %}
},
#[cfg(all(test, feature = "mockall", not(feature = "trait")))]
publisher: ModulePublisher::default(),
ready: ::std::sync::Condvar::new(),
ready_flag: ::std::sync::Mutex::new(false),
})
});
runtime.as_ref().set_subscriber(inner.clone());
// Block until on_ready has fired.
let mut ready = inner.ready_flag.lock().unwrap();
while !*ready {
ready = inner.ready.wait(ready).unwrap();
}
&inner.publisher
}
}
impl ::everestrs::Subscriber for ModuleInner {
fn handle_command(
&self,
implementation_id: &str,
name: &str,
parameters: ::std::collections::HashMap<String, __serde_json::Value>,
) -> ::everestrs::Result<__serde_json::Value> {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index: 0,
};
match implementation_id {
{% for provide in provides %}
"{{ provide.implementation_id }}" => {
dispatch_command_to_{{ provide.interface | snake }}(&context, self.{{ provide.implementation_id | identifier }}.as_ref(), name, parameters)
},
{% endfor %}
other => Err(::everestrs::Error::MessageParsingError(
format!("Unknown implementation_id {other} called."),
)),
}
}
fn handle_variable(
&self,
implementation_id: &str,
index: usize,
name: &str,
value: __serde_json::Value,
) -> ::everestrs::Result<()> {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index,
};
match implementation_id {
{% for req in requires %}
"{{ req.implementation_id }}" => {
{% if req.min_connections == 1 and req.max_connections == 1 %}
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), name, value)
{% else %}
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), name, value)
{% endif %}
},
{% endfor %}
other => Err(::everestrs::Error::MessageParsingError(
format!("Unknown variable {other} received."),
))
}
}
fn handle_on_error(
&self,
implementation_id: &str,
index: usize,
error: ::everestrs::FfiErrorType,
raised: bool
) {
let context = Context {
publisher: &self.publisher,
name: implementation_id,
index,
};
match implementation_id {
{% for req in requires %}
"{{ req.implementation_id }}" => {
{% if req.min_connections == 1 and req.max_connections == 1 %}
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), error, raised)
{% else %}
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), error, raised)
{% endif %}
},
{% endfor %}
_ => everestrs::log::error!("Received an unknown error from {implementation_id}"),
}
}
fn on_ready(&self) {
self.on_ready.on_ready(&self.publisher);
let mut ready = self.ready_flag.lock().unwrap();
*ready = true;
self.ready.notify_all();
}
}
}

View File

@@ -0,0 +1,138 @@
/// {{trait.description | replace("\n", " ")}}
pub(crate) trait {{trait.name | title}}ServiceSubscriber: Sync + Send {
{%- for cmd in trait.cmds %}
/// {{cmd.description | replace("\n", " ")}}
///
{%- for arg in cmd.arguments %}
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
{%- endfor %}
{% if cmd.result -%}
///
/// Returns: {{cmd.result.description | replace("\n", " ")}}
{% endif -%}
fn {{cmd.name}}(&self,
context: &Context,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}>;
{% endfor %}
}
#[cfg(all(feature = "mockall", feature = "trait"))]
mockall::mock! {
pub(crate) {{trait.name | title}}ServiceSubscriber {}
impl {{trait.name | title}}ServiceSubscriber for {{trait.name | title}}ServiceSubscriber {
{%- for cmd in trait.cmds %}
fn {{cmd.name}}<'a>(&self,
context: &Context<'a>,
{%- for arg in cmd.arguments %}
{{arg.name | identifier }}: {{arg.data_type.name}},
{%- endfor %}
) -> ::everestrs::Result<{%- if cmd.result -%}
{{cmd.result.data_type.name}}
{%- else -%}
()
{%- endif -%}>;
{% endfor %}
}
}
fn dispatch_command_to_{{ trait.name | snake }}(
context: &Context,
service: &dyn {{trait.name | title}}ServiceSubscriber,
name: &str,
mut parameters: ::std::collections::HashMap<String, __serde_json::Value>,
) -> ::everestrs::Result<__serde_json::Value> {
match name {
{%- for cmd in trait.cmds %}
"{{ cmd.name }}" => {
{%- for arg in cmd.arguments %}
let {{ arg.name | identifier }}: {{ arg.data_type.name }} = __serde_json::from_value(
parameters.remove("{{ arg.name }}")
.ok_or(::everestrs::Error::MessageParsingError("Argument `{{ arg.name }}` not provided".to_string()))?,
)
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize argument `{{ arg.name }}`: {e:?}")))?;
{%- endfor %}
let retval = service.{{ cmd.name }}(context,
{%- for arg in cmd.arguments %}
{{ arg.name | identifier }},
{%- endfor %}
)?;
__serde_json::to_value(retval).map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to serialize result: {e:?}")))
},
{%- endfor %}
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown command `{other}` called."))),
}
}
pub(crate) mod __mockall_{{trait.name | snake }}_service {
use super::types;
use super::errors;
#[derive(Clone)]
pub(crate) struct {{trait.name | title }}ServicePublisher {
pub(super) implementation_id: &'static str,
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
}
impl {{trait.name | title }}ServicePublisher {
{% for var in trait.vars %}
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()> {
if let Some(runtime) = self.runtime.upgrade() {
runtime.publish_variable(self.implementation_id, "{{ var.name }}", &value)
}
Ok(())
}
{% endfor %}
{%- if trait.errors %}
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.raise_error(self.implementation_id, error);
}
}
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.clear_error(self.implementation_id, error, true);
}
}
pub(crate) fn clear_all_errors(&self) {
if let Some(runtime) = self.runtime.upgrade() {
runtime.clear_error(self.implementation_id, "", true);
}
}
{%- endif %}
}
#[cfg(all(feature = "mockall", not(feature = "trait")))]
mockall::mock!{
pub(crate) {{trait.name | title }}ServicePublisher {
{% for var in trait.vars %}
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()>;
{% endfor %}
{%- if trait.errors %}
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error);
pub(crate) fn clear_all_errors(&self);
{%- endif %}
}
impl Clone for {{trait.name | title }}ServicePublisher {
fn clone(&self) -> Self;
}
}
}
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
pub(crate) use __mockall_{{trait.name | snake }}_service::{{trait.name | title }}ServicePublisher;

View File

@@ -0,0 +1,31 @@
{% for name, types in types.children | items %}
pub mod {{ name }} {
mod types { pub use super::super::*; }
{% include "types" %}
}
{% endfor %}
use everestrs::serde as __serde;
{% for object in types.objects %}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub struct {{ object.name }} {
{% for p in object.properties %}
/// {{ p.description | replace("\n", " ") }}
#[serde(rename="{{ p.name }}"{% if p.data_type.extra_serde_annotations %},{{ p.data_type.extra_serde_annotations | join(",") }}{% endif %})]
pub {{ p.name | identifier }}: {{ p.data_type.name }},
{% endfor %}
}
{% endfor %}
{% for enum in types.enums %}
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
#[serde(crate = "__serde")]
pub enum {{ enum.name }} {
{% for item in enum.items %}
{{ item }},
{% endfor %}
}
{% endfor %}

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use argh::FromArgs;
use everestrs_build::Builder;
use std::path::PathBuf;
#[derive(FromArgs)]
/// Codegen for EVerest-rs
struct Args {
/// path to EVerest
#[argh(option)]
pub everest_core: Vec<PathBuf>,
/// manifest to generate code for
#[argh(option)]
pub manifest: PathBuf,
/// output directory to put the generated code to.
#[argh(option)]
pub out_dir: PathBuf,
}
pub fn main() -> Result<()> {
let args: Args = argh::from_env();
Builder::new(args.manifest, args.everest_core)
.out_dir(args.out_dir)
.generate()?;
Ok(())
}

View File

@@ -0,0 +1,904 @@
use crate::schema::{
self,
interface::ErrorReference,
manifest::{ConfigEntry, ConfigEnum, Ignore},
types::{DataTypes, ObjectOptions, StringOptions, Type, TypeBase, TypeEnum},
ErrorList, Interface, Manifest,
};
use anyhow::{anyhow, bail, Context, Result};
use convert_case::{Case, Casing};
use minijinja::{Environment, UndefinedBehavior};
use serde::{de::DeserializeOwned, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
// We include the JINJA templates into the binary. This has the disadvantage
// that every change to the templates requires a recompilation, but the
// advantage that the codegen library/binary is truly standalone and needs
// nothing shipped with it to work.
const CLIENT_JINJA: &str = include_str!("../jinja/client.jinja2");
const CONFIG_JINJA: &str = include_str!("../jinja/config.jinja2");
const ERRORS_JINJA: &str = include_str!("../jinja/errors.jinja2");
const MODULE_JINJA: &str = include_str!("../jinja/module.jinja2");
const SERVICE_JINJA: &str = include_str!("../jinja/service.jinja2");
const TYPES_JINJA: &str = include_str!("../jinja/types.jinja2");
fn is_reserved_keyword(s: &str) -> bool {
// From https://doc.rust-lang.org/reference/keywords.html.
matches!(
s,
"abstract"
| "as"
| "async"
| "await"
| "become"
| "box"
| "break"
| "const"
| "continue"
| "crate"
| "do"
| "dyn"
| "else"
| "enum"
| "extern"
| "false"
| "final"
| "fn"
| "for"
| "if"
| "impl"
| "in"
| "let"
| "loop"
| "macro"
| "macro_rules"
| "match"
| "mod"
| "move"
| "mut"
| "override"
| "priv"
| "pub"
| "ref"
| "return"
| "self"
| "static"
| "struct"
| "super"
| "trait"
| "true"
| "try"
| "type"
| "typeof"
| "union"
| "unsafe"
| "unsized"
| "use"
| "virtual"
| "where"
| "while"
| "yield"
)
}
fn lazy_load<'a, T: DeserializeOwned>(
storage: &'a mut HashMap<String, T>,
everest_root: &Vec<PathBuf>,
prefix: &str,
postfix: &str,
) -> Result<&'a mut T> {
if storage.contains_key(postfix) {
return Ok(storage.get_mut(postfix).unwrap());
}
let mut matches = everest_root
.iter()
.filter_map(|core| {
let p = core.join(format!("{prefix}/{postfix}.yaml"));
// If the file is missing we ignore the error since it may be
// present in an different root.
let Ok(blob) = fs::read_to_string(&p) else {
return None;
};
let out = serde_yaml::from_str(&blob).with_context(|| format!("Failed to parse {p:?}"));
match out {
Err(err) => {
println!("{err:?}");
None
}
Ok(res) => Some(res),
}
})
.collect::<Vec<_>>();
assert!(
matches.len() == 1,
"The name `{prefix}/{postfix}` must be defined exactly once: Found {}",
{ matches.len() }
);
storage.insert(postfix.to_string(), matches.pop().unwrap());
Ok(storage.get_mut(postfix).unwrap())
}
/// A lazy loader for YAML files. If the same file is requested twice, it will
/// not be re-parsed again.
#[derive(Default, Debug)]
struct YamlRepo {
// This might be also a HashMap of "namespaces" and paths.
everest_root: Vec<PathBuf>,
interfaces: HashMap<String, Interface>,
data_types: HashMap<String, DataTypes>,
error_types: HashMap<String, ErrorList>,
}
impl YamlRepo {
pub fn new(everest_root: Vec<PathBuf>) -> Self {
Self {
everest_root,
..Default::default()
}
}
pub fn get_interface<'a>(&'a mut self, name: &str) -> Result<&'a mut Interface> {
lazy_load(&mut self.interfaces, &self.everest_root, "interfaces", name)
}
pub fn get_data_types<'a>(&'a mut self, name: &str) -> Result<&'a mut DataTypes> {
lazy_load(&mut self.data_types, &self.everest_root, "types", name)
}
pub fn get_errors<'a>(&'a mut self, prefix: &str, name: &str) -> Result<&'a mut ErrorList> {
lazy_load(&mut self.error_types, &self.everest_root, prefix, name)
}
}
// We just pull out of ObjectOptions what we really need for codegen.
#[derive(Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
struct TypeRef {
/// The same as the file name under EVerest/types.
module_path: Vec<String>,
type_name: String,
}
impl TypeRef {
fn from_object(args: &ObjectOptions) -> Result<Self> {
assert!(args.object_reference.is_some());
assert!(
args.properties.is_empty(),
"Found an object with $ref, but also with properties. Cannot handle that case."
);
Self::from_reference(args.object_reference.as_ref().unwrap())
}
fn from_string(args: &StringOptions) -> Result<Self> {
assert!(args.object_reference.is_some());
Self::from_reference(args.object_reference.as_ref().unwrap())
}
fn from_reference(r: &str) -> Result<Self> {
let parts: Vec<_> = r.trim_start_matches('/').split("#/").collect();
if parts.len() != 2 {
bail!("Unexpected type reference: {}", r);
}
let module_name = parts[0].to_string();
let module_path = module_name.split('/').map(|s| s.to_string()).collect();
let type_name = parts[1].to_string();
Ok(Self {
module_path,
type_name,
})
}
pub fn module_name(&self) -> String {
format!("types::{}", self.module_path.join("::"),)
}
pub fn absolute_type_path(&self) -> String {
format!("{}::{}", self.module_name(), self.type_name)
}
}
impl std::fmt::Debug for TypeRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"TypeRef /{}#/{}",
self.module_path.join("/"),
self.type_name
)
}
}
fn as_typename(arg: &TypeBase, type_refs: &mut BTreeSet<TypeRef>) -> Result<String> {
use TypeBase::*;
use TypeEnum::*;
Ok(match arg {
Single(Null) => "()".to_string(),
Single(Boolean(_)) => "bool".to_string(),
Single(String(args)) => {
if args.object_reference.is_none() {
"String".to_string()
} else {
let t = TypeRef::from_string(args)?;
let name = t.absolute_type_path();
type_refs.insert(t);
name
}
}
Single(Number(_)) => "f64".to_string(),
Single(Integer(_)) => "i64".to_string(),
Single(Object(args)) => {
if args.object_reference.is_none() {
"__serde_json::Value".to_string()
} else {
let t = TypeRef::from_object(args)?;
let name = t.absolute_type_path();
type_refs.insert(t);
name
}
}
Single(Array(args)) => match args.items {
None => "Vec<__serde_json::Value>".to_string(),
Some(ref v) => {
let item_type = as_typename(&v.arg, type_refs)?;
format!("Vec<{item_type}>")
}
},
Multiple(_) => "__serde_json::Value".to_string(),
})
}
#[derive(Debug, Clone, Serialize)]
struct DataTypeContext {
name: String,
extra_serde_annotations: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
struct ArgumentContext {
name: String,
description: Option<String>,
data_type: DataTypeContext,
}
impl ArgumentContext {
pub fn from_schema(
name: String,
var: &Type,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
Ok(ArgumentContext {
name,
description: var.description.clone(),
data_type: DataTypeContext {
name: as_typename(&var.arg, type_refs)?,
extra_serde_annotations: Vec::new(),
},
})
}
}
#[derive(Debug, Clone, Serialize)]
struct CommandContext {
name: String,
description: String,
result: Option<ArgumentContext>,
arguments: Vec<ArgumentContext>,
}
impl CommandContext {
pub fn from_schema(
name: String,
cmd: &crate::schema::interface::Command,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
let mut arguments = Vec::new();
for (name, arg) in &cmd.arguments {
arguments.push(ArgumentContext::from_schema(name.clone(), arg, type_refs)?);
}
Ok(CommandContext {
name,
description: cmd.description.clone(),
result: match &cmd.result {
None => None,
Some(arg) => Some(ArgumentContext::from_schema(
"return_value".to_string(),
arg,
type_refs,
)?),
},
arguments,
})
}
}
/// The error group maps to one error yaml file.
#[derive(Debug, Clone, Serialize)]
struct ErrorGroupContext {
/// The name is basically the yaml file in which the errors are defined.
name: String,
/// The list of errors
error_list: schema::error::ErrorList,
}
mod impl_error {
#[derive(Hash, Eq, PartialEq)]
pub struct ErrorPath<'a> {
/// The prefix where the error files are.
pub prefix: &'a str,
/// The error file itself.
pub file: &'a str,
}
pub struct ErrorDefinition<'a> {
/// The path of the error.
pub path: ErrorPath<'a>,
/// The type which is optional. If the type is not defined we accept
/// all errors in the path.
pub error_type: Option<&'a str>,
}
impl<'a> ErrorDefinition<'a> {
/// Try to construct an error definition from the string.
pub fn try_new(value: &'a str) -> anyhow::Result<Self> {
let mut splits = value.split("#/");
let path = splits.next().ok_or(anyhow::anyhow!("No path defined"))?;
// Split the path and remove the empty parts.
// (The first element might be empty if we have a leading `/`).
let paths = path
.split("/")
.filter(|path| !path.is_empty())
.collect::<Vec<_>>();
anyhow::ensure!(paths.len() == 2, "Expecting exactly two paths");
anyhow::ensure!(
paths.iter().all(|path| !path.is_empty()),
"Empty paths not allowed"
);
let path = ErrorPath {
prefix: paths[0],
file: paths[1],
};
let error_type = splits.next();
if let Some(inner) = error_type {
anyhow::ensure!(!inner.is_empty(), "Type must not be empty");
}
Ok(Self { path, error_type })
}
}
}
impl ErrorGroupContext {
/// Generates the [ErrorGroupContext] from the `error_reference`.
///
/// The error_reference can have two forms:
/// - /errors/example
/// - /errors/example#/ExampleErrorA
///
/// The first type is straight forward. For the second type however, we want
/// to group them by their file name.
fn from_yaml(yaml_repo: &mut YamlRepo, errors: &[ErrorReference]) -> Vec<Self> {
// The errors may be defined multiple times. If we find a definition
// which would use all, we use all. Otherwise we use the specific
// defintions.
enum ErrorOption {
/// Use all errors in a file.
All,
/// Use only specific errors in a file.
Some(HashSet<String>),
}
// Find all the error options defined.
let mut error_definitions = HashMap::new();
for error_ref in errors {
let new_error = impl_error::ErrorDefinition::try_new(&error_ref.reference)
.expect("Failed to parse {error_ref}");
let mut error_definition = error_definitions
.entry(new_error.path)
.or_insert(ErrorOption::Some(HashSet::new()));
// We don't "downgrade" `All` to `Some`.
if let ErrorOption::Some(options) = &mut error_definition {
if let Some(new_option) = new_error.error_type {
options.insert(new_option.to_string());
} else {
*error_definition = ErrorOption::All;
}
}
}
let mut output = Vec::new();
// Load the error yaml form the disk.
for (error_path, error_option) in error_definitions {
let error_list = yaml_repo
.get_errors(error_path.prefix, error_path.file)
.unwrap();
let mut error_group_context = ErrorGroupContext {
name: error_path.file.to_string(),
error_list: error_list.clone(),
};
// Remove unused options.
if let ErrorOption::Some(options) = error_option {
error_group_context
.error_list
.errors
.retain(|e| options.contains(&e.name));
}
// The yaml file might have no errors defined at all. This would
// still comply with the EVerest schema but the user can't do
// anything with it.
if !error_group_context.error_list.errors.is_empty() {
output.push(error_group_context);
}
}
output
}
}
#[derive(Debug, Clone, Serialize)]
struct InterfaceContext {
name: String,
description: String,
cmds: Vec<CommandContext>,
vars: Vec<ArgumentContext>,
/// The errors of an interface.
errors: Vec<ErrorGroupContext>,
}
impl InterfaceContext {
pub fn from_yaml(
yaml_repo: &mut YamlRepo,
name: &str,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<Self> {
let interface_yaml = yaml_repo.get_interface(name)?;
let mut vars = Vec::new();
for (name, var) in &interface_yaml.vars {
vars.push(ArgumentContext::from_schema(name.clone(), var, type_refs)?);
}
let mut cmds = Vec::new();
for (name, cmd) in &interface_yaml.cmds {
cmds.push(CommandContext::from_schema(name.clone(), cmd, type_refs)?);
}
// We can only borrow the yaml_repo once. It's actually not necessary so
// we should refactor this.
let description = interface_yaml.description.clone();
let errors = interface_yaml.errors.clone();
let errors = ErrorGroupContext::from_yaml(yaml_repo, &errors);
Ok(InterfaceContext {
name: name.to_string(),
description,
vars,
cmds,
errors,
})
}
}
#[derive(Debug, Clone, Serialize, Default)]
struct TypeModuleContext {
children: BTreeMap<String, TypeModuleContext>,
objects: Vec<ObjectTypeContext>,
enums: Vec<EnumTypeContext>,
}
#[derive(Debug, Clone, Serialize)]
struct ObjectTypeContext {
name: String,
properties: Vec<ArgumentContext>,
}
#[derive(Debug, Clone, Serialize)]
struct EnumTypeContext {
name: String,
items: Vec<String>,
}
#[derive(Debug, Clone)]
enum TypeContext {
Object(ObjectTypeContext),
Enum(EnumTypeContext),
}
fn type_context_from_ref(
r: &TypeRef,
yaml_repo: &mut YamlRepo,
type_refs: &mut BTreeSet<TypeRef>,
) -> Result<TypeContext> {
use TypeBase::*;
use TypeEnum::*;
let module_path = r.module_path.join("/");
let data_types_yaml = yaml_repo.get_data_types(&module_path)?;
let type_descr = data_types_yaml
.types
.get_mut(&r.type_name)
.ok_or_else(|| anyhow!("Unable to find data type {:?}. Is it defined?", r))?;
let mut new_types: BTreeMap<std::string::String, Type> = BTreeMap::new();
let res = match &mut type_descr.arg {
Single(Object(args)) => {
let mut properties = Vec::new();
for (name, var) in &mut args.properties {
let mut extra_serde_annotations = Vec::new();
let data_type = {
// This is some "trick" - if we have enums which are defined
// inplace, we create a new entry.
if let Single(String(enum_args)) = &mut var.arg {
match &enum_args.enum_items {
Some(items) => {
let new_type = Type {
description: Some("An inlined type".to_string()),
arg: Single(String(StringOptions {
pattern: None,
format: None,
max_length: None,
min_length: None,
enum_items: Some(items.clone()),
default: None,
object_reference: None,
})),
qos: None,
};
let new_name = format!(
"{}AutoGen{}",
r.type_name.to_case(Case::Pascal),
name.to_case(Case::Pascal)
);
enum_args.object_reference =
Some(format!("/{}#/{}", module_path, new_name));
new_types.insert(new_name, new_type);
}
_ => {}
}
}
let d = as_typename(&var.arg, type_refs)?;
if !args.required.contains(name) {
extra_serde_annotations
.push("skip_serializing_if = \"Option::is_none\"".to_string());
format!("Option<{}>", d)
} else {
d
}
};
properties.push(ArgumentContext {
name: name.clone(),
description: var.description.clone(),
data_type: DataTypeContext {
name: data_type,
extra_serde_annotations,
},
});
}
Ok(TypeContext::Object(ObjectTypeContext {
name: r.type_name.clone(),
properties,
}))
}
Single(String(args)) => {
assert!(
args.enum_items.is_some(),
"Expected a named string type to be an enum, but {} was not.",
r.type_name
);
Ok(TypeContext::Enum(EnumTypeContext {
name: r.type_name.clone(),
items: args.enum_items.clone().unwrap(),
}))
}
other => unreachable!("Does not support $ref for {other:?}"),
};
data_types_yaml.types.extend(new_types);
return res;
}
#[derive(Debug, Clone, Serialize)]
struct SlotContext {
implementation_id: String,
interface: String,
min_connections: i64,
max_connections: i64,
}
#[derive(Debug, Clone, Serialize)]
struct ConfigContext {
name: String,
config: Vec<ArgumentContext>,
}
#[derive(Debug, Clone, Serialize)]
struct RenderContext {
/// The interfaces the user will need to fill in.
provided_interfaces: Vec<InterfaceContext>,
/// The interfaces we are requiring.
required_interfaces: Vec<InterfaceContext>,
/// All errors involved - those we can raise and those we can receive.
involved_errors: HashMap<String, Vec<ErrorGroupContext>>,
provides: Vec<SlotContext>,
requires: Vec<SlotContext>,
requires_with_generics: bool,
types: TypeModuleContext,
module_config: Vec<ArgumentContext>,
provided_config: Vec<ConfigContext>,
}
fn title_case(arg: String) -> String {
arg.to_case(Case::Pascal)
}
fn snake_case(arg: String) -> String {
arg.to_case(Case::Snake)
}
/// Like `snake_case`, but can deal with reserved names (and will then use raw identifiers).
fn identifier_case(arg: String) -> String {
let arg = snake_case(arg);
if is_reserved_keyword(&arg) {
format!("r#{arg}")
} else {
arg
}
}
/// Converts the config data read from yaml and generates the context for Jinja.
///
/// The config data contains the config name (key) and the config data (value).
/// We use the value to derive the type and the (optional) description.
fn emit_config(config: BTreeMap<String, ConfigEntry>) -> Vec<ArgumentContext> {
config
.into_iter()
.map(|(k, v)| match v.value {
ConfigEnum::Boolean(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "bool".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::Integer(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "i64".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::Number(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "f64".to_string(),
extra_serde_annotations: Vec::new(),
},
},
ConfigEnum::String(_) => ArgumentContext {
name: k,
description: v.description,
data_type: DataTypeContext {
name: "String".to_string(),
extra_serde_annotations: Vec::new(),
},
},
})
.collect::<Vec<_>>()
}
pub fn emit(manifest_path: PathBuf, everest_core: Vec<PathBuf>) -> Result<String> {
let blob = fs::read_to_string(&manifest_path).context("While reading manifest file")?;
let manifest: Manifest = serde_yaml::from_str(&blob).context("While parsing manifest")?;
emit_manifest(manifest, everest_core)
}
pub fn emit_manifest(manifest: Manifest, everest_core: Vec<PathBuf>) -> Result<String> {
let mut yaml_repo = YamlRepo::new(everest_core);
let mut env = Environment::new();
env.set_undefined_behavior(UndefinedBehavior::Strict);
env.add_filter("title", title_case);
env.add_filter("snake", snake_case);
env.add_filter("identifier", identifier_case);
env.add_template("client", CLIENT_JINJA)?;
env.add_template("config", CONFIG_JINJA)?;
env.add_template("errors", ERRORS_JINJA)?;
env.add_template("module", MODULE_JINJA)?;
env.add_template("service", SERVICE_JINJA)?;
env.add_template("types", TYPES_JINJA)?;
let provided_config = manifest
.provides
.iter()
.filter(|(_, data)| !data.config.is_empty())
.map(|(name, data)| ConfigContext {
name: name.clone(),
config: emit_config(data.config.clone()),
})
.collect::<Vec<_>>();
let mut type_refs = BTreeSet::new();
let mut provided_interfaces = HashMap::with_capacity(manifest.provides.len());
let mut provides = Vec::with_capacity(manifest.provides.len());
for (implementation_id, imp) in manifest.provides {
if !provided_interfaces.contains_key(&imp.interface) {
let interface_context =
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
provided_interfaces.insert(imp.interface.clone(), interface_context);
}
provides.push(SlotContext {
implementation_id,
interface: imp.interface.clone(),
min_connections: 1,
max_connections: 1,
})
}
let mut required_interfaces = HashMap::with_capacity(manifest.requires.len());
let mut requires = Vec::with_capacity(manifest.requires.len());
// We remove the intersection off all ignored interfaces from the trait
// signature.
let mut ignored = HashMap::with_capacity(manifest.requires.len());
for (implementation_id, imp) in manifest.requires {
ignored
.entry(imp.interface.clone())
.and_modify(|merged_ignore: &mut Ignore| {
merged_ignore.vars = merged_ignore
.vars
.intersection(&imp.ignore.vars)
.cloned()
.collect();
merged_ignore.errors = merged_ignore.errors & imp.ignore.errors;
})
.or_insert(imp.ignore);
if !required_interfaces.contains_key(&imp.interface) {
let interface_context =
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
required_interfaces.insert(imp.interface.clone(), interface_context);
}
requires.push(SlotContext {
implementation_id,
interface: imp.interface.clone(),
min_connections: imp.min_connections.unwrap_or(1),
max_connections: imp.max_connections.unwrap_or(1),
})
}
for (interface, merged_ignore) in ignored.into_iter() {
// Check if all ignored interfaces are known.
if let Some(required_interface) = required_interfaces.get(&interface) {
if let Some(unknown_var) = merged_ignore.vars.iter().find(|&ignored_var| {
required_interface
.vars
.iter()
.find(|&required_var| &required_var.name == ignored_var)
.is_none()
}) {
panic!("The interface `{interface}` cannot ignore unkown variable `{unknown_var}`");
}
}
// Remove those interfaces which were never used.
required_interfaces
.entry(interface)
.and_modify(|interface| {
interface
.vars
.retain(|cmd| !merged_ignore.vars.contains(&cmd.name));
if merged_ignore.errors {
interface.errors.clear();
}
});
}
let mut type_module_root = TypeModuleContext::default();
let mut done: BTreeSet<TypeRef> = BTreeSet::new();
while done.len() != type_refs.len() {
let mut new = BTreeSet::new();
for t in &type_refs {
if done.contains(t) {
continue;
}
let mut module = &mut type_module_root;
for p in &t.module_path {
module = module.children.entry(p.clone()).or_default();
}
match type_context_from_ref(t, &mut yaml_repo, &mut new)? {
TypeContext::Object(item) => module.objects.push(item),
TypeContext::Enum(item) => module.enums.push(item),
}
done.insert(t.clone());
}
type_refs.extend(new.into_iter());
}
let module_config = emit_config(manifest.config);
let requires_with_generics = requires
.iter()
.any(|elem| elem.min_connections != 1 || elem.max_connections != 1);
let involved_errors = provided_interfaces
.iter()
.chain(required_interfaces.iter())
.filter(|(_key, value)| !value.errors.is_empty())
.map(|(key, value)| (key.clone(), value.errors.clone()))
.collect::<HashMap<_, _>>();
let context = RenderContext {
provided_interfaces: provided_interfaces.values().cloned().collect(),
required_interfaces: required_interfaces.values().cloned().collect(),
involved_errors,
provides,
requires,
requires_with_generics,
types: type_module_root,
module_config,
provided_config,
};
let tmpl = env.get_template("module").unwrap();
Ok(tmpl.render(context).unwrap())
}
#[cfg(test)]
mod tests {
#[test]
fn test_split_paths_invalid() {
use super::impl_error::*;
let invalid_input = [
"/foo/bar/baz", // too many
"/foo", // too few,
"/foo/", // no type
"//foo", // no path,
"", // just empty
];
for input in invalid_input {
assert!(ErrorDefinition::try_new(input).is_err());
}
}
#[test]
fn test_split_paths() {
use super::impl_error::*;
let res = ErrorDefinition::try_new("/foo/bar#/baz").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(matches!(res.error_type, Some("baz")));
let res = ErrorDefinition::try_new("/foo/bar").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(res.error_type.is_none());
let res = ErrorDefinition::try_new("foo/bar#/baz").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(matches!(res.error_type, Some("baz")));
let res = ErrorDefinition::try_new("foo/bar").unwrap();
assert_eq!(res.path.prefix, "foo");
assert_eq!(res.path.file, "bar");
assert!(res.error_type.is_none());
}
}

View File

@@ -0,0 +1,53 @@
pub mod codegen;
pub mod manifest_resolver;
pub mod schema;
use anyhow::{Context, Result};
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
pub use manifest_resolver::build_test_manifest;
#[derive(Debug, Default)]
pub struct Builder {
everest_root: Vec<PathBuf>,
// TODO(hrapp): This is almost always the same anyways.
manifest_path: PathBuf,
out_dir: Option<PathBuf>,
}
impl Builder {
pub fn new(manifest_path: impl Into<PathBuf>, everest_root: Vec<impl Into<PathBuf>>) -> Self {
Self {
everest_root: everest_root
.into_iter()
.map(|element| element.into())
.collect::<Vec<_>>(),
manifest_path: manifest_path.into(),
..Builder::default()
}
}
pub fn out_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.out_dir = Some(path.into());
self
}
pub fn generate(self) -> Result<()> {
let path = self
.out_dir
.unwrap_or_else(|| PathBuf::from(std::env::var("OUT_DIR").unwrap()))
.join("generated.rs");
let out = codegen::emit(self.manifest_path, self.everest_root)?;
let mut f = std::fs::File::create(&path).context("Could not generate the output file.")?;
f.write_all(out.as_bytes())?;
if let Err(_) = Command::new("rustfmt").args(path.to_str()).output() {
println!("Failed to format code");
}
Ok(())
}
}

View File

@@ -0,0 +1,205 @@
use crate::schema;
use crate::schema::manifest::{Manifest, ProvidesEntry, RequiresEntry};
use anyhow::{anyhow, bail, Context, Result};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
mod inner {
use super::*;
/// A cache that lazily loads and stores module manifests by module type name.
pub(super) struct ManifestCache<'a> {
everest_core: &'a [PathBuf],
entries: BTreeMap<String, (PathBuf, Manifest)>,
}
impl<'a> ManifestCache<'a> {
pub(super) fn new(everest_core: &'a [PathBuf]) -> Self {
Self {
everest_core,
entries: BTreeMap::new(),
}
}
/// Returns the manifest for `module_type`, loading it on first access.
pub(super) fn get(&mut self, module_type: &str) -> Result<&Manifest> {
if !self.entries.contains_key(module_type) {
let (path, manifest) = find_manifest(module_type, self.everest_core)?;
self.entries
.insert(module_type.to_string(), (path, manifest));
}
Ok(&self.entries[module_type].1)
}
/// Returns the paths of all manifests that were loaded.
pub(super) fn into_paths(self) -> impl Iterator<Item = PathBuf> {
self.entries.into_values().map(|(path, _)| path)
}
}
/// Recursively searches `dir` for a directory named `module_type` containing
/// a `manifest.yaml`. Returns the path to the manifest on first match.
/// Symlinks are skipped to avoid circular traversal.
fn find_manifest_in(dir: &Path, module_type: &str) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
for entry in entries.flatten() {
let ft = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
if ft.is_symlink() {
continue;
}
let path = entry.path();
if path.file_name().map_or(false, |n| n == module_type) {
let manifest = path.join("manifest.yaml");
if manifest.is_file() {
return Some(manifest);
}
}
if ft.is_dir() {
if let Some(found) = find_manifest_in(&path, module_type) {
return Some(found);
}
}
}
None
}
/// Finds a module manifest by searching for `{ModuleName}/manifest.yaml`
/// anywhere under each everest_core root. Modules can be nested arbitrarily
/// deep (e.g. `modules/Examples/RustExamples/RsExample/manifest.yaml`).
/// Symlinks are skipped to avoid circular traversal.
fn find_manifest(module_type: &str, everest_core: &[PathBuf]) -> Result<(PathBuf, Manifest)> {
for root in everest_core {
if let Some(path) = find_manifest_in(root, module_type) {
let blob = std::fs::read_to_string(&path)
.with_context(|| format!("Failed to read {path:?}"))?;
let manifest: Manifest = serde_yaml::from_str(&blob)
.with_context(|| format!("Failed to parse {path:?}"))?;
return Ok((path, manifest));
}
}
bail!("Could not find manifest for module type '{module_type}' in any everest_core root");
}
}
/// Reads config.yaml, finds the target module instance, deduces its
/// interfaces from connected modules' manifests, returns a synthetic
/// Manifest and the list of files read (for dependency tracking).
pub fn build_test_manifest(
config_path: &Path,
module_instance: &str,
everest_core: &[PathBuf],
) -> Result<(Manifest, Vec<PathBuf>)> {
let blob = std::fs::read_to_string(config_path)
.with_context(|| format!("While reading config {config_path:?}"))?;
let config: schema::Config =
serde_yaml::from_str(&blob).with_context(|| format!("While parsing {config_path:?}"))?;
let target = config.active_modules.get(module_instance).ok_or_else(|| {
anyhow!("Module instance '{module_instance}' not found in {config_path:?}")
})?;
let mut cache = inner::ManifestCache::new(everest_core);
// Step 1: Resolve outgoing connections (what the target module requires).
// For each connection slot in the target module, find what interface the
// connected module provides at that implementation_id.
let mut requires = BTreeMap::new();
for (slot_name, connections) in &target.connections {
let mut interfaces = std::collections::HashSet::new();
for conn in connections {
let connected_module = config.active_modules.get(&conn.module_id).ok_or_else(|| {
anyhow!(
"Connected module '{}' not found in {config_path:?}",
conn.module_id
)
})?;
let connected_manifest = cache.get(&connected_module.module)?;
let provides_entry = connected_manifest
.provides
.get(&conn.implementation_id)
.ok_or_else(|| {
anyhow!(
"Module type '{}' does not provide '{}'",
connected_module.module,
conn.implementation_id
)
})?;
interfaces.insert(provides_entry.interface.clone());
}
if interfaces.len() != 1 {
bail!("Slot '{slot_name}' has connections with mismatched interfaces: {interfaces:?}");
}
requires.insert(
slot_name.clone(),
RequiresEntry {
interface: interfaces.into_iter().next().unwrap(),
min_connections: Some(1),
max_connections: Some(connections.len() as i64),
ignore: Default::default(),
},
);
}
// Step 2: Resolve incoming connections (what the target module must provide).
// Scan all other modules' connections to find ones pointing at our target.
let mut provides = BTreeMap::new();
for (other_id, other_module) in &config.active_modules {
if other_id == module_instance {
continue;
}
for (other_slot, connections) in &other_module.connections {
for conn in connections {
if conn.module_id != module_instance {
continue;
}
// other_module requires interface via other_slot,
// connected to our target's conn.implementation_id.
let other_manifest = cache.get(&other_module.module)?;
let requires_entry = other_manifest.requires.get(other_slot).ok_or_else(|| {
anyhow!(
"Module type '{}' does not require '{}'",
other_module.module,
other_slot
)
})?;
provides.insert(
conn.implementation_id.clone(),
ProvidesEntry {
interface: requires_entry.interface.clone(),
description: format!(
"Auto-generated from {}.{} connection",
other_id, other_slot
),
config: BTreeMap::new(),
},
);
}
}
}
let manifest = Manifest {
description: format!("Synthetic test manifest for {module_instance}"),
metadata: None,
provides,
requires,
enable_telemetry: false,
enable_external_mqtt: false,
config: BTreeMap::new(),
capabilities: Vec::new(),
enable_global_errors: false,
};
let mut tracked_files = vec![config_path.to_path_buf()];
tracked_files.extend(cache.into_paths());
Ok((manifest, tracked_files))
}

View File

@@ -0,0 +1,20 @@
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Debug, Deserialize)]
pub struct Config {
pub active_modules: BTreeMap<String, ActiveModule>,
}
#[derive(Debug, Deserialize)]
pub struct ActiveModule {
pub module: String,
#[serde(default)]
pub connections: BTreeMap<String, Vec<Connection>>,
}
#[derive(Debug, Deserialize)]
pub struct Connection {
pub module_id: String,
pub implementation_id: String,
}

View File

@@ -0,0 +1,57 @@
use serde::{Deserialize, Serialize};
/// Implements the schema defined under `error-declaration.yaml`. Every type has
/// mandatory `name` and `description` fields.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Error {
/// The description of the error.
pub description: String,
/// The name of the error.
pub name: String,
/// The namespace of the error.
pub namespace: Option<String>,
}
/// Implements the list of errors.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorList {
/// The description of all errors in the file.
pub description: String,
/// The list of errors.
/// We add default to allow make the `errors` field optional.
#[serde(default)]
pub errors: Vec<Error>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_yaml;
#[test]
fn test_deserialization() {
// Test with the list.
let _ = serde_yaml::from_str::<ErrorList>(
r#"
description: this is a description
errors:
- name: foo
description: bar
- name: this
description: that
"#,
)
.unwrap();
// Test without the list
let _ = serde_yaml::from_str::<ErrorList>(
r#"
description: just a description without errors
"#,
)
.unwrap();
}
}

View File

@@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use super::types::Type;
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Interface {
pub description: String,
#[serde(default)]
pub cmds: BTreeMap<String, Command>,
#[serde(default)]
pub vars: BTreeMap<String, Type>,
/// The error reference represents the entry in the manifest were
/// we reference an error file.
#[serde(default)]
pub errors: Vec<ErrorReference>,
}
/// The same as the one above but the cpp runtime returns the errors as a map
/// contrary to the definition inside the yaml file...
#[derive(Debug, Deserialize, Serialize)]
pub struct InterfaceFromEverest {
// Note: EVerest config over mqtt does not return descriptions even so
// they should be necessary.
#[serde(default)]
pub description: String,
#[serde(default)]
pub cmds: BTreeMap<String, Command>,
#[serde(default)]
pub vars: BTreeMap<String, Type>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Command {
// Note: EVerest config over mqtt does not return descriptions even so
// they should be necessary.
#[serde(default)]
pub description: String,
#[serde(default)]
pub arguments: BTreeMap<String, Type>,
pub result: Option<Type>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ErrorReference {
pub reference: String,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_deserialization() {
serde_yaml::from_str::<Interface>(
r#"
description: >-
This is an example interface used for the error framework example modules.
errors:
- reference: /errors/example#/ExampleErrorA
- reference: /errors/example#/ExampleErrorB
- reference: /errors/example#/ExampleErrorC
- reference: /errors/example#/ExampleErrorD
"#,
)
.unwrap();
serde_yaml::from_str::<Interface>(
r#"
description: Nothing here.
"#,
)
.unwrap();
}
}

View File

@@ -0,0 +1,98 @@
use super::types::{BooleanOptions, IntegerOptions, NumberOptions, StringOptions};
use serde::Deserialize;
use std::collections::{BTreeMap, HashSet};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
#[serde(default)]
pub description: String,
#[serde(default)]
pub metadata: Option<Metadata>,
pub provides: BTreeMap<String, ProvidesEntry>,
#[serde(default)]
pub requires: BTreeMap<String, RequiresEntry>,
#[serde(default)]
pub enable_telemetry: bool,
// This is just here, so that we do not crash for deny_unknown_fields,
// this is never used in Rust code.
#[allow(dead_code)]
#[serde(default)]
pub enable_external_mqtt: bool,
#[serde(default)]
pub config: BTreeMap<String, ConfigEntry>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub enable_global_errors: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProvidesEntry {
pub interface: String,
pub description: String,
#[serde(default)]
pub config: BTreeMap<String, ConfigEntry>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequiresEntry {
pub interface: String,
pub min_connections: Option<i64>,
pub max_connections: Option<i64>,
#[serde(default)]
pub ignore: Ignore,
}
#[derive(Debug, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Ignore {
#[serde(default)]
pub vars: HashSet<String>,
#[serde(default)]
pub errors: bool,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Metadata {
pub license: String,
pub authors: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConfigEntry {
pub description: Option<String>,
#[serde(flatten)]
pub value: ConfigEnum,
#[serde(default = "MutabilityEnum::default")]
pub mutability: MutabilityEnum,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
pub enum ConfigEnum {
Boolean(BooleanOptions),
String(StringOptions),
Integer(IntegerOptions),
Number(NumberOptions),
}
#[derive(Debug, Clone, Deserialize)]
pub enum MutabilityEnum {
ReadOnly,
ReadWrite,
WriteOnly,
}
impl MutabilityEnum {
fn default() -> Self {
MutabilityEnum::ReadOnly
}
}

View File

@@ -0,0 +1,11 @@
pub mod config;
pub mod error;
pub mod interface;
pub mod manifest;
pub mod types;
pub use config::Config;
pub use error::ErrorList;
pub use interface::{Interface, InterfaceFromEverest};
pub use manifest::Manifest;
pub use types::Type;

View File

@@ -0,0 +1,155 @@
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, HashSet};
/// Implements the schema defined under `type.yaml`. Every type has a `type`
/// and a `description` field.
#[derive(Debug, Deserialize, Serialize)]
pub struct Type {
// TODO(ddo) The schema says that this field is required, but multiple
// type definitions do not obey this rule.
pub description: Option<String>,
#[serde(flatten)]
pub arg: TypeBase,
/// This is part of the Variable definition.
pub qos: Option<i64>,
}
/// The type may be either represented by a string or by an array of strings.
/// In the case of an array of strings.
#[derive(Debug, Serialize)]
pub enum TypeBase {
Single(TypeEnum),
Multiple(Vec<TypeEnum>),
}
impl<'de> Deserialize<'de> for TypeBase {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let serde_yaml::Value::Mapping(map) = Deserialize::deserialize(deserializer)? else {
return Err(serde::de::Error::custom("Variable must be a mapping"));
};
let arg_type = map
.get("type")
.ok_or("The `type` tag is missing")
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
let arg = match arg_type {
serde_yaml::Value::String(_) => {
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(map))
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
TypeBase::Single(t)
}
serde_yaml::Value::Sequence(s) => {
let mut types = Vec::with_capacity(s.len());
for t in s.into_iter() {
let mut mapping = serde_yaml::Mapping::new();
mapping.insert(serde_yaml::Value::String("type".to_string()), t.clone());
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(mapping))
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
types.push(t);
}
TypeBase::Multiple(types)
}
_ => {
return Err(serde::de::Error::custom(
"'type' must be a sequence or a string.",
))
}
};
Ok(arg)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct BooleanOptions {
pub default: Option<bool>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NumberOptions {
pub minimum: Option<f64>,
pub maximum: Option<f64>,
pub default: Option<f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct IntegerOptions {
pub minimum: Option<i64>,
pub maximum: Option<i64>,
pub default: Option<i64>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ArrayOptions {
pub min_items: Option<usize>,
pub max_items: Option<usize>,
pub items: Option<Box<Type>>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ObjectOptions {
#[serde(default)]
pub properties: BTreeMap<String, Type>,
#[serde(default)]
pub required: HashSet<String>,
#[serde(default)]
pub additional_properties: bool,
#[serde(rename = "$ref")]
pub object_reference: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum StringFormat {
#[serde(rename = "date-time")]
DateTime,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct StringOptions {
pub pattern: Option<String>,
pub format: Option<StringFormat>,
pub max_length: Option<usize>,
pub min_length: Option<usize>,
#[serde(rename = "enum")]
pub enum_items: Option<Vec<String>>,
pub default: Option<String>,
#[serde(rename = "$ref")]
pub object_reference: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
pub enum TypeEnum {
Null,
Boolean(BooleanOptions),
String(StringOptions),
Number(NumberOptions),
Integer(IntegerOptions),
Array(ArrayOptions),
Object(ObjectOptions),
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DataTypes {
pub description: String,
pub types: BTreeMap<String, Type>,
}

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)",
))
}
}

View File

@@ -0,0 +1,81 @@
load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
load("@rules_cc//cc:defs.bzl", "cc_library")
load("@rules_rust//rust:defs.bzl", "rust_library")
rust_library(
name = "everestrs",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
proc_macro_deps = [
"//lib/everest/framework/everestrs/everestrs-derive",
],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge",
":everestrs_sys",
"//lib/everest/framework/everestrs/everestrs-build",
"@cxx.rs//:cxx",
"@everest_framework_crate_index//:clap",
"@everest_framework_crate_index//:log",
"@everest_framework_crate_index//:nix",
"@everest_framework_crate_index//:serde",
"@everest_framework_crate_index//:serde_json",
"@everest_framework_crate_index//:serde_yaml",
"@everest_framework_crate_index//:thiserror",
],
)
run_binary(
name = "everestrs_bridge/generated",
srcs = ["src/lib.rs"],
outs = [
"src/lib.rs.cc",
"src/lib.rs.h",
],
args = [
"$(location src/lib.rs)",
"-o",
"$(location src/lib.rs.h)",
"-o",
"$(location src/lib.rs.cc)",
],
tool = "@cxx.rs//:codegen",
)
cc_library(
name = "everestrs_bridge",
srcs = ["src/lib.rs" + ".cc"],
cxxopts = ["-std=c++17"],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge/include",
":everestrs_sys_include",
"//lib/everest/framework",
],
)
cc_library(
name = "everestrs_sys",
srcs = ["src/everestrs_sys.cpp"],
cxxopts = ["-std=c++17"],
visibility = ["//visibility:public"],
deps = [
":everestrs_bridge/include",
":everestrs_sys_include",
"//lib/everest/framework",
],
alwayslink = True,
)
cc_library(
name = "everestrs_bridge/include",
hdrs = ["src/lib.rs" + ".h"],
include_prefix = "everestrs",
)
cc_library(
name = "everestrs_sys_include",
hdrs = ["src/everestrs_sys.hpp"],
include_prefix = "everestrs",
deps = ["@cxx.rs//:core"],
)

View File

@@ -0,0 +1,28 @@
[package]
name = "everestrs"
version = "0.25.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.27", features = ["derive"] }
everestrs-build = { workspace = true }
everestrs-derive = { workspace = true }
log = { version = "0.4.20", features = ["std"] }
serde = { version = "1.0.175", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9.34"
thiserror = "1.0.48"
nix = { version = "0.31.2", features = ["fs"] }
# Note: the version must be kept in sync with the `cxxbridge-cmd` version installed in `everestrs/CMakeLists.txt`.
# The `=` before the version number ensures that Cargo uses exactly this version when generating new lockfiles.
cxx = { version = "=1.0.194", features = ["c++17"] }
[features]
build_bazel = []
# We use this for the integration tests in this project.
[dev-dependencies]
mockall = "0.13.0"
mockall_double = "0.3.1"
# For tests
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "sync", "time"] }

View File

@@ -0,0 +1,63 @@
# Rust support for EVerest
This is Rust support using cxx.rs to wrap the framework C++ library.
## Trying it out
- Install Rust as outlined on <https://rustup.rs/>, which should just be this
one line: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
- Built your workspace as outlined in `EVerest` README, make sure to tell
cMake to enable `EVEREST_ENABLE_RS_SUPPORT`.
- You can now try building the code, but it will not do anything: `cd everestrs
&& cargo build --all`
- You should now be able to configure the `RsExample` or `RsExampleUser` modules in your config
YAML.
## Differences to other EVerest language wrappers
- The `enable_external_mqtt` is ignored for Rust modules. If you want to interact
with MQTT externally, just pull an external mqtt module (for example the
really excellent [rumqttc](https://docs.rs/rumqttc/latest/rumqttc/)) crate
into your module and use it directly. This is a conscious decision to future
proof, should EVerest at some point move to something different than MQTT as
transport layer and for cleaner abstraction.
## Status
Full support for requiring and providing interfaces is implemented, missing
currently is:
- Support for EVerest logging
- Support for implementations with `max_connections != 1` or `min_connections != 1`
## Mocking in Unit-Tests
The Rust wrapper supports mocking, which allows you to unit tests your modules.
To enable mocking in your code you need to do some steps however
* Add [mockall](https://github.com/asomers/mockall) and
[mockall_double](https://github.com/asomers/mockall) to your module as dependencies
* Add a `mockall` feature to your module and enable it for your tests.
Then all publishers are mocked with `mockall`.
## Building from external repositories without CMake
Note that the `EVerest` (and `everest-framework` prior to migration to a monorepo layout) repositories are
automatically configured for this use case, this section is only relevant for
Rust modules residing in their own repositories.
While external Rust modules can be compiled using CMake without any setup,
compiling with `cargo` directly requires some additional setup. This is
required for rust-analyzer's IDE integration to work properly.
- First, build your EVerest workspace with Rust support enabled by
passing `-DEVEREST_ENABLE_RS_SUPPORT=ON` to CMake.
- Create a file at `modules/.cargo/config.toml` in your repository.
It file should contain the following, with `<build-dir>` replaced by the
name of your EVerest build directory.
```toml
[env.EVEREST_RS_LINK_DEPENDENCIES]
value = "../<build-dir>/everestrs-link-dependencies.txt"
relative = true
```

View File

@@ -0,0 +1,81 @@
use std::{
env,
fs::File,
io::{self, BufRead, BufReader},
path::Path,
};
/// Takes a path to a library like `libframework.so` and returns the name for the linker, aka
/// `framework`
fn libname_from_path(p: &Path) -> String {
let base = p
.file_stem()
.and_then(|os_str| os_str.to_str())
.expect("'p' must be valid UTF-8 and have an extension.")
.strip_prefix("lib")
.expect("'p' should start with `lib`");
// foo.so.1.2.3 -> foo
base.split('.').next().unwrap_or(base).to_string()
}
fn print_link_options(p: &Path) {
println!(
"cargo:rustc-link-search=native={}",
p.parent().unwrap().to_string_lossy()
);
println!("cargo:rustc-link-lib={}", libname_from_path(p));
}
/// Registers the libraries specified in the `EVEREST_RS_LINK_DEPENDENCIES` environment variable.
/// Expected to be a path to a text file that contains one object file path per line.
fn register_everest_link_deps(link_deps_path: &str) -> io::Result<()> {
let link_deps = File::open(link_deps_path).map_err(|e| {
io::Error::new(
e.kind(),
format!("Could not open EVEREST_RS_LINK_DEPENDENCIES file '{link_deps_path}': {e}"),
)
})?;
let mut found_any = false;
for line in BufReader::new(link_deps).lines() {
let line = line?;
let path = Path::new(&line);
if !path.is_file() {
return Err(io::Error::new(io::ErrorKind::NotFound, format!(
"Cannot find library path '{line}' specified in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path}).",
)));
}
print_link_options(path);
found_any = true;
}
if found_any {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("No library paths found in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path})."),
))
}
}
fn main() {
// See https://doc.rust-lang.org/cargo/reference/features.html#build-scripts
// for details.
if env::var("CARGO_FEATURE_BUILD_BAZEL").is_ok() {
println!("Skipping due to bazel");
return;
}
let Ok(link_deps_path) = env::var("EVEREST_RS_LINK_DEPENDENCIES") else {
panic!("EVEREST_RS_LINK_DEPENDENCIES environment variable is not set. This variable should point to the build/everestrs-link-dependencies.txt file generated by CMake.");
};
register_everest_link_deps(&link_deps_path)
.expect("Failed to register libraries specified in EVEREST_RS_LINK_DEPENDENCIES");
println!("cargo:rustc-link-lib=boost_log");
println!("cargo:rustc-link-lib=boost_log_setup");
}

View File

@@ -0,0 +1,310 @@
#include "everestrs/src/everestrs_sys.hpp"
#include "everestrs/src/lib.rs.h"
#include <everest/logging.hpp>
#include <utils/conversions.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_manager_req.hpp>
#include <utils/exceptions.hpp>
#include <utils/types.hpp>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <stdexcept>
#include <type_traits>
#include <variant>
namespace {
JsonBlob json2blob(const json& j) {
// I did not find a way to not copy the data at least once here.
const std::string dumped = j.dump();
rust::Vec<uint8_t> vec;
vec.reserve(dumped.size());
std::copy(dumped.begin(), dumped.end(), std::back_inserter(vec));
return JsonBlob{vec};
}
// Below are overloads to be used with std::visit and our std::variant. We force
// a compilation error if someone changes the underlying std::variant without
// extending/adjusting the functions below.
template <typename T, typename... VARIANT_T> struct VariantMemberImpl : public std::false_type {};
template <typename T, typename... VARIANT_T>
struct VariantMemberImpl<T, std::variant<VARIANT_T...>> : public std::disjunction<std::is_same<T, VARIANT_T>...> {};
/// @brief Static checker if the type T can be converted to `everest::config::ConfigEntry`.
///
/// We use this to detect `get_config_field` overloads which receive arguments
/// which aren't part of our `everest::config::ConfigEntry` variant.
template <typename T> struct ConfigEntryMember : public VariantMemberImpl<T, everest::config::ConfigEntry> {};
inline ConfigField get_config_field(const std::string& _name, bool _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Boolean, _value, {}, 0, 0};
}
inline ConfigField get_config_field(const std::string& _name, const std::string& _value) {
static_assert(ConfigEntryMember<std::remove_cv_t<std::remove_reference_t<decltype(_value)>>>::value);
return {_name, ConfigType::String, false, _value, 0, 0};
}
inline ConfigField get_config_field(const std::string& _name, double _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Number, false, {}, _value, 0};
}
inline ConfigField get_config_field(const std::string& _name, int _value) {
static_assert(ConfigEntryMember<decltype(_value)>::value);
return {_name, ConfigType::Integer, false, {}, 0, _value};
}
} // namespace
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix) {
auto socket_path = std::string(mqtt_broker_socket_path);
Everest::MQTTSettings mqtt_settings;
if (not socket_path.empty()) {
Everest::populate_mqtt_settings(mqtt_settings, socket_path, std::string(mqtt_everest_prefix),
std::string(mqtt_external_prefix));
} else {
Everest::populate_mqtt_settings(mqtt_settings, std::string(mqtt_broker_host), mqtt_broker_port,
std::string(mqtt_everest_prefix), std::string(mqtt_external_prefix));
}
return std::make_unique<Module>(std::string(module_id), std::string(prefix), mqtt_settings);
}
Module::Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings) :
module_id_(module_id) {
const auto mqtt_abstraction =
std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(mqtt_settings));
// TODO(ddo) what happens when this returns false?
mqtt_abstraction->connect();
mqtt_abstraction->spawn_main_loop_thread();
const auto result = Everest::get_module_config(mqtt_abstraction, this->module_id_);
this->rs_ = std::make_unique<Everest::RuntimeSettings>(result.at("settings"));
config_ = std::make_shared<Everest::Config>(mqtt_settings, result);
handle_ = std::make_unique<Everest::Everest>(this->module_id_, *this->config_, this->rs_->validate_schema,
mqtt_abstraction, this->rs_->telemetry_prefix,
this->rs_->telemetry_enabled, this->rs_->forward_exceptions);
// Not needed but done to be congruent with the other bindings.
handle_->spawn_main_loop_thread();
}
Module::~Module() {
// MQTTAbstractionImpl spawns a main loop thread whose only exit condition
// is `disconnect_event` being notified. Without this call the `Thread`
// member destructor joins a thread that will never exit.
if (handle_) {
handle_->disconnect();
}
}
JsonBlob Module::get_interface(rust::Str interface_name) const {
const auto& interface_def = config_->get_interface_definition(std::string(interface_name));
return json2blob(interface_def);
}
JsonBlob Module::get_manifest() const {
const std::string& module_name = config_->get_module_name(std::string(module_id_));
return json2blob(config_->get_manifests().at(module_name));
}
void Module::signal_ready(const Runtime& rt) const {
handle_->register_on_ready_handler([&rt]() { rt.on_ready(); });
handle_->signal_ready();
}
void Module::provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const {
using namespace Everest;
handle_->provide_cmd(std::string(implementation_id), std::string(name), [&rt, implementation_id, name](json args) {
JsonBlob blob = rt.handle_command(implementation_id, name, json2blob(args));
const auto retval = json::parse(blob.data.begin(), blob.data.end());
// Check if our command handler failed.
if (retval.contains(conversions::ERROR_TYPE)) {
const auto error_str = retval.at(conversions::ERROR_TYPE).get<std::string>();
const auto error_msg = retval.at(conversions::ERROR_MSG).get<std::string>();
const auto error_enm = conversions::string_to_cmd_error_type(error_str);
switch (error_enm) {
case CmdErrorType::MessageParsingError:
throw MessageParsingError(error_msg);
case CmdErrorType::SchemaValidationError:
throw SchemaValidationError(error_msg);
case CmdErrorType::HandlerException:
throw HandlerException(error_msg);
case CmdErrorType::CmdTimeout:
throw CmdTimeout(error_msg);
case CmdErrorType::Shutdown:
throw Shutdown(error_msg);
case CmdErrorType::NotReady:
throw NotReady(error_msg);
}
}
return retval;
});
}
void Module::subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
rust::String name) const {
const auto req = Requirement{std::string(implementation_id), index};
// The handle_ptr is guaranteed to be alive in the callback.
const auto handle_ptr = handle_.get();
handle_->subscribe_var(req, std::string(name), [&rt, implementation_id, index, name, handle_ptr](json args) {
handle_ptr->ensure_ready();
rt.handle_variable(implementation_id, index, name, json2blob(args));
});
}
void Module::subscribe_all_errors(const Runtime& rt) const {
for (const Requirement& req : config_->get_requirements(module_id_)) {
std::shared_ptr<Everest::error::ErrorManagerReq> error_manager_ptr = nullptr;
try {
error_manager_ptr = handle_->get_error_manager_req(req);
} catch (const std::runtime_error& ex) {
// This is expected if the manifest has `ignore.errors=true`
// configured.
continue;
}
const auto handle_ptr = handle_.get();
error_manager_ptr->subscribe_all_errors(
[&rt, req, handle_ptr](Everest::error::Error error) {
handle_ptr->ensure_ready();
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, true);
},
[&rt, req, handle_ptr](Everest::error::Error error) {
handle_ptr->ensure_ready();
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, false);
});
}
}
JsonBlob Module::call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob blob) const {
using namespace Everest;
const auto req = Requirement{std::string(implementation_id), index};
json retval;
try {
retval = handle_->call_cmd(req, std::string(name), json::parse(blob.data.begin(), blob.data.end()));
} catch (const MessageParsingError& ex) {
to_json(retval, CmdResultError{CmdErrorType::MessageParsingError, ex.what(), nullptr});
} catch (const SchemaValidationError& ex) {
to_json(retval, CmdResultError{CmdErrorType::SchemaValidationError, ex.what(), nullptr});
} catch (const HandlerException& ex) {
to_json(retval, CmdResultError{CmdErrorType::HandlerException, ex.what(), nullptr});
} catch (const CmdTimeout& ex) {
to_json(retval, CmdResultError{CmdErrorType::CmdTimeout, ex.what(), nullptr});
} catch (const Shutdown& ex) {
to_json(retval, CmdResultError{CmdErrorType::Shutdown, ex.what(), nullptr});
} catch (const NotReady& ex) {
to_json(retval, CmdResultError{CmdErrorType::NotReady, ex.what(), nullptr});
}
return json2blob(retval);
}
void Module::publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const {
handle_->publish_var(std::string(implementation_id), std::string(name),
json::parse(blob.data.begin(), blob.data.end()));
}
void Module::raise_error(rust::Str implementation_id, ErrorType error_type) const {
// Here everything is called implementation_id: We have the Rust string type
// then we have its c++ counterpart as std::string. And then we have the
// ImplementationIdentifier type :S
const std::string impl_id = std::string(implementation_id);
const auto full_mapping = handle_->get_3_tier_model_mapping();
std::optional<Mapping> mapping;
if (full_mapping.has_value()) {
const auto& inner = *full_mapping;
mapping = inner.module;
// We might have multiple mappings. In this case we pick the
// implementation mapping is since this is more specific.
const auto impl_mapping_iter = inner.implementations.find(impl_id);
if (impl_mapping_iter != inner.implementations.end()) {
mapping = impl_mapping_iter->second;
}
}
const ImplementationIdentifier id{module_id_, impl_id, mapping};
const Everest::error::Error error{std::string(error_type.error_type),
std::string{},
std::string(error_type.message),
std::string(error_type.description),
id,
static_cast<Everest::error::Severity>(error_type.severity)};
handle_->get_error_manager_impl(std::string(implementation_id))->raise_error(error);
}
void Module::clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const {
const auto manager = handle_->get_error_manager_impl(std::string(implementation_id));
if (error_type.empty()) {
manager->clear_all_errors();
} else {
if (clear_all) {
manager->clear_all_errors(std::string(error_type));
} else {
manager->clear_error(std::string(error_type));
}
}
}
rust::Vec<RsModuleConnections> Module::get_module_connections() const {
const auto connections = config_->get_module_config().connections;
// Iterate over the connections block.
rust::Vec<RsModuleConnections> out;
out.reserve(connections.size());
for (const auto& [req_id, fulfillment] : connections) {
out.emplace_back(RsModuleConnections{rust::String{req_id}, fulfillment.size()});
};
return out;
}
rust::Vec<RsModuleConfig> Module::get_module_configs(rust::Str module_id) const {
// TODO(ddo) We call this before initializing the logger.
const auto module_configs = config_->get_module_configs(std::string(module_id));
rust::Vec<RsModuleConfig> out;
out.reserve(module_configs.size());
// Iterate over all modules stored in the module_config.
for (const auto& mm : module_configs) {
RsModuleConfig mm_out{mm.first, {}};
mm_out.data.reserve(mm.second.size());
// Iterate over all configs stored in the mm (our current module).
for (const auto& cc : mm.second) {
mm_out.data.emplace_back(
std::visit([&](auto&& _value) { return ::get_config_field(cc.first, _value); }, cc.second));
}
out.emplace_back(std::move(mm_out));
}
return out;
}
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file) {
const std::string module_id_cpp{module_id};
const std::string logging_config_file_cpp{logging_config_file};
// // Init the CPP logger.
return Everest::Logging::init(logging_config_file_cpp, module_id_cpp);
}
void log2cxx(int level, int line, rust::Str file, rust::Str message) {
Everest::Logging::ffi_log(level, line, std::string(file), std::string(message));
}

View File

@@ -0,0 +1,66 @@
#pragma once
#include <cstddef>
#include <memory>
#include <string>
#include <framework/everest.hpp>
#include <framework/runtime.hpp>
#include <utils/types.hpp>
#include "rust/cxx.h"
struct JsonBlob;
struct Runtime;
struct RsModuleConfig;
struct RsModuleConnections;
struct ConfigField;
struct ErrorType;
enum class ConfigTypes : uint8_t;
enum class ErrorState : uint8_t;
enum class ErrorSeverity : uint8_t;
class Module {
public:
/// @brief The c'tor should not be called by the user code.
///
/// In order to create the Module use the `create_module` function.
Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings);
/// Stops the MQTT main loop before member destruction so the
/// `mqtt_mainloop_thread` join inside `~MQTTAbstractionImpl` doesn't
/// deadlock.
~Module();
JsonBlob get_manifest() const;
JsonBlob get_interface(rust::Str interface_name) const;
rust::Vec<RsModuleConfig> get_module_configs(rust::Str module_name) const;
rust::Vec<RsModuleConnections> get_module_connections() const;
void signal_ready(const Runtime& rt) const;
void provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const;
JsonBlob call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob args) const;
void subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
rust::String name) const;
void subscribe_all_errors(const Runtime& rt) const;
void publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const;
void raise_error(rust::Str implementation_id, ErrorType error_type) const;
void clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const;
private:
const std::string module_id_;
std::unique_ptr<Everest::RuntimeSettings> rs_;
std::shared_ptr<Everest::Config> config_;
std::unique_ptr<Everest::Everest> handle_;
};
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix);
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file);
void log2cxx(int level, int line, rust::Str file, rust::Str message);

View File

@@ -0,0 +1,854 @@
pub mod manager;
use everestrs_build::schema;
use clap::Parser;
use log::debug;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Once;
use std::sync::OnceLock;
use thiserror::Error;
/// Prevent calling the init of loggers more than once.
static INIT_LOGGER_ONCE: Once = Once::new();
// Reexport everything so the clients can use it.
pub use everestrs_derive::{harness, main, test};
pub use log;
pub use serde;
pub use serde_json;
// TODO(ddo) Drop this again - its only there as a MVP for the enum support
// of errors.
pub use serde_yaml;
/// Errors matching the exceptions defined under `exceptions.hpp`.
///
/// The tags must match the tags defined under `conversions.hpp`. For client
/// side code - always use `HandlerException`.
#[derive(Error, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "__everest__error_type", content = "__everest__error_msg")]
pub enum Error {
#[error("Message Parsing Error: {0}")]
MessageParsingError(String),
#[error("Schema Validation Error: {0}")]
SchemaValidationError(String),
#[error("Handler Exception: {0}")]
HandlerException(String),
#[error("Command Timeout: {0}")]
CmdTimeout(String),
#[error("Shutdown: {0}")]
Shutdown(String),
#[error("Not Ready: {0}")]
NotReady(String),
}
pub type Result<T> = ::std::result::Result<T, Error>;
#[cxx::bridge]
mod ffi {
extern "Rust" {
type Runtime;
fn handle_command(
self: &Runtime,
implementation_id: &str,
name: &str,
json: JsonBlob,
) -> JsonBlob;
fn handle_variable(
self: &Runtime,
implementation_id: &str,
index: usize,
name: &str,
json: JsonBlob,
);
fn handle_on_error(
self: &Runtime,
implementation_id: &str,
index: usize,
error: ErrorType,
raised: bool,
);
fn on_ready(&self);
}
struct JsonBlob {
data: Vec<u8>,
}
/// The possible types a config can have. Note: Naturally this would be am
/// enum **with** values - however, cxx can't (for now) map Rusts enums to
/// std::variant or union.
#[derive(Debug)]
enum ConfigType {
Boolean = 0,
String = 1,
Number = 2,
Integer = 3,
}
/// One config entry: As said above, we can't use an enum and have to
/// declare all values. Note: also Option is not an option...
struct ConfigField {
/// The name of the option, e.x. `max_voltage`.
name: String,
/// Our poor-mans enum.
config_type: ConfigType,
/// The value of the config field. The field has only a meaning if
/// `conifg_type is set to [ConfigType::Boolean].
bool_value: bool,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::String].
string_value: String,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::Number].
number_value: f64,
/// The value of the config field. The field has only a meaning if
/// `config_type` is set to [ConfigType::Integer].
integer_value: i64,
}
/// The configs of one module. Roughly maps to the cpp's counterpart
/// `ModuleConfig`.
struct RsModuleConfig {
/// The name of the group, e.x. "PowerMeter".
module_name: String,
/// All `ConfigFields` in this group.
data: Vec<ConfigField>,
}
/// The information form the `connections` field of our current module.
struct RsModuleConnections {
/// The `implementation_id` of the connection block.
implementation_id: String,
/// Number of slots under the `implementation_id`.
slots: usize,
}
#[derive(Debug)]
pub enum ErrorSeverity {
Low,
Medium,
High,
}
/// Rust's version of the `<utils/error.hpp>`'s Error.
#[derive(Debug)]
pub struct ErrorType {
/// The type of the error. We generate that in the codegen. The
/// full error type looks like "evse_manager/PowermeterTransactionStartFailed"
/// and may have a namespace sprinkled into it (?).
pub error_type: String,
/// The description.
pub description: String,
/// The message - no idea what the difference to the description
/// actually is.
pub message: String,
/// The severity of the error.
pub severity: ErrorSeverity,
}
unsafe extern "C++" {
include!("everestrs/src/everestrs_sys.hpp");
type Module;
/// Creates the module only once. The module lives then until the end
/// of the process.
fn create_module(
module_id: &str,
prefix: &str,
mqtt_broker_socket_path: &str,
mqtt_broker_host: &str,
mqtt_broker_port: &u16,
mqtt_everest_prefix: &str,
mqtt_external_prefix: &str,
) -> UniquePtr<Module>;
/// Returns the manifest.
fn get_manifest(self: &Module) -> JsonBlob;
/// Returns the interface definition.
fn get_interface(self: &Module, interface_name: &str) -> JsonBlob;
/// Registers the callback of the `Subscriber` to be called and calls
/// `Everest::Module::signal_ready`.
fn signal_ready(self: &Module, rt: Pin<&Runtime>);
/// Informs the runtime that we implement the command described by `implementation_id` and
/// `name`, and registers the `handle_command` method from the `Subscriber` as the handler.
fn provide_command(
self: &Module,
rt: Pin<&Runtime>,
implementation_id: String,
name: String,
);
/// Call the command described by 'implementation_id' and `name` with the given 'args'.
/// Returns the return value.
fn call_command(
self: &Module,
implementation_id: &str,
index: usize,
name: &str,
args: JsonBlob,
) -> JsonBlob;
/// Informs the runtime that we want to receive the variable described by
/// `implementation_id` and `name` and registers the `handle_variable` method from the
/// `Subscriber` as the handler.
fn subscribe_variable(
self: &Module,
rt: Pin<&Runtime>,
implementation_id: String,
index: usize,
name: String,
);
/// Subscribes to all errors of the required modules.
fn subscribe_all_errors(self: &Module, rt: Pin<&Runtime>);
/// Returns the `connections` block defined in the `config.yaml` for
/// the current module.
fn get_module_connections(self: &Module) -> Vec<RsModuleConnections>;
/// Publishes the given `blob` under the `implementation_id` and `name`.
fn publish_variable(self: &Module, implementation_id: &str, name: &str, blob: JsonBlob);
/// Raises an error
fn raise_error(self: &Module, implementation_id: &str, error: ErrorType);
/// Clears an error
/// If the error_type is empty, we will clear all errors from the module.
fn clear_error(self: &Module, implementation_id: &str, error_type: &str, clear_all: bool);
/// Returns the module config from cpp.
fn get_module_configs(self: &Module, module_id: &str) -> Vec<RsModuleConfig>;
/// Call this once.
fn init_logging(module_id: &str, prefix: &str, conf: &str) -> i32;
/// Logging sink for the EVerest module.
fn log2cxx(level: i32, line: i32, file: &str, message: &str);
}
}
impl ffi::JsonBlob {
fn as_bytes(&self) -> &[u8] {
&self.data
}
fn deserialize<T: DeserializeOwned>(self) -> T {
// TODO(hrapp): Error handling
serde_json::from_slice(self.as_bytes()).expect(&format!(
"Failed to deserialize {:?}",
String::from_utf8_lossy(self.as_bytes())
))
}
fn from_vec(data: Vec<u8>) -> Self {
Self { data }
}
}
/// Very simple logger to use by the Rust modules.
mod logger {
use super::ffi;
use crate::INIT_LOGGER_ONCE;
pub(crate) struct Logger {
level: log::Level,
}
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
// Rust gives the Error level 1 and all other severities a higher
// value.
metadata.level() <= self.level
}
fn log(&self, record: &log::Record) {
// The doc says `log` has to perform the filtering itself.
if !self.enabled(record.metadata()) {
return;
}
// This mapping should be kept in sync with liblog's
// Everest::Logging::severity_level.
let level = match record.level() {
log::Level::Trace => 0,
log::Level::Debug => 1,
log::Level::Info => 2,
log::Level::Warn => 3,
log::Level::Error => 4,
};
ffi::log2cxx(
level,
record.line().unwrap_or_default() as i32,
record.file().unwrap_or_default(),
&format!("{}", record.args()),
)
}
fn flush(&self) {}
}
impl Logger {
/// Init the logger for everest.
///
/// Don't do this on your own as we must also control some cxx code.
pub(crate) fn init_logger(module_name: &str, prefix: &str, conf: &str) {
INIT_LOGGER_ONCE.call_once(|| {
let level = match ffi::init_logging(module_name, prefix, conf) {
-1 => {
return;
}
0 => log::Level::Trace,
1 => log::Level::Debug,
2 => log::Level::Info,
3 => log::Level::Warn,
4 => log::Level::Error,
_ => log::Level::Info,
};
let logger = Self { level };
log::set_boxed_logger(Box::new(logger)).unwrap();
log::set_max_level(level.to_level_filter());
});
}
}
}
/// The cpp_module is for Rust an opaque type - so Rust can't tell if it is safe
/// to be accessed from multiple threads. We know that the c++ runtime is meant
/// to be used concurrently.
unsafe impl Sync for ffi::Module {}
unsafe impl Send for ffi::Module {}
pub use ffi::{ErrorSeverity, ErrorType as FfiErrorType};
#[derive(Debug)]
pub struct ErrorType<T> {
/// Serialised type from the FfiErrorType
pub error_type: T,
/// Carried over directly from the FfiErrorType
pub description: String,
/// Carried over directly from the FfiErrorType
pub message: String,
/// The severity of the error.
/// Carried over directly from the FfiErrorType
pub severity: ErrorSeverity,
}
impl<T> From<T> for ErrorType<T> {
fn from(t: T) -> ErrorType<T> {
ErrorType {
error_type: t,
description: String::new(),
message: String::new(),
severity: ErrorSeverity::High,
}
}
}
/// Arguments for an EVerest node.
#[derive(Parser, Debug)]
pub struct Args {
/// prefix of installation.
#[arg(long)]
pub prefix: PathBuf,
/// logging configuration that we are using.
#[arg(long, long = "log_config")]
pub log_config: PathBuf,
/// module name for us.
#[arg(long)]
pub module: String,
/// MQTT broker socket path
#[arg(long = "mqtt_broker_socket_path")]
pub mqtt_broker_socket_path: Option<PathBuf>,
/// MQTT broker hostname
#[arg(long = "mqtt_broker_host")]
pub mqtt_broker_host: String,
/// MQTT broker port
#[arg(long = "mqtt_broker_port")]
pub mqtt_broker_port: u16,
/// MQTT EVerest prefix
#[arg(long = "mqtt_everest_prefix")]
pub mqtt_everest_prefix: String,
/// MQTT external prefix
#[arg(long = "mqtt_external_prefix")]
pub mqtt_external_prefix: String,
}
/// Implements the handling of commands & variables, but has no specific information about the
/// details of the current module, i.e. it deals with JSON blobs and strings as command names. Code
/// generation is used to build the concrete, strongly typed abstractions that are then used by
/// final implementors.
pub trait Subscriber: Sync + Send {
/// Handler for the command `name` on `implementation_id` with the given `parameters`. The return value
/// will be returned as the result of the call.
fn handle_command(
&self,
implementation_id: &str,
name: &str,
parameters: HashMap<String, serde_json::Value>,
) -> Result<serde_json::Value>;
/// Handler for the variable `name` on `implementation_id` with the given `value`.
fn handle_variable(
&self,
implementation_id: &str,
index: usize,
name: &str,
value: serde_json::Value,
) -> Result<()>;
/// Handler for the error raised/cleared callback
/// The `raised` flag indicates if the error is raised or cleared.
fn handle_on_error(
&self,
implementation_id: &str,
index: usize,
error: ffi::ErrorType,
raised: bool,
);
fn on_ready(&self) {}
}
enum PanicStrategy {
/// Log the panic and abort the process. Appropriate for production modules
/// where a panic in a callback means the module is in a broken state.
Abort,
/// Capture the panic and re-raise it on the main/test thread when the
/// module is dropped. Appropriate for tests where panics (e.g. from
/// mockall assertions) should be forwarded to the test harness.
Capture(std::sync::Mutex<Option<Box<dyn std::any::Any + Send>>>),
}
/// The [Runtime] is the central piece of the bridge between c++ and Rust. Rust
/// owns the [ffi::Module], and the ffi::Module owns the entire mqtt stack. So
/// when dropping, we first drop the ffi::Module and then the
/// Subscribers/Runtime which own the callbacks.
pub struct Runtime {
cpp_module: cxx::UniquePtr<ffi::Module>,
sub_impl: OnceLock<Arc<dyn Subscriber>>,
/// The config for the client module.
config: HashMap<String, HashMap<String, Config>>,
panic_strategy: PanicStrategy,
}
impl Runtime {
/// Handle a panic payload according to the configured strategy.
fn handle_panic(&self, payload: Box<dyn std::any::Any + Send>) {
match &self.panic_strategy {
PanicStrategy::Abort => {
// Extract a message for the log, then abort.
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
log::error!("Panic on MQTT callback thread: {msg}");
std::process::abort();
}
PanicStrategy::Capture(captured) => {
let mut slot = captured.lock().unwrap();
if slot.is_none() {
*slot = Some(payload);
}
}
}
}
/// If a panic was captured on an MQTT callback thread, re-raise it on the
/// current thread. This is intended to be called from `Drop`.
pub fn check_panic(&self) {
if let PanicStrategy::Capture(captured) = &self.panic_strategy {
if let Some(payload) = captured.lock().unwrap().take() {
std::panic::resume_unwind(payload);
}
}
}
fn on_ready(&self) {
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.sub_impl.get().unwrap().on_ready();
})) {
self.handle_panic(payload);
}
}
fn handle_command(&self, impl_id: &str, name: &str, json: ffi::JsonBlob) -> ffi::JsonBlob {
debug!("handle_command: {impl_id}, {name}, '{:?}'", json.as_bytes());
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let parameters: Option<HashMap<String, serde_json::Value>> = json.deserialize();
let retval = self.sub_impl.get().unwrap().handle_command(
impl_id,
name,
parameters.unwrap_or_default(),
);
match retval {
Ok(blob) => ffi::JsonBlob::from_vec(serde_json::to_vec(&blob).unwrap()),
Err(err) => ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap()),
}
}));
match result {
Ok(blob) => blob,
Err(payload) => {
self.handle_panic(payload);
// Return an error response so the C++ side doesn't hang.
// Only reached with PanicStrategy::Capture (Abort never returns).
let err = Error::HandlerException("panic in command handler".into());
ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap())
}
}
}
fn handle_variable(&self, impl_id: &str, index: usize, name: &str, json: ffi::JsonBlob) {
debug!(
"handle_variable: {impl_id}, {name}, '{:?}'",
json.as_bytes()
);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if let Err(err) = self.sub_impl.get().unwrap().handle_variable(
impl_id,
index,
name,
json.deserialize(),
) {
log::error!("`handle_variable` failed: {err:?}");
}
}));
if let Err(payload) = result {
self.handle_panic(payload);
}
}
fn handle_on_error(&self, impl_id: &str, index: usize, error: ffi::ErrorType, raised: bool) {
debug!("handle_on_error: {impl_id}, index {index}, raised {raised}");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.sub_impl
.get()
.unwrap()
.handle_on_error(impl_id, index, error, raised);
}));
if let Err(payload) = result {
self.handle_panic(payload);
}
}
pub fn publish_variable<T: serde::Serialize>(
&self,
impl_id: &str,
var_name: &str,
message: &T,
) {
let blob = ffi::JsonBlob::from_vec(
serde_json::to_vec(&message).expect("Serialization of data cannot fail."),
);
(self.cpp_module).publish_variable(impl_id, var_name, blob);
}
pub fn call_command<T: serde::Serialize, R: serde::de::DeserializeOwned>(
&self,
impl_id: &str,
index: usize,
name: &str,
args: &T,
) -> Result<R> {
let blob = ffi::JsonBlob::from_vec(
serde_json::to_vec(args).expect("Serialization of data cannot fail."),
);
let return_value = (self.cpp_module).call_command(impl_id, index, name, blob);
match serde_json::from_slice(&return_value.data) {
Ok(ok) => Ok(ok),
Err(_) => match serde_json::from_slice::<Error>(&return_value.data) {
Ok(err) => Err(err),
Err(err) => Err(Error::MessageParsingError(format!("{err:?}"))),
},
}
}
/// Called from the generated code.
/// The type T should be an error.
pub fn raise_error<T: serde::Serialize + core::fmt::Debug>(
&self,
impl_id: &str,
error: ErrorType<T>,
) {
let error_string = serde_yaml::to_string(&error.error_type).unwrap_or_default();
// Remove the new line -> this should be gone once we stop using yaml
// since we don't really want yaml.
let error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
debug!("Raising error {error_string:?} from {error:?}");
let error_type = ffi::ErrorType {
error_type: error_string.to_string(),
description: error.description,
message: error.message,
severity: error.severity,
};
self.cpp_module.raise_error(impl_id, error_type);
}
/// Called from the generated code.
/// The type T should be an error.
pub fn clear_error<T: serde::Serialize + core::fmt::Debug>(
&self,
impl_id: &str,
error: T,
clear_all: bool,
) {
let error_string = serde_yaml::to_string(&error).unwrap_or_default();
let mut error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
// The yaml conversion changes empty strings into a string containing two
// single quotes which we catch and convert to an actual empty string
if error_string == "''" {
error_string = "";
}
debug!("Clearing the {error_string} from {error:?}");
self.cpp_module
.clear_error(impl_id, &error_string, clear_all);
}
/// Create a runtime by parsing CLI arguments. Uses [`PanicStrategy::Abort`]
/// since this is the production entry point.
pub fn new() -> Pin<Arc<Self>> {
let args: Args = Args::parse();
Self::create(args, PanicStrategy::Abort)
}
/// Create a runtime with explicit args instead of parsing CLI arguments.
/// Uses [`PanicStrategy::Capture`] since this is used by test harnesses.
pub fn new_with_args(args: Args) -> Pin<Arc<Self>> {
Self::create(args, PanicStrategy::Capture(std::sync::Mutex::new(None)))
}
fn create(args: Args, panic_strategy: PanicStrategy) -> Pin<Arc<Self>> {
logger::Logger::init_logger(
&args.module,
&args.prefix.to_string_lossy(),
&args.log_config.to_string_lossy(),
);
let cpp_module = ffi::create_module(
&args.module,
&args.prefix.to_string_lossy(),
&args
.mqtt_broker_socket_path
.unwrap_or_default()
.to_string_lossy(),
&args.mqtt_broker_host,
&args.mqtt_broker_port,
&args.mqtt_everest_prefix,
&args.mqtt_external_prefix,
);
let raw_config = cpp_module.get_module_configs(&args.module);
// Convert the nested Vec's into nested HashMaps.
let mut config: HashMap<String, HashMap<String, Config>> = HashMap::new();
for mm_config in raw_config {
let cc_config = mm_config
.data
.into_iter()
.map(|field| {
let value = match field.config_type {
ffi::ConfigType::Boolean => Config::Boolean(field.bool_value),
ffi::ConfigType::String => Config::String(field.string_value),
ffi::ConfigType::Number => Config::Number(field.number_value),
ffi::ConfigType::Integer => Config::Integer(field.integer_value),
_ => panic!("Unexpected value {:?}", field.config_type),
};
(field.name, value)
})
.collect::<HashMap<_, _>>();
// If we have already an entry with the `module_name`, we try to extend
// it.
config
.entry(mm_config.module_name)
.or_default()
.extend(cc_config);
}
Arc::pin(Self {
cpp_module,
sub_impl: OnceLock::new(),
config,
panic_strategy,
})
}
pub fn set_subscriber(self: Pin<&Self>, sub_impl: Arc<dyn Subscriber>) {
self.sub_impl
.set(sub_impl)
.unwrap_or_else(|_| panic!("set_subscriber called twice"));
let manifest_json = self.cpp_module.get_manifest();
let manifest: schema::Manifest = manifest_json.deserialize();
log::debug!("Deserialiazed the manifest {manifest:?}");
// Implement all commands for all of our implementations, dispatch everything to the
// Subscriber.
for (implementation_id, provides) in manifest.provides {
let interface_s = self.cpp_module.get_interface(&provides.interface);
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
log::debug!("Deserialiazed the interface {interface:?}");
for (name, _) in interface.cmds {
self.cpp_module
.provide_command(self, implementation_id.clone(), name);
}
}
let connections = self.get_module_connections();
// Subscribe to all variables that might be of interest.
for (implementation_id, requires) in manifest.requires {
let connection = connections.get(&implementation_id).cloned().unwrap_or(0);
let interface_s = self.cpp_module.get_interface(&requires.interface);
// EVerest framework may return null if an interface is not used in
// the config (the connection is then 0).
if interface_s.as_bytes() == b"null" && connection == 0 {
debug!("Skipping the interface {implementation_id}");
continue;
}
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
log::debug!("Deserialiazed the interface {interface:?}");
for i in 0usize..connection {
for (name, _) in interface.vars.iter() {
if requires.ignore.vars.contains(name) {
continue;
}
self.cpp_module.subscribe_variable(
self,
implementation_id.clone(),
i,
name.clone(),
);
}
}
}
self.cpp_module.subscribe_all_errors(self);
// Since users can choose to overwrite `on_ready`, we can call signal_ready right away.
// TODO(hrapp): There were some doubts if this strategy is too inflexible, discuss design
// again.
(self.cpp_module).signal_ready(self);
}
/// The interface for fetching the module connections though the C++ runtime.
pub fn get_module_connections(&self) -> HashMap<String, usize> {
let raw_connections = self.cpp_module.get_module_connections();
raw_connections
.into_iter()
.map(|connection| (connection.implementation_id, connection.slots))
.collect()
}
/// Interface for fetching the configurations through the C++ runtime.
pub fn get_module_configs(&self) -> &HashMap<String, HashMap<String, Config>> {
&self.config
}
}
impl Drop for Runtime {
fn drop(&mut self) {
// Re-raise any panic that was captured on an MQTT callback thread,
// but only if we're not already unwinding from another panic.
if !std::thread::panicking() {
self.check_panic();
}
}
}
/// A store for our config values. The type is closely related to
/// [ffi::ConfigField] and [ffi::ConfigType].
#[derive(Debug)]
pub enum Config {
Boolean(bool),
String(String),
Number(f64),
Integer(i64),
}
impl TryFrom<&Config> for bool {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Boolean(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for String {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::String(value) => Ok(value.clone()),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for f64 {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Number(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}
impl TryFrom<&Config> for i64 {
type Error = Error;
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
match value {
Config::Integer(value) => Ok(*value),
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
}
}
}

View File

@@ -0,0 +1,150 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
use nix::fcntl::{open, OFlag};
use nix::sys::stat;
use nix::unistd;
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
/// Handle to a running EVerest manager process.
///
/// The manager is spawned as a subprocess using `{prefix}/bin/manager`. When
/// dropped, the manager process is killed if it is still running.
pub struct Manager {
child: Arc<Mutex<Child>>,
stopping: Arc<AtomicBool>,
watcher: Option<thread::JoinHandle<()>>,
}
impl Manager {
/// Start the EVerest manager as a subprocess and wait until it is ready.
///
/// * `prefix` — the EVerest sysroot (passed as `--prefix`)
/// * `config` — path to the config file (passed as `--config`)
/// * `standalone` — module IDs that the manager should not spawn
/// (passed as `--standalone`). If non-empty, this function blocks until
/// the manager reports that all managed modules are started and it is
/// waiting for the standalone modules.
/// * `mqtt_everest_prefix` — optional MQTT topic prefix override. When set,
/// passed as `--mqtt_everest_prefix` to the manager, allowing multiple
/// test instances to run in parallel without topic collisions.
pub fn start(
prefix: &Path,
config: &Path,
standalone: &[&str],
mqtt_everest_prefix: Option<&str>,
) -> io::Result<Self> {
let binary = Self::manager_binary(prefix);
let mut cmd = Command::new(&binary);
cmd.arg("--prefix").arg(prefix);
cmd.arg("--config").arg(config);
if let Some(mqtt_prefix) = mqtt_everest_prefix {
cmd.arg("--mqtt_everest_prefix").arg(mqtt_prefix);
}
let mut fifo = None;
if !standalone.is_empty() {
let fifo_name = match mqtt_everest_prefix {
Some(p) => format!("status-{}.fifo", p.replace('/', "_")),
None => format!("status-{}.fifo", std::process::id()),
};
let fifo_path = std::env::temp_dir().join(fifo_name);
unistd::mkfifo(&fifo_path, stat::Mode::S_IRWXU)?;
fifo = Some(open(
&fifo_path,
OFlag::O_RDONLY | OFlag::O_NONBLOCK,
stat::Mode::empty(),
)?);
cmd.arg("--status-fifo").arg(&fifo_path);
cmd.arg("--standalone");
for module_id in standalone {
cmd.arg(module_id);
}
}
let child = Arc::new(Mutex::new(cmd.spawn().map_err(|e| {
io::Error::new(
e.kind(),
format!("Failed to start manager at {}: {e}", binary.display()),
)
})?));
let stopping = Arc::new(AtomicBool::new(false));
let watcher = {
let child = child.clone();
let stopping = stopping.clone();
Some(std::thread::spawn(move || loop {
let status = child.lock().unwrap().try_wait().unwrap();
match status {
None => thread::sleep(Duration::from_millis(100)),
Some(status) => {
if !stopping.load(Ordering::Relaxed) {
panic!("Manager process exited unexpectedly with status: {status}");
}
return;
}
}
}))
};
// Wait for the manager to signal readiness via the status fifo.
if let Some(fd) = fifo {
let mut buf = [0u8; 256];
let mut accumulated = String::new();
loop {
match unistd::read(&fd, &mut buf) {
Ok(n) => {
accumulated.push_str(&String::from_utf8_lossy(&buf[..n]));
if accumulated.contains("WAITING_FOR_STANDALONE_MODULES")
|| accumulated.contains("ALL_MODULES_STARTED")
{
break;
}
}
Err(nix::errno::Errno::EAGAIN) => {
thread::sleep(Duration::from_millis(50));
}
Err(e) => return Err(io::Error::from_raw_os_error(e as i32)),
}
// The watcher thread crashed - we won't see anything in our
// fifo.
if watcher.as_ref().unwrap().is_finished() {
break;
}
}
let _ = unistd::close(fd);
}
Ok(Self {
child,
stopping,
watcher,
})
}
/// Returns the path to the manager binary for a given prefix.
fn manager_binary(prefix: &Path) -> PathBuf {
prefix.join("bin").join("manager")
}
}
impl Drop for Manager {
fn drop(&mut self) {
self.stopping.store(true, Ordering::Relaxed);
let _ = self.child.lock().unwrap().kill();
if let Err(panic) = self.watcher.take().expect("always there").join() {
std::panic::resume_unwind(panic);
}
}
}

View File

@@ -0,0 +1,3 @@
exports_files([
"logging.ini",
])

View File

@@ -0,0 +1,5 @@
filegroup(
name = "errors",
srcs = glob(["*.yaml"]),
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,10 @@
description: Example errors
errors:
- name: ExampleErrorA
description: Example error A
- name: ExampleErrorB
description: Example error B
- name: ExampleErrorC
description: Example error C
- name: ExampleErrorD
description: Example error D

View File

@@ -0,0 +1,10 @@
description: More errors.
errors:
- name: ExampleErrorA
description: >-
The same error as in example_errors.yaml. The error names are placed in a
namespace
- name: MoreError
description: additinal error
- name: snake_case_error
description: We will correct snake cases

View File

@@ -0,0 +1,2 @@
description: No errors.
errors: []

View File

@@ -0,0 +1,5 @@
filegroup(
name = "interfaces",
srcs = glob(["*.yaml"]),
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1 @@
description: An empty interface.

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we allow duplicate errors
errors:
- reference: /errors/example_errors#/ExampleErrorA
- reference: /errors/example_errors

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we respect multiple error definitions
errors:
- reference: /errors/example_errors
- reference: /errors/more_errors

View File

@@ -0,0 +1,4 @@
description: >-
We make sure that we can parse the error files which don't define any errors
errors:
- reference: /errors/no_errors

View File

@@ -0,0 +1,5 @@
description: >-
We make sure that we respect a subset of errors
errors:
- reference: /errors/example_errors#/ExampleErrorA
- reference: /errors/example_errors#/ExampleErrorB

View File

@@ -0,0 +1,20 @@
description: >-
This interface defines an example interface that uses multiple framework
features
cmds:
uses_something:
description: This command checks if something is stored under a given key
arguments:
key:
description: Key to check the existence for
type: string
pattern: ^[A-Za-z0-9_.]+$
result:
description: Returns 'True' if something was stored for this key
type: boolean
vars:
max_current:
description: Provides maximum current of this supply in ampere
type: number
errors:
- reference: /errors/example_errors

View File

@@ -0,0 +1,24 @@
# for documentation on this file format see:
# https://www.boost.org/doc/libs/1_54_0/libs/log/doc/html/log/detailed/utilities.html#log.detailed.utilities.setup.filter_formatter
[Core]
DisableLogging=false
# To get debug logs of only one module, add the "%Process% contains" filter, e.g.:
#
# "(%Process% contains OCPP201 and %Severity% >= DEBG)"
#
# whereas "OCPP201" is the value of the field `active_modules.NAME.module` in the respective /config/config-*.yaml.
Filter="%Severity% >= INFO"
[Sinks.Console]
Destination=Console
# Filter="%Target% contains \"MySink1\""
Format="%TimeStamp% [%Severity%] \033[1;32m%Process%\033[0m \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%"
Asynchronous=false
AutoFlush=true
SeverityStringColorDebug="\033[1;30m"
SeverityStringColorInfo="\033[1;37m"
SeverityStringColorWarning="\033[1;33m"
SeverityStringColorError="\033[1;31m"
SeverityStringColorCritical="\033[1;35m"

View File

@@ -0,0 +1 @@
exports_files(["smoke_test.sh"])

View File

@@ -0,0 +1,96 @@
load("@rules_python//python:defs.bzl", "py_test")
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//applications/utils:requirements.bzl", "requirement")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsAsyncBinary",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
"@everest_framework_crate_index//:tokio",
],
)
rs_everest_module(
name = "RsAsync",
binary = ":RsAsyncBinary",
manifest = "manifest.yaml",
)
everest_env(
name = "config_env",
config_file = "config.yaml",
modules = [
":RsAsync",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":config_env"],
tags = ["exclusive"],
)
# Integration tests for RsAsync. Will launch EVerest with a ProbeModule and
# validate calls to our trait mocks.
rust_test(
name = "RsAsyncMockedTestBinary",
srcs = ["rs_tests/tests.rs"],
compile_data = [
"config.yaml",
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
crate_features = [
"mockall",
"trait",
],
data = ["config_env"],
edition = "2021",
proc_macro_deps = [
"//lib/everest/framework/everestrs/everestrs-derive",
"@everest_framework_crate_index//:mockall_double",
],
rustc_env = {
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsAsync",
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
},
tags = ["exclusive"],
deps = [
"//lib/everest/framework/everestrs/everestrs",
"@everest_framework_crate_index//:mockall",
"@everest_framework_crate_index//:tokio",
],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,14 @@
# Test config for regression tests for the framework.
active_modules:
example_0:
module: RsAsync
connections:
receiver:
- module_id: example_1
implementation_id: sender
example_1:
module: RsAsync
connections:
receiver:
- module_id: example_0
implementation_id: sender

View File

@@ -0,0 +1,14 @@
description: Simple Example
provides:
sender:
interface: example
description: An example interface.
requires:
receiver:
interface: example
ignore:
errors: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors

View File

@@ -0,0 +1,60 @@
#![allow(non_snake_case)]
use std::sync::Arc;
use tokio::sync::oneshot;
#[tokio::test]
#[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
async fn test_tokio_everest(module: &Module) {
use super::*;
use generated::*;
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let (tx, rx) = oneshot::channel();
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.withf(|_, value| *value == 123.0)
.times(1)
.return_once(move |_, _| {
tx.send(()).unwrap();
});
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.await.expect("Timed out waiting for on_max_current");
}
#[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
#[tokio::test]
async fn test_everest_tokio(module: &Module) {
use super::*;
use generated::*;
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let (tx, rx) = oneshot::channel();
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.withf(|_, value| *value == 123.0)
.times(1)
.return_once(move |_, _| {
tx.send(()).unwrap();
});
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.await.expect("Timed out waiting for on_max_current");
}

View File

@@ -0,0 +1,77 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
OnReadySubscriber,
};
use std::sync::{Arc, Mutex};
// Just to use async.
use tokio::sync::oneshot;
use tokio::time::sleep;
pub struct OneClass {
tx: Mutex<Option<oneshot::Sender<String>>>,
}
impl ExampleServiceSubscriber for OneClass {
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
log::info!("Received {key}");
let tx = self.tx.lock().unwrap().take();
if let Some(tx) = tx {
tx.send(key).unwrap();
}
Ok(true)
}
}
impl ExampleClientSubscriber for OneClass {
fn on_max_current(&self, context: &Context, value: f64) {
log::info!("Received {value}");
context
.publisher
.receiver
.uses_something(format!("{value}"))
.unwrap();
}
}
impl OnReadySubscriber for OneClass {
fn on_ready(&self, _publishers: &ModulePublisher) {
log::info!("Ready");
}
}
/// Example how to use async with Everest. Everything in EVerest (all traits)
/// remain strictly sync because of the underlying c++ runtime. However, you can
/// combine your async code with sync EVerest.
///
/// You can combine the `everestrs::main` macro with `tokio::main` macro. The
/// ordering does not really matter, so for non-main function (functions which
/// can receive input args), you can also write
/// ```ignore
/// #[tokio::main]
/// #[everestrs::main]
/// async fn my_fun(module: &Module) {}
/// ```
#[everestrs::main]
#[tokio::main]
async fn main(module: &Module) {
let config = module.get_config();
log::info!("Received the config {config:?}");
let (tx, rx) = oneshot::channel();
let one_class = Arc::new(OneClass {
tx: Mutex::new(Some(tx)),
});
let publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
publishers.sender.max_current(123.).unwrap();
// Simulate some async steps...
let result = rx.await.unwrap();
log::info!("Done {result}");
loop {
sleep(std::time::Duration::from_secs(1)).await;
}
}

View File

@@ -0,0 +1,71 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsCmdErrorsBinary",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rust_test(
name = "RsCmdErrorsTest",
srcs = [],
crate = ":RsCmdErrorsBinary",
crate_features = [
"mockall",
"mockall_double",
],
edition = "2021",
proc_macro_deps = ["@everest_framework_crate_index//:mockall_double"],
deps = ["@everest_framework_crate_index//:mockall"],
)
rs_everest_module(
name = "RsCmdErrors",
binary = ":RsCmdErrorsBinary",
manifest = "manifest.yaml",
)
everest_env(
name = "integration_env",
config_file = "config.yaml",
modules = [
":RsCmdErrors",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":integration_env"],
tags = ["exclusive"],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,16 @@
settings:
# This is the setting we want to test.
forward_exceptions: true
active_modules:
example_0:
module: RsCmdErrors
connections:
other:
- module_id: example_1
implementation_id: example
example_1:
module: RsCmdErrors
connections:
other:
- module_id: example_0
implementation_id: example

View File

@@ -0,0 +1,18 @@
description: Tests if we can raise errors from commands
provides:
example:
interface: example
description: An example interface.
requires:
other:
interface: example
# We don't care about everything which is non-cmd.
ignore:
vars:
- max_current
errors: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,90 @@
//! Integration test for the "cmd-errors" handling.
//!
//! The Rust binding can receive/return errors from command calls. The
//! exceptions need the `forward_exceptions` setting. Then errors from the
//! server should propagate to the client.
//!
//! Below we test all possible errors supported by EVerest. In user code it
//! only `HandlerException` typically makes sense.
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
OnReadySubscriber,
};
use std::sync::{Arc, OnceLock};
use std::{thread, time};
pub struct OneClass {
publisher: OnceLock<ModulePublisher>,
}
impl ExampleServiceSubscriber for OneClass {
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
match key.as_str() {
"MessageParsingError" => Err(::everestrs::Error::MessageParsingError(
"this message?".to_string(),
)),
"SchemaValidationError" => Err(::everestrs::Error::SchemaValidationError(
"not my schema".to_string(),
)),
"HandlerException" => Err(::everestrs::Error::HandlerException(
"my handler".to_string(),
)),
"CmdTimeout" => Err(::everestrs::Error::CmdTimeout("no time".to_string())),
"Shutdown" => Err(::everestrs::Error::Shutdown("dead".to_string())),
"NotReady" => Err(::everestrs::Error::NotReady("too soon".to_string())),
_ => Ok(true),
}
}
}
// The compilation test is that we don't generate the method interfaces for
// the ignored methods.
impl ExampleClientSubscriber for OneClass {}
impl OnReadySubscriber for OneClass {
fn on_ready(&self, publishers: &ModulePublisher) {
let _ = self.publisher.set(publishers.clone());
}
}
#[everestrs::main]
fn main(module: &Module) {
let one_class = Arc::new(OneClass {
publisher: OnceLock::new(),
});
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
log::info!("Module initialized");
let publisher = one_class.publisher.wait();
for (key, _expected) in [
(
"HandlerException",
Err(::everestrs::Error::HandlerException(String::new())),
),
("foo", Ok(true)),
(
"SchemaValidationError",
Err(::everestrs::Error::SchemaValidationError(String::new())),
),
(
"MessageParsingError",
Err(::everestrs::Error::MessageParsingError(String::new())),
),
(
"CmdTimeout",
Err(::everestrs::Error::CmdTimeout(String::new())),
),
("Shutdown", Err(::everestrs::Error::Shutdown(String::new()))),
("NotReady", Err(::everestrs::Error::NotReady(String::new()))),
] {
let res = publisher.other.uses_something(key.to_string());
assert!(matches!(res, _expected));
}
loop {
let dt = time::Duration::from_millis(250);
thread::sleep(dt);
}
}

View File

@@ -0,0 +1,66 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsErrors",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rust_test(
name = "RsErrorsTest",
srcs = [],
crate = ":RsErrors",
edition = "2021",
deps = ["@everest_framework_crate_index//:serde_yaml"],
)
rs_everest_module(
name = "RsErrorsModule",
binary = ":RsErrors",
manifest = "manifest.yaml",
)
everest_env(
name = "integration_env",
config_file = "config.yaml",
modules = [
":RsErrorsModule",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":integration_env"],
tags = ["exclusive"],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,20 @@
# Test config for regression tests for the framework.
active_modules:
errors_0:
module: RsErrorsModule
connections:
errors_multiple:
- module_id: errors_1
implementation_id: multiple
mapping:
module:
evse: 1
errors_1:
module: RsErrorsModule
connections:
errors_multiple:
- module_id: errors_0
implementation_id: multiple
mapping:
module:
evse: 2

View File

@@ -0,0 +1,13 @@
description: Tests for the Errors
requires:
errors_multiple:
interface: errors_multiple
provides:
multiple:
interface: errors_multiple
description: Multiple errors
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,119 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use crate::generated::Context;
use crate::generated::ErrorsMultipleClientSubscriber;
use crate::generated::Module;
use crate::generated::{ModulePublisher, OnReadySubscriber};
use everestrs::{ErrorSeverity, ErrorType};
use generated::errors::errors_multiple::{Error as ExampleError, ExampleErrorsError};
use std::collections::HashSet;
use std::sync::{Arc, Condvar, Mutex};
const MESSAGE: &str = "a message";
const DESCRIPTION: &str = "a description";
const SEVERITY: ErrorSeverity = ErrorSeverity::Low;
struct ErrorCommunacator {
errors_raised: Mutex<HashSet<ExampleErrorsError>>,
errors_cleared: Mutex<HashSet<ExampleErrorsError>>,
errors_cleared_cv: Condvar,
}
impl Eq for ExampleErrorsError {}
impl std::hash::Hash for ExampleErrorsError {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
}
}
impl OnReadySubscriber for ErrorCommunacator {
fn on_ready(&self, publishers: &ModulePublisher) {
let error_a = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
let error_b = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorB);
let error_c = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorC);
publishers.multiple.raise_error(error_a.clone().into());
publishers.multiple.raise_error(error_b.into());
// Raise an error also with description and severity.
let error_c = ErrorType {
error_type: error_c,
description: DESCRIPTION.to_owned(),
message: MESSAGE.to_owned(),
severity: SEVERITY,
};
publishers.multiple.raise_error(error_c);
publishers.multiple.clear_error(error_a);
publishers.multiple.clear_all_errors();
}
}
impl ErrorsMultipleClientSubscriber for ErrorCommunacator {
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
let mut raised_set = self.errors_raised.lock().unwrap();
log::info!("Error raised {:?}", error.error_type);
if let ExampleError::ExampleErrors(inner) = &error.error_type {
raised_set.insert(inner.clone());
}
// Check the handling for custom message, description and severity.
if let ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorC) = error.error_type {
assert_eq!(&error.description, DESCRIPTION);
assert_eq!(&error.message, MESSAGE);
assert_eq!(error.severity, SEVERITY);
}
}
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
let mut cleared_set = self.errors_cleared.lock().unwrap();
log::info!("Error cleared {:?}", error.error_type);
if let ExampleError::ExampleErrors(inner) = error.error_type {
cleared_set.insert(inner.clone());
}
// The integration test links this module to another version of itself
// so the magic 3 here must match the number of calls to raise_error
// (and thus also clear_error through the clear_all_errors call)
// in on_ready
if cleared_set.len() == 3 {
self.errors_cleared_cv.notify_one();
}
}
}
impl crate::generated::ErrorsMultipleServiceSubscriber for ErrorCommunacator {}
#[everestrs::main]
fn main(module: &Module) {
let one_class = Arc::new(ErrorCommunacator {
errors_raised: Mutex::new(HashSet::new()),
errors_cleared: Mutex::new(HashSet::new()),
errors_cleared_cv: Condvar::new(),
});
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
let mut tests_passed = false;
// This mutex goes into the condvar, but it is dummy data as we know that the
// notify_once will fire after this
let mutex = Mutex::new(true);
loop {
let mutex_inner = mutex.lock().unwrap();
let res = one_class
.errors_cleared_cv
.wait_timeout(mutex_inner, std::time::Duration::from_secs(2))
.unwrap();
if res.1.timed_out() & !tests_passed {
panic!("Timeout hit");
} else {
let raised_set = one_class.errors_raised.lock().unwrap();
let cleared_set = one_class.errors_cleared.lock().unwrap();
log::info!("Raised Errors: {:?}", raised_set);
log::info!("Cleared Errors: {:?}", cleared_set);
assert_eq!(*raised_set, *cleared_set);
tests_passed = true;
}
}
}

View File

@@ -0,0 +1,42 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsErrorsCompilation",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rust_test(
name = "RsErrorsCompilationTest",
srcs = [],
crate = ":RsErrorsCompilation",
edition = "2021",
deps = ["@everest_framework_crate_index//:serde_yaml"],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,25 @@
description: Tests for the Errors
requires:
errors_multiple:
interface: errors_multiple
provides:
duplicate:
interface: errors_duplicate
description: Duplicated errors
multiple:
interface: errors_multiple
description: Multiple errors
selected:
interface: errors_selected
description: Selected errors
none:
interface: errors_none
description: No errors defined
empty:
interface: empty
description: An empty interface
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,86 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
fn main() {}
#[cfg(test)]
mod tests {
/// In this test we check that we can deserialize all four errors.
#[test]
fn test_duplicate() {
use crate::generated::errors::errors_duplicate::Error;
use crate::generated::errors::errors_duplicate::ExampleErrorsError;
for (error_str, error_enum) in [
(
"example_errors/ExampleErrorA",
ExampleErrorsError::ExampleErrorA,
),
(
"example_errors/ExampleErrorB",
ExampleErrorsError::ExampleErrorB,
),
(
"example_errors/ExampleErrorC",
ExampleErrorsError::ExampleErrorC,
),
(
"example_errors/ExampleErrorD",
ExampleErrorsError::ExampleErrorD,
),
] {
assert_eq!(
serde_yaml::from_str::<Error>(error_str).unwrap(),
Error::ExampleErrors(error_enum)
);
}
}
/// In this test we check that we can only deserialize the selected two
/// errors.
#[test]
fn test_selected() {
use crate::generated::errors::errors_selected::Error;
use crate::generated::errors::errors_selected::ExampleErrorsError;
for (error_str, error_enum) in [
(
"example_errors/ExampleErrorA",
ExampleErrorsError::ExampleErrorA,
),
(
"example_errors/ExampleErrorB",
ExampleErrorsError::ExampleErrorB,
),
] {
assert_eq!(
serde_yaml::from_str::<Error>(error_str).unwrap(),
Error::ExampleErrors(error_enum)
);
}
for error_str in [
"example_errors/ExampleErrorC",
"example_errors/ExampleErrorD",
] {
assert!(serde_yaml::from_str::<Error>(error_str).is_err());
}
}
/// This test should just compile. The deserialization is tested above.
#[test]
fn test_multiple() {
{
use crate::generated::errors::errors_multiple::ExampleErrorsError;
let _ = ExampleErrorsError::ExampleErrorA;
let _ = ExampleErrorsError::ExampleErrorB;
let _ = ExampleErrorsError::ExampleErrorC;
let _ = ExampleErrorsError::ExampleErrorD;
}
{
use crate::generated::errors::errors_multiple::MoreErrorsError;
let _ = MoreErrorsError::ExampleErrorA;
let _ = MoreErrorsError::MoreError;
let _ = MoreErrorsError::SnakeCaseError;
}
}
}

View File

@@ -0,0 +1,161 @@
load("@rules_python//python:defs.bzl", "py_test")
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//applications/utils:requirements.bzl", "requirement")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env", "everest_test")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsExampleBinary",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rust_test(
name = "RsExampleTest",
srcs = [],
crate = ":RsExampleBinary",
crate_features = [
"mockall",
"mockall_double",
],
edition = "2021",
proc_macro_deps = ["@everest_framework_crate_index//:mockall_double"],
deps = ["@everest_framework_crate_index//:mockall"],
)
rs_everest_module(
name = "RsExample",
binary = ":RsExampleBinary",
manifest = "manifest.yaml",
)
everest_env(
name = "config_env",
config_file = "config.yaml",
modules = [
":RsExample",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":config_env"],
tags = ["exclusive"],
)
everest_env(
name = "config_probe_env",
config_file = "config_probe.yaml",
modules = [
":RsExample",
],
)
# Integration tests for RsExample using Python's everest.testing framework.
# Uses ProbeModule to verify RsExample publishes variables and handles commands.
py_test(
name = "py_mocked_test",
srcs = glob(["py_tests/**/*.py"]),
data = ["config_probe_env"],
legacy_create_init = False,
main = "py_tests/mocked_test.py",
tags = ["exclusive"],
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
deps = [
"//applications/utils/everest-testing",
"//lib/everest/framework/everestpy/src:framework",
requirement("pytest"),
requirement("pytest-asyncio"),
],
)
# Integration tests for RsExample. Will launch EVerest with a ProbeModule and
# verify that we can launch our module together with the ProbeModule.
rust_test(
name = "RsExampleHarnessTestBinary",
srcs = ["rs_tests/harness_test.rs"],
compile_data = [
"config_probe.yaml",
"config_multiple_connections.yaml",
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
data = ["config_probe_env"],
edition = "2021",
proc_macro_deps = [
"//lib/everest/framework/everestrs/everestrs-derive",
],
rustc_env = {
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsExample",
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
},
tags = ["exclusive"],
deps = [
"//lib/everest/framework/everestrs/everestrs",
],
)
# Integration tests for RsExample. Will launch EVerest with a ProbeModule and
# validate calls to our trait mocks.
rust_test(
name = "RsExampleMockedTestBinary",
srcs = ["rs_tests/mocked_test.rs"],
compile_data = [
"config_probe.yaml",
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
crate_features = [
"mockall",
"trait",
],
data = ["config_probe_env"],
edition = "2021",
proc_macro_deps = [
"//lib/everest/framework/everestrs/everestrs-derive",
"@everest_framework_crate_index//:mockall_double",
],
rustc_env = {
"CARGO_MANIFEST_DIR": "lib/everest/framework/everestrs/tests/modules/RsExample",
"EVEREST_CORE_ROOT": "lib/everest/framework/everestrs/tests",
},
tags = ["exclusive"],
deps = [
"//lib/everest/framework/everestrs/everestrs",
"@everest_framework_crate_index//:mockall",
],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,14 @@
# Test config for regression tests for the framework.
active_modules:
example_0:
module: RsExample
connections:
a_friend:
- module_id: example_1
implementation_id: foobar
example_1:
module: RsExample
connections:
a_friend:
- module_id: example_0
implementation_id: foobar

View File

@@ -0,0 +1,21 @@
active_modules:
example_0:
module: RsExample
connections:
a_friend:
- module_id: probe
implementation_id: foobar
example_1:
module: RsExample
connections:
a_friend:
- module_id: probe
implementation_id: foobar
probe:
module: ProbeModule
connections:
a_friend:
- module_id: example_0
implementation_id: foobar
- module_id: example_1
implementation_id: foobar

View File

@@ -0,0 +1,14 @@
# Test config for regression tests for the framework.
active_modules:
example_0:
module: RsExample
connections:
a_friend:
- module_id: example_1
implementation_id: foobar
example_1:
module: ProbeModule
connections:
a_friend:
- module_id: example_0
implementation_id: foobar

View File

@@ -0,0 +1,30 @@
description: Simple Example
config:
some_string_config:
description: A module level string config.
type: string
default: Hello world
some_number_config:
description: A module level number config.
type: number
default: 42
provides:
foobar:
interface: example
description: An example interface.
config:
some_bool_config:
description: An interface level bool config
type: boolean
default: true
some_integer_config:
description: An interface level integer config.
type: integer
default: 1234
requires:
a_friend:
interface: example
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors

View File

@@ -0,0 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
def pytest_addoption(parser):
parser.addoption(
"--everest-prefix",
action="store",
default=".",
help="everest prefix path; default = '.' (for bazel runfiles)",
)

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
"""
Smoke test for RsExample using the Python testing framework.
Launches EVerest with RsExample (example_0) and a ProbeModule (example_1),
then verifies that RsExample publishes max_current(123.0) on ready and
responds to the uses_something command.
"""
import asyncio
import sys
from unittest.mock import Mock
import pytest
from everest.testing.core_utils.common import Requirement
from everest.testing.core_utils.fixtures import *
from everest.testing.core_utils.everest_core import EverestCore
from everest.testing.core_utils.probe_module import ProbeModule
async def wait_for_mock(mock, timeout=10):
"""Poll until mock has been called or timeout."""
for _ in range(int(timeout * 10)):
if mock.called:
return
await asyncio.sleep(0.1)
raise TimeoutError(f"Mock not called within {timeout}s")
@pytest.mark.asyncio
@pytest.mark.probe_module(
connections={"a_friend": [Requirement("example_0", "foobar")]},
module_id="example_1",
)
@pytest.mark.everest_core_config("config_probe.yaml")
async def test_rs_example_publishes_max_current(everest_core: EverestCore):
"""Verify that RsExample publishes max_current(123.0) on ready."""
everest_core.start()
probe = ProbeModule(everest_core.get_runtime_session(), module_id="example_1")
max_current_mock = Mock()
probe.subscribe_variable("a_friend", "max_current", max_current_mock)
probe.implement_command("foobar", "uses_something", lambda args: True)
probe.start()
await probe.wait_to_be_ready(timeout=10)
await wait_for_mock(max_current_mock, timeout=10)
value = max_current_mock.call_args[0][0]
assert value == 123.0, f"Expected max_current=123.0, got {value}"
@pytest.mark.asyncio
@pytest.mark.probe_module(
connections={"a_friend": [Requirement("example_0", "foobar")]},
module_id="example_1",
)
@pytest.mark.everest_core_config("config_probe.yaml")
async def test_rs_example_uses_something(everest_core: EverestCore):
"""Verify that we can call uses_something on RsExample."""
everest_core.start()
probe = ProbeModule(everest_core.get_runtime_session(), module_id="example_1")
probe.implement_command("foobar", "uses_something", lambda args: True)
probe.start()
await probe.wait_to_be_ready(timeout=10)
result = await probe.call_command("a_friend", "uses_something", {"key": "hello"})
# The C++ binding may return None for boolean results; verify at least no exception.
assert result is None or result == True, f"Unexpected result: {result!r}"
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"] + sys.argv[1:]))

View File

@@ -0,0 +1,148 @@
#![allow(non_snake_case)]
#[everestrs::test(config = "config_probe.yaml", module = "example_1", harness = true)]
fn test_harness_generates_counterpart(module: &Module) {
use generated::*;
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
let (tx, rx) = channel();
struct Dummy(Sender<()>);
impl OnReadySubscriber for Dummy {
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
}
impl ExampleServiceSubscriber for Dummy {
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
Ok(false)
}
}
impl ExampleClientSubscriber for Dummy {
fn on_max_current(&self, _context: &Context, value: f64) {
assert_eq!(value, 123.);
self.0.send(()).unwrap();
}
fn on_error_raised(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
fn on_error_cleared(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
}
let dummy = Arc::new(Dummy(tx));
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
#[everestrs::harness(config = "config_probe.yaml", module = "example_1")]
mod some_module {
use generated::*;
use std::sync::mpsc::{channel, Sender};
use std::sync::Arc;
struct Dummy(Sender<()>, f64);
impl OnReadySubscriber for Dummy {
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
}
impl ExampleServiceSubscriber for Dummy {
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
Ok(false)
}
}
impl ExampleClientSubscriber for Dummy {
fn on_max_current(&self, _context: &Context, value: f64) {
assert_eq!(value, self.1);
self.0.send(()).unwrap();
}
fn on_error_raised(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
fn on_error_cleared(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
}
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
fn test_harness_in_module(module: &Module) {
let (tx, rx) = channel();
let dummy = Arc::new(Dummy(tx, 123.));
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
#[should_panic]
fn test_harness_with_panic(module: &Module) {
let (tx, rx) = channel();
let dummy = Arc::new(Dummy(tx, 124.));
let _pub = module.start(dummy.clone(), dummy.clone(), dummy.clone());
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
}
#[everestrs::harness(config = "config_multiple_connections.yaml", module = "probe")]
mod multiple_connections_compilation {
use generated::*;
use std::sync::Arc;
struct Dummy;
impl OnReadySubscriber for Dummy {
fn on_ready(&self, _pub_impl: &ModulePublisher) {}
}
impl ExampleServiceSubscriber for Dummy {
fn uses_something(&self, _context: &Context, _key: String) -> everestrs::Result<bool> {
Ok(false)
}
}
impl ExampleClientSubscriber for Dummy {
fn on_max_current(&self, _context: &Context, _value: f64) {}
fn on_error_raised(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
fn on_error_cleared(
&self,
_context: &Context,
_error: everestrs::ErrorType<errors::example::Error>,
) {
}
}
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
fn test_harness_in_module(module: &Module) {
let dummy = Arc::new(Dummy);
let _pub = module.start(dummy.clone(), dummy.clone(), |_a: usize| dummy.clone());
}
}

View File

@@ -0,0 +1,104 @@
#![allow(non_snake_case)]
#[everestrs::test(config = "config_probe.yaml", module = "example_1", harness = true)]
fn test_mocked_generates_counterpart(module: &Module) {
use generated::*;
use std::sync::Arc;
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let (tx, rx) = std::sync::mpsc::channel();
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.withf(|_, value| *value == 123.0)
.times(1)
.return_once(move |_, _| {
tx.send(()).unwrap();
});
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
#[everestrs::harness(config = "config_probe.yaml", module = "example_1")]
mod some_module {
use generated::*;
use std::sync::Arc;
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
fn test_mocked_in_module(module: &Module) {
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let (tx, rx) = std::sync::mpsc::channel();
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.withf(|_, value| *value == 123.0)
.times(1)
.return_once(move |_, _| {
tx.send(()).unwrap();
});
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
#[should_panic]
fn test_mocked_with_panic_handler(module: &Module) {
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let (tx, rx) = std::sync::mpsc::channel();
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.withf(|_, value| *value != 123.0)
.times(1)
.return_once(move |_, _| {
tx.send(()).unwrap();
});
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
// Wait for RsExample's on_ready to publish max_current(123.0).
rx.recv_timeout(std::time::Duration::from_secs(5))
.expect("Timed out waiting for on_max_current");
}
#[everestrs::test(config = "config_probe.yaml", module = "example_1")]
#[should_panic]
fn test_mocked_with_panic_mocks(module: &Module) {
let mock_service = Arc::new(MockExampleServiceSubscriber::new());
let mut mock_client = MockExampleClientSubscriber::new();
mock_client
.expect_on_max_current()
.times(2..) // No one will call us twice.
.return_const(());
let mock_client = Arc::new(mock_client);
let mut mock_on_ready = MockOnReadySubscriber::new();
mock_on_ready.expect_on_ready().times(1).return_once(|_| ());
let _pub = module.start(Arc::new(mock_on_ready), mock_service, mock_client);
}
}

View File

@@ -0,0 +1,128 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use everestrs::ErrorType;
use generated::errors::example::Error as ExampleError;
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module,
ModulePublisher, OnReadySubscriber,
};
use std::sync::Arc;
use std::{thread, time};
pub struct OneClass {}
impl ExampleServiceSubscriber for OneClass {
fn uses_something(&self, context: &Context, key: String) -> ::everestrs::Result<bool> {
use crate::generated::errors::example::ExampleErrorsError;
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
if key.is_empty() {
// Explicit cast
let error: ErrorType<_> = error.into();
context.publisher.foobar.raise_error(error);
} else if &key == "clear_all" {
context.publisher.foobar.clear_all_errors();
} else {
context.publisher.foobar.clear_error(error);
}
Ok(true)
}
}
impl ExampleClientSubscriber for OneClass {
fn on_max_current(&self, _context: &Context, value: f64) {
log::info!("Received {value}");
}
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
log::warn!("Recieved an error {:?}", error.error_type);
}
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
log::info!("Cleared an error {:?} - what a relief", error.error_type);
}
}
impl OnReadySubscriber for OneClass {
fn on_ready(&self, publishers: &ModulePublisher) {
log::info!("Ready");
match publishers.foobar.max_current(123.0) {
Ok(_) => log::info!("Adjusted the max current"),
Err(err) => log::error!("Failed to set the max current: {err:?}"),
}
}
}
#[everestrs::main]
fn main(module: &Module) {
let config = module.get_config();
log::info!("Received the config {config:?}");
let one_class = Arc::new(OneClass {});
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
log::info!("Module initialized");
loop {
let dt = time::Duration::from_millis(250);
thread::sleep(dt);
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_on_ready() {
let mut everest_mock = ModulePublisher::default();
everest_mock
.foobar
.expect_max_current()
.times(1)
.return_once(|_| Ok(()));
let module = OneClass {};
module.on_ready(&everest_mock);
}
#[test]
fn test_uses_something() {
use mockall::Sequence;
let mut seq = Sequence::new();
let mut everest_mock = ModulePublisher::default();
everest_mock
.foobar
.expect_raise_error()
.times(1)
.in_sequence(&mut seq)
.return_once(|_| ());
everest_mock
.foobar
.expect_clear_error()
.times(1)
.in_sequence(&mut seq)
.return_once(|_| ());
everest_mock
.foobar
.expect_clear_all_errors()
.times(1)
.in_sequence(&mut seq)
.return_once(|| ());
let context = Context {
name: "foo",
publisher: &everest_mock,
index: 0,
};
let module = OneClass {};
for message in [String::new(), "clear".to_owned(), "clear_all".to_owned()] {
let _ = module.uses_something(&context, message);
}
}
}

View File

@@ -0,0 +1,58 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsIgnoreBinary",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rs_everest_module(
name = "RsIgnore",
binary = ":RsIgnoreBinary",
manifest = "manifest.yaml",
)
everest_env(
name = "integration_env",
config_file = "config.yaml",
modules = [
":RsIgnore",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":integration_env"],
tags = ["exclusive"],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,14 @@
# Test config for regression tests for the framework.
active_modules:
example_0:
module: RsIgnore
connections:
other:
- module_id: example_1
implementation_id: example
example_1:
module: RsIgnore
connections:
other:
- module_id: example_0
implementation_id: example

View File

@@ -0,0 +1,17 @@
description: Simple Example
provides:
example:
interface: example
description: An example interface.
requires:
other:
interface: example
ignore:
vars:
- max_current
errors: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,50 @@
//! Integration test for the "ignore" handling.
//!
//! The Rust binding allow you to ignore variables and errors. The variables
//! are ignored by adding them to the "ignore.vars" list. Errors can only be ignored
//! at bulk, by setting "ignore.errors" to true. Ignored elements are removed
//! from the trait and thus don't have to be implemented.
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use generated::errors::example::{Error as ExampleError, ExampleErrorsError};
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
OnReadySubscriber,
};
use std::sync::Arc;
use std::{thread, time};
pub struct OneClass {}
impl ExampleServiceSubscriber for OneClass {
fn uses_something(&self, _context: &Context, _key: String) -> ::everestrs::Result<bool> {
Ok(true)
}
}
// The compilation test is that we don't generate the method interfaces for
// the ignored methods.
impl ExampleClientSubscriber for OneClass {}
impl OnReadySubscriber for OneClass {
fn on_ready(&self, publishers: &ModulePublisher) {
// Call the other module. This calls should be ignored.
publishers.example.max_current(12.3).unwrap();
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
publishers.example.raise_error(error.clone().into());
publishers.example.clear_error(error);
}
}
#[everestrs::main]
fn main(module: &Module) {
let one_class = Arc::new(OneClass {});
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
log::info!("Module initialized");
loop {
let dt = time::Duration::from_millis(250);
thread::sleep(dt);
}
}

View File

@@ -0,0 +1,58 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//lib/everest/framework/bazel:everest_env.bzl", "everest_env")
load("//lib/everest/framework/bazel:modules_def.bzl", "rs_everest_module")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
rust_binary(
name = "RsOnReadyRaceConditionBinary",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)
rs_everest_module(
name = "RsOnReadyRaceCondition",
binary = ":RsOnReadyRaceConditionBinary",
manifest = "manifest.yaml",
)
everest_env(
name = "integration_env",
config_file = "config.yaml",
modules = [
":RsOnReadyRaceCondition",
],
)
sh_test(
name = "integration_test",
srcs = ["//lib/everest/framework/everestrs/tests/modules:smoke_test.sh"],
data = [":integration_env"],
tags = ["exclusive"],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,14 @@
# Test config for regression tests for the framework.
active_modules:
example_0:
module: RsOnReadyRaceCondition
connections:
other:
- module_id: example_1
implementation_id: example
example_1:
module: RsOnReadyRaceCondition
connections:
other:
- module_id: example_0
implementation_id: example

View File

@@ -0,0 +1,13 @@
description: Simple Example
provides:
example:
interface: example
description: An example interface.
requires:
other:
interface: example
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,82 @@
//! Integration test for the "ready_received" handling.
//!
//! We assume that every module shall recieve first `on_ready` before forwarding
//! any other call to the user code. The code below recreates the race condition
//! by making calls to the `other` module from within `on_ready` (and adding a
//! delay in `on_ready`).
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use everestrs::ErrorType;
use generated::errors::example::{Error as ExampleError, ExampleErrorsError};
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
OnReadySubscriber,
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::{thread, time};
pub struct OneClass {
/// Flag that the on-ready has been called.
on_ready_called: AtomicBool,
}
impl ExampleServiceSubscriber for OneClass {
fn uses_something(&self, _context: &Context, _key: String) -> ::everestrs::Result<bool> {
assert!(self.on_ready_called.load(Ordering::Relaxed));
Ok(true)
}
}
impl ExampleClientSubscriber for OneClass {
fn on_max_current(&self, _context: &Context, _value: f64) {
assert!(self.on_ready_called.load(Ordering::Relaxed));
log::info!("max current");
}
fn on_error_raised(&self, _context: &Context, _error: ErrorType<ExampleError>) {
assert!(self.on_ready_called.load(Ordering::Relaxed));
log::info!("Error raised");
}
fn on_error_cleared(&self, _context: &Context, _error: ErrorType<ExampleError>) {
assert!(self.on_ready_called.load(Ordering::Relaxed));
log::info!("Error cleared");
}
}
impl OnReadySubscriber for OneClass {
fn on_ready(&self, publishers: &ModulePublisher) {
log::info!("Enter Ready");
// Call the other module.
publishers.example.max_current(12.3).unwrap();
let error = ExampleError::ExampleErrors(ExampleErrorsError::ExampleErrorA);
publishers.example.raise_error(error.clone().into());
publishers.example.clear_error(error);
// TODO(ddo) Add here the `uses_something` call once the framework can
// reject too early calls.
// Sleep here to trigger the race condition.
std::thread::sleep(std::time::Duration::from_secs(1));
// Update the flag.
self.on_ready_called.store(true, Ordering::Relaxed);
log::info!("Exit Ready!");
}
}
#[everestrs::main]
fn main(module: &Module) {
let one_class = Arc::new(OneClass {
on_ready_called: AtomicBool::new(false),
});
let _publishers = module.start(one_class.clone(), one_class.clone(), one_class.clone());
log::info!("Module initialized");
loop {
let dt = time::Duration::from_millis(250);
thread::sleep(dt);
}
}

View File

@@ -0,0 +1,35 @@
load("@rules_rust//cargo:defs.bzl", "cargo_build_script")
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test")
cargo_build_script(
name = "build_script",
srcs = ["build.rs"],
build_script_env = {
"EVEREST_CORE_ROOT": "../..",
},
data = [
"manifest.yaml",
"//lib/everest/framework/everestrs/tests/errors",
"//lib/everest/framework/everestrs/tests/interfaces",
"//lib/everest/framework/everestrs/tests/types",
],
edition = "2021",
deps = [
"//lib/everest/framework/everestrs/everestrs-build",
],
)
# For now just a compilation test
rust_binary(
name = "RsOptionalConnection",
srcs = glob(["src/**/*.rs"]),
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":build_script",
"//lib/everest/framework/everestrs/everestrs",
"//lib/everest/framework/everestrs/everestrs:everestrs_bridge",
"//lib/everest/framework/everestrs/everestrs:everestrs_sys",
"@everest_framework_crate_index//:log",
],
)

View File

@@ -0,0 +1,13 @@
use everestrs_build::Builder;
pub fn main() {
Builder::new(
"manifest.yaml",
vec![std::env::var("EVEREST_CORE_ROOT").unwrap_or("../../..".to_string())],
)
.generate()
.unwrap();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=manifest.yaml");
}

View File

@@ -0,0 +1,15 @@
description: The tests how to use optinal connections in Rust
provides:
foobar:
interface: example
description: An example interface.
requires:
optional_connection:
interface: example
min_connections: 0
max_connections: 1
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Everest authors
enable_external_mqtt: false

View File

@@ -0,0 +1,58 @@
#![allow(non_snake_case)]
include!(concat!(env!("OUT_DIR"), "/generated.rs"));
use everestrs::ErrorType;
use generated::errors::example::Error as ExampleError;
use generated::{
Context, ExampleClientSubscriber, ExampleServiceSubscriber, Module, ModulePublisher,
OnReadySubscriber,
};
use std::sync::Arc;
use std::{thread, time};
pub struct OptionalConnection {}
impl ExampleServiceSubscriber for OptionalConnection {
fn uses_something(&self, _context: &Context, key: String) -> ::everestrs::Result<bool> {
log::info!("Received {key}");
Ok(&key == "hello")
}
}
impl ExampleClientSubscriber for OptionalConnection {
fn on_max_current(&self, _context: &Context, value: f64) {
log::info!("Received {value}");
}
fn on_error_raised(&self, _context: &Context, error: ErrorType<ExampleError>) {
log::info!("Recieved an error {:?}", error.error_type);
}
fn on_error_cleared(&self, _context: &Context, error: ErrorType<ExampleError>) {
log::info!("Cleared an error {:?} - what a relief", error.error_type);
}
}
impl OnReadySubscriber for OptionalConnection {
fn on_ready(&self, publishers: &ModulePublisher) {
log::info!("Ready");
if let Some(publisher) = publishers.optional_connection_slots.get(0) {
let res = publisher.uses_something("hello".to_string()).unwrap();
assert!(res);
}
}
}
#[everestrs::main]
fn main(module: &Module) {
let one_class = Arc::new(OptionalConnection {});
let _publishers = module.start(one_class.clone(), one_class.clone(), |_index| {
one_class.clone()
});
log::info!("Module initialized");
loop {
let dt = time::Duration::from_millis(250);
thread::sleep(dt);
}
}

View File

@@ -0,0 +1,18 @@
#!/bin/sh
set -ex
echo "Starting the manager"
bin/manager --prefix . --config etc/everest/config.yaml &
PID_MANAGER=$!
sleep 5
echo "Exit"
if ps -p $PID_MANAGER > /dev/null
then
kill $PID_MANAGER
else
echo "manager died"
exit 1
fi

View File

@@ -0,0 +1,5 @@
filegroup(
name = "types",
srcs = glob(["*.yaml"]),
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,5 @@
description: Example type
types:
Something:
description: Some simple type.
type: string