Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,19 @@ jobs:
run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C /usr/local/bin

- name: Build
run: cargo build
run: |
cargo build
cargo build --all-features

- name: Test
run: cargo nextest run
run: |
cargo nextest run
cargo nextest run --all-features

- name: Cargo doctests
run: |
cargo test --doc
cargo test --doc --all-features

- name: Clippy
uses: auguwu/clippy-action@94a9ff2f6920180b89e5c03d121d0af04a9d3e03 # 1.4.0
Expand All @@ -44,6 +53,3 @@ jobs:

- name: Cargo fmt
run: cargo fmt --check

- name: Cargo doctests
run: cargo test --doc
15 changes: 15 additions & 0 deletions .zed/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {
"rust-analyzer": {
"initialization_options": {
"cargo": {
"features": ["antithesis", "enabled"]
}
}
}
}
}
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ rustc_version_runtime = "0.3"
libloading = { version = "0.9", optional = true }

[features]
# disable all runtime overhead of this crate
disabled = []
# enable all features in this crate
# if this feature is not specified, this crate has 0 runtime overhead
enabled = []

# enable the antithesis dispatcher
antithesis = ["libloading"]
11 changes: 4 additions & 7 deletions src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ use std::{
use crate::dispatch::{self, Event, dispatcher};

/// Catalog of all antithesis assertions provided
#[cfg(not(feature = "disabled"))]
#[cfg(feature = "enabled")]
#[linkme::distributed_slice]
pub static PRECEPT_CATALOG: [CatalogEntry];

#[cfg(feature = "disabled")]
#[cfg(not(feature = "enabled"))]
pub static PRECEPT_CATALOG: [&CatalogEntry; 0] = [];

pub fn init_catalog() {
pub(crate) fn init_catalog() {
let dispatch = dispatcher();
for entry in PRECEPT_CATALOG {
dispatch.emit(Event::RegisterEntry(entry));
Expand Down Expand Up @@ -48,7 +48,7 @@ impl Expectation {
pub struct CatalogEntry {
// the type of this expectation
expectation: Expectation,
// the name of the entry, also serves as it's id
// the name of the entry, also serves as its id
property: &'static str,
// panic::Location::caller()
location: &'static Location<'static>,
Expand All @@ -64,7 +64,6 @@ pub struct CatalogEntry {
}

impl CatalogEntry {
#[inline]
pub const fn new(
expectation: Expectation,
property: &'static str,
Expand Down Expand Up @@ -120,12 +119,10 @@ impl CatalogEntry {
self.function
}

#[inline]
pub fn pass_count(&self) -> usize {
self.pass_count.load(atomic::Ordering::Acquire)
}

#[inline]
pub fn fail_count(&self) -> usize {
self.fail_count.load(atomic::Ordering::Acquire)
}
Expand Down
30 changes: 28 additions & 2 deletions src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,47 @@ pub mod test;
#[cfg(feature = "antithesis")]
pub mod antithesis;

/// Events that can be emitted through the dispatcher.
pub enum Event {
/// Registers a new catalog entry with the dispatcher.
RegisterEntry(&'static CatalogEntry),
/// Emits an assertion evaluation result.
EmitEntry {
/// The catalog entry being evaluated.
entry: &'static CatalogEntry,
/// Whether the assertion condition passed.
condition: bool,
/// Additional context about the assertion.
details: serde_json::Value,
},
/// Signals that application setup is complete.
SetupComplete {
/// Additional context about the setup.
details: serde_json::Value,
},
/// A custom user-defined event.
Custom {
/// The event name.
name: &'static str,
/// The event payload.
value: serde_json::Value,
},
}

/// Trait for event dispatchers that handle precept events and random number generation.
///
/// Implementors receive events from precept assertions and provide random numbers
/// for fault injection decisions.
pub trait Dispatch: Sync + Send {
/// Handles an incoming event.
fn emit(&self, event: Event);
/// Returns a random u64 value for decision making.
fn random(&self) -> u64;
}

static DISPATCHER: OnceLock<&'static dyn Dispatch> = OnceLock::new();

/// Error returned when attempting to set a dispatcher that has already been set.
#[derive(Debug)]
pub struct SetDispatchError;

Expand Down Expand Up @@ -64,13 +82,21 @@ pub fn dispatcher() -> &'static dyn Dispatch {
}

/// Generate a random u64 using the dispatcher
#[inline]
pub fn get_random() -> u64 {
dispatcher().random()
}

/// Choose a random value from a slice of options using the dispatcher
pub fn choose<T>(options: &[T]) -> Option<&T> {
if options.is_empty() {
None
} else {
let idx: usize = (get_random() as usize) % options.len();
Some(&options[idx])
}
}

/// Emit an event using the dispatcher
#[inline]
pub fn emit(event: Event) {
dispatcher().emit(event);
}
5 changes: 1 addition & 4 deletions src/dispatch/antithesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ impl Dispatch for AntithesisDispatch {
}
}

#[inline]
fn random(&self) -> u64 {
match self {
Self::Voidstar(handler) => handler.random(),
Expand Down Expand Up @@ -162,7 +161,7 @@ pub struct LibVoidstarHandler {
impl LibVoidstarHandler {
fn try_load() -> Result<Self, libloading::Error> {
// SAFETY:
// - The `libvoidstar` library must not have initalization procedures.
// - The `libvoidstar` library must not have initialization procedures.
// - The `libvoidstar` library must export symbols with the expected type signatures.
unsafe {
let lib = Library::new("/usr/lib/libvoidstar.so")?;
Expand All @@ -189,7 +188,6 @@ impl LibVoidstarHandler {
}
}

#[inline]
fn random(&self) -> u64 {
(self.fuzz_get_random)()
}
Expand Down Expand Up @@ -230,7 +228,6 @@ impl FileHandler {
Ok(())
}

#[inline]
fn random(&self) -> u64 {
rand::random()
}
Expand Down
1 change: 0 additions & 1 deletion src/dispatch/noop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ impl NoopDispatch {
impl Dispatch for NoopDispatch {
fn emit(&self, _event: Event) {}

#[inline]
fn random(&self) -> u64 {
rand::random()
}
Expand Down
1 change: 0 additions & 1 deletion src/dispatch/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ impl Dispatch for TestDispatch {
}
}

#[inline]
fn random(&self) -> u64 {
rand::random()
}
Expand Down
121 changes: 121 additions & 0 deletions src/fault.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::{
collections::HashSet,
fmt::Debug,
sync::atomic::{AtomicBool, AtomicU32, Ordering},
};

use crate::ENABLED;

#[cfg(feature = "enabled")]
#[doc(hidden)]
#[linkme::distributed_slice]
pub static FAULT_CATALOG: [FaultEntry];

#[cfg(not(feature = "enabled"))]
#[doc(hidden)]
pub static FAULT_CATALOG: [&FaultEntry; 0] = [];

pub(crate) fn init_faults() {
let mut seen = HashSet::new();
for entry in FAULT_CATALOG {
// fail if we have already seen this entry
if !seen.insert(entry.name) {
panic!("Duplicate Precept fault: {}", entry.name);
}
}
}

/// A fault injection point that can be triggered during testing.
///
/// Faults can be enabled/disabled and can be forced to trigger a specific
/// number of times using the pending trips mechanism.
#[derive(Debug)]
pub struct FaultEntry {
/// the name of the fault, also serves as its Catalog id
name: &'static str,

/// whether or not this fault is enabled
enabled: AtomicBool,

/// if this value is > 0, the next call to `trip` will return true and this
/// value will be decremented
pending_trips: AtomicU32,
}

impl FaultEntry {
/// Creates a new fault entry with the given name.
pub const fn new(name: &'static str) -> Self {
Self {
name,
enabled: AtomicBool::new(true),
pending_trips: AtomicU32::new(0),
}
}

/// Returns true when the fault should trip
pub fn trip(&self) -> bool {
if self
.pending_trips
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| {
if count > 0 { Some(count - 1) } else { None }
})
.is_ok()
{
// forced trigger
true
} else if self.enabled.load(Ordering::Acquire) {
let should_fault = crate::dispatch::choose(&[true, false]);
should_fault.is_some_and(|&t| t)
} else {
false
}
}

/// Enables this fault, allowing it to trip.
pub fn enable(&self) {
self.enabled.store(true, Ordering::Release);
}

/// Disables this fault, preventing it from tripping.
pub fn disable(&self) {
self.enabled.store(false, Ordering::Release);
}

/// Sets the number of pending forced trips.
///
/// When pending trips are set, the next `count` calls to [`trip`](Self::trip)
/// will return `true` regardless of random chance.
pub fn set_pending(&self, count: u32) {
self.pending_trips.store(count, Ordering::Release);
}

/// Returns the number of pending forced trips remaining.
pub fn count_pending(&self) -> u32 {
self.pending_trips.load(Ordering::Acquire)
}
}

/// Enables all registered faults.
///
/// Panics if precept is disabled.
pub fn enable_all() {
assert!(ENABLED, "Precept is disabled");
for entry in FAULT_CATALOG {
entry.enable()
}
}

/// Disables all registered faults.
pub fn disable_all() {
tracing::warn!("Precept Faults disabled");
for entry in FAULT_CATALOG {
entry.disable();
}
}

/// Looks up a fault entry by its name.
///
/// Returns `None` if no fault with the given name exists.
pub fn get_fault_by_name(name: &str) -> Option<&'static FaultEntry> {
FAULT_CATALOG.into_iter().find(|&entry| entry.name == name)
}
Loading