chore: initial release v0.1.0

Runtime-agnostic oneshot broadcast channel for Rust.
This commit is contained in:
swaits committed 2025-10-13 19:36:37 -06:00
commit e8e55b7676
64 files changed
+23966

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "announcement-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.announcement]
path = ".."
[[bin]]
name = "fuzz_announce"
path = "fuzz_targets/fuzz_announce.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_close"
path = "fuzz_targets/fuzz_close.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_listen"
path = "fuzz_targets/fuzz_listen.rs"
test = false
doc = false
bench = false
[[bin]]
name = "fuzz_concurrent"
path = "fuzz_targets/fuzz_concurrent.rs"
test = false
doc = false
bench = false
+182
View File
@@ -0,0 +1,182 @@
# Fuzz Testing for Announcement
This directory contains fuzz testing infrastructure using [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) and libFuzzer.
## Overview
We have 4 fuzz targets that test different aspects of the `announcement` library:
1. **fuzz_announce** - Tests the announce operation with various data types and patterns
2. **fuzz_close** - Tests close/drop scenarios and the closed detection API
3. **fuzz_listen** - Tests listener operations (try_listen, is_announced, etc.)
4. **fuzz_concurrent** - Tests concurrent access patterns with multiple threads
## Prerequisites
Fuzzing requires nightly Rust and cargo-fuzz:
```bash
rustup install nightly
cargo install cargo-fuzz
```
## Running Fuzz Tests
Run a single fuzz target:
```bash
cargo +nightly fuzz run fuzz_announce
```
Run with a time limit (recommended for CI):
```bash
cargo +nightly fuzz run fuzz_announce -- -max_total_time=60
```
Run with a specific number of iterations:
```bash
cargo +nightly fuzz run fuzz_announce -- -runs=10000
```
Run all fuzz targets sequentially:
```bash
for target in fuzz_announce fuzz_close fuzz_listen fuzz_concurrent; do
echo "Running $target..."
cargo +nightly fuzz run $target -- -runs=10000
done
```
## Continuous Fuzzing
For deeper testing, run fuzz targets for extended periods:
```bash
# Run overnight (8 hours)
cargo +nightly fuzz run fuzz_concurrent -- -max_total_time=28800
# Run until crash (or Ctrl+C)
cargo +nightly fuzz run fuzz_announce
```
## Understanding Output
Fuzzer output includes:
- `cov: X` - Code coverage (number of edges covered)
- `ft: X` - Feature coverage (number of unique code paths)
- `corp: X/Yb` - Corpus size (X inputs, Y bytes total)
- `exec/s: X` - Executions per second
If a crash is found:
- Crash input is saved to `artifacts/FUZZ_TARGET/crash-*`
- Reproduce with: `cargo +nightly fuzz run FUZZ_TARGET artifacts/FUZZ_TARGET/crash-*`
## Fuzz Target Details
### fuzz_announce
Tests basic announce operations:
- Various data types (Vec<u8>, String, u64, Arc<T>)
- Multiple listeners receiving the same data
- is_announced() flag behavior
- Data integrity verification
### fuzz_close
Tests closed detection API:
- Drop without announce
- Explicit close() call
- Announce then drop (should not be closed)
- Multiple listeners with close
- Listener created after close
### fuzz_listen
Tests listener operations:
- try_listen() before and after announce
- Multiple try_listen() calls
- Multiple listeners
- Clone and listener creation
- is_announced() vs is_closed() states
### fuzz_concurrent
Tests thread safety:
- Multiple threads with concurrent listeners
- Concurrent clone and listener creation
- Concurrent is_announced() checks
- Concurrent is_closed() checks
- Various timing patterns with barriers
## Adding New Fuzz Targets
1. Create a new file in `fuzz_targets/` (e.g., `fuzz_new_feature.rs`)
2. Use the template:
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
use announcement::Announcement;
fuzz_target!(|data: &[u8]| {
// Your fuzz test here
});
```
3. Add to `fuzz/Cargo.toml`:
```toml
[[bin]]
name = "fuzz_new_feature"
path = "fuzz_targets/fuzz_new_feature.rs"
test = false
doc = false
bench = false
```
4. Build and run:
```bash
cargo +nightly fuzz build
cargo +nightly fuzz run fuzz_new_feature
```
## CI Integration
Fuzzing runs automatically in CI on every push/PR:
- Each target runs for 60-120 seconds
- Targets run 1,000-10,000 iterations minimum
- Any crashes fail the build
See `.github/workflows/ci.yml` for details.
## Corpus Management
The fuzzer builds a corpus of interesting inputs in `corpus/FUZZ_TARGET/`. These inputs:
- Are automatically discovered during fuzzing
- Can be committed to Git for regression testing
- Help future fuzzing sessions start with good coverage
To minimize the corpus:
```bash
cargo +nightly fuzz cmin fuzz_announce
```
## Tips
1. **Start with short runs** - Use `-max_total_time=60` to catch obvious bugs quickly
2. **Run overnight** - For thorough testing, run for hours or days
3. **Check coverage** - Monitor `cov:` values to ensure good code coverage
4. **Save crashes** - Commit crash artifacts as regression tests
5. **Update regularly** - Re-run fuzzing when adding new features
## References
- [cargo-fuzz book](https://rust-fuzz.github.io/book/cargo-fuzz.html)
- [libFuzzer documentation](https://llvm.org/docs/LibFuzzer.html)
- [Fuzzing in Rust](https://rust-fuzz.github.io/book/)
+77
View File
@@ -0,0 +1,77 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use announcement::Announcement;
use std::sync::Arc;
fuzz_target!(|data: &[u8]| {
// Test various data types and patterns
// Test with raw bytes
if !data.is_empty() {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
// Convert to Vec for owned data
let vec_data = data.to_vec();
let _ = announcer.announce(vec_data.clone());
// Verify listener receives the data
if let Some(received) = listener.try_listen() {
assert_eq!(received, vec_data);
}
}
// Test with Arc-wrapped data
if data.len() >= 4 {
let (announcer, announcement) = Announcement::new();
let listener1 = announcement.listener();
let listener2 = announcement.listener();
let arc_data = Arc::new(data.to_vec());
let _ = announcer.announce(arc_data.clone());
// Both listeners should get the same Arc
if let (Some(r1), Some(r2)) = (listener1.try_listen(), listener2.try_listen()) {
assert_eq!(r1, r2);
assert_eq!(*r1, data.to_vec());
}
}
// Test with String data (if valid UTF-8)
if let Ok(string_data) = std::str::from_utf8(data) {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let _ = announcer.announce(string_data.to_string());
if let Some(received) = listener.try_listen() {
assert_eq!(received, string_data);
}
}
// Test with numeric data
if data.len() >= 8 {
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&data[..8]);
let num = u64::from_le_bytes(bytes);
let _ = announcer.announce(num);
if let Some(received) = listener.try_listen() {
assert_eq!(received, num);
}
}
// Test is_announced flag
{
let (announcer, announcement) = Announcement::new();
assert!(!announcement.is_announced());
let _ = announcer.announce(42u32);
assert!(announcement.is_announced());
}
});
+84
View File
@@ -0,0 +1,84 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use announcement::Announcement;
fuzz_target!(|data: &[u8]| {
if data.is_empty() {
return;
}
let action = data[0] % 4;
match action {
0 => {
// Test drop without announce
let (announcer, announcement) = Announcement::<u32>::new();
let listener = announcement.listener();
assert!(!announcement.is_closed());
drop(announcer);
// Should be closed now
assert!(announcement.is_closed());
assert!(listener.is_closed());
}
1 => {
// Test explicit close
let (announcer, announcement) = Announcement::<u32>::new();
let listener = announcement.listener();
assert!(!announcement.is_closed());
announcer.close();
// Should be closed now
assert!(announcement.is_closed());
assert!(listener.is_closed());
}
2 => {
// Test announce then drop (should not be closed)
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let _ = announcer.announce(42u32);
// announcer is consumed by announce(), no need to drop
// Should NOT be closed (announced successfully)
assert!(!announcement.is_closed());
assert!(!listener.is_closed());
// Listener should get value
assert_eq!(listener.try_listen(), Some(42u32));
}
_ => {
// Test listener created after close
let (announcer, announcement) = Announcement::<u32>::new();
announcer.close();
// Create listener after close
let listener = announcement.listener();
// Should immediately know it's closed
assert!(listener.is_closed());
}
}
// Test with multiple listeners
if data.len() >= 2 {
let num_listeners = (data[1] % 10) as usize + 1; // 1-10 listeners
let (announcer, announcement) = Announcement::<u32>::new();
let listeners: Vec<_> = (0..num_listeners)
.map(|_| announcement.listener())
.collect();
// Drop announcer
drop(announcer);
// All should see closed state
for listener in listeners {
assert!(listener.is_closed());
}
}
});
+184
View File
@@ -0,0 +1,184 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use announcement::Announcement;
use std::sync::{Arc, Barrier};
use std::thread;
fuzz_target!(|data: &[u8]| {
if data.len() < 4 {
return;
}
// Extract test parameters from fuzz data
let num_threads = (data[0] % 8) as usize + 2; // 2-9 threads
let announce_delay = data[1] % 3; // 0-2 delay pattern
let pattern = data[2] % 4;
match pattern {
0 => {
// Test concurrent listeners
let (announcer, announcement) = Announcement::new();
let barrier = Arc::new(Barrier::new(num_threads + 1));
let handles: Vec<_> = (0..num_threads)
.map(|i| {
let listener = announcement.listener();
let barrier = barrier.clone();
thread::spawn(move || {
// Synchronize start
barrier.wait();
// All threads try to listen
let value = listener.try_listen();
// Return thread id and value
(i, value)
})
})
.collect();
// Start all threads together
barrier.wait();
// Announce after threads are started
match announce_delay {
0 => {
// Immediate announce
let _ = announcer.announce(42u32);
}
1 => {
// Short delay
thread::sleep(std::time::Duration::from_micros(10));
let _ = announcer.announce(42u32);
}
_ => {
// Drop without announcing
drop(announcer);
}
}
// Collect results
for handle in handles {
let (id, value) = handle.join().unwrap();
if announce_delay < 2 {
// Should get value if we announced
// (might be None if thread checked before announce)
if let Some(v) = value {
assert_eq!(v, 42u32, "Thread {} got wrong value", id);
}
}
}
}
1 => {
// Test concurrent clone and listener creation
let (announcer, announcement) = Announcement::new();
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let ann_clone = announcement.clone();
thread::spawn(move || {
// Create listener from clone
let listener = ann_clone.listener();
// Try to read
listener.try_listen()
})
})
.collect();
// Announce while threads are creating listeners
let _ = announcer.announce(42u32);
// All should eventually see the value
for handle in handles {
let value = handle.join().unwrap();
// May or may not have seen the value depending on timing
if let Some(v) = value {
assert_eq!(v, 42u32);
}
}
}
2 => {
// Test concurrent is_announced checks
let (announcer, announcement) = Announcement::new();
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let ann_clone = announcement.clone();
thread::spawn(move || {
// Check announced state multiple times
let mut states = vec![];
for _ in 0..10 {
states.push(ann_clone.is_announced());
}
states
})
})
.collect();
// Announce in the middle
thread::sleep(std::time::Duration::from_micros(5));
let _ = announcer.announce(42u32);
// Collect all state observations
for handle in handles {
let states = handle.join().unwrap();
// States should only transition from false to true, never back
let mut seen_true = false;
for state in states {
if seen_true {
assert!(state, "is_announced went from true to false");
}
if state {
seen_true = true;
}
}
}
}
_ => {
// Test concurrent close checks
let (announcer, announcement) = Announcement::<u32>::new();
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let ann_clone = announcement.clone();
thread::spawn(move || {
// Check closed state multiple times
let mut states = vec![];
for _ in 0..10 {
states.push(ann_clone.is_closed());
}
states
})
})
.collect();
// Close in the middle
thread::sleep(std::time::Duration::from_micros(5));
announcer.close();
// Collect all state observations
for handle in handles {
let states = handle.join().unwrap();
// States should only transition from false to true, never back
let mut seen_true = false;
for state in states {
if seen_true {
assert!(state, "is_closed went from true to false");
}
if state {
seen_true = true;
}
}
}
}
}
});
+103
View File
@@ -0,0 +1,103 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use announcement::Announcement;
fuzz_target!(|data: &[u8]| {
if data.is_empty() {
return;
}
let pattern = data[0] % 5;
match pattern {
0 => {
// Test try_listen before announce
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
// Should return None
assert_eq!(listener.try_listen(), None);
// Now announce
let _ = announcer.announce(42u32);
// Should return Some
assert_eq!(listener.try_listen(), Some(42u32));
}
1 => {
// Test try_listen after announce
let (announcer, announcement) = Announcement::new();
let _ = announcer.announce(42u32);
let listener = announcement.listener();
// Should immediately return Some
assert_eq!(listener.try_listen(), Some(42u32));
}
2 => {
// Test multiple try_listen calls
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
let _ = announcer.announce(42u32);
// Multiple calls should return the same value
assert_eq!(listener.try_listen(), Some(42u32));
assert_eq!(listener.try_listen(), Some(42u32));
assert_eq!(listener.try_listen(), Some(42u32));
}
3 => {
// Test multiple listeners
if data.len() >= 2 {
let num_listeners = (data[1] % 10) as usize + 1; // 1-10 listeners
let (announcer, announcement) = Announcement::new();
let listeners: Vec<_> = (0..num_listeners)
.map(|_| announcement.listener())
.collect();
let _ = announcer.announce(42u32);
// All should get the value
for listener in listeners {
assert_eq!(listener.try_listen(), Some(42u32));
}
}
}
_ => {
// Test clone and listener
let (announcer, announcement) = Announcement::new();
let announcement2 = announcement.clone();
let l1 = announcement.listener();
let l2 = announcement2.listener();
let _ = announcer.announce(42u32);
// Both should get the value
assert_eq!(l1.try_listen(), Some(42u32));
assert_eq!(l2.try_listen(), Some(42u32));
}
}
// Test is_announced with various patterns
if data.len() >= 3 {
let test_announced = data[2] % 2 == 0;
let (announcer, announcement) = Announcement::new();
let listener = announcement.listener();
assert!(!announcement.is_announced());
if test_announced {
let _ = announcer.announce(42u32);
assert!(announcement.is_announced());
assert_eq!(listener.try_listen(), Some(42u32));
} else {
announcer.close();
assert!(!announcement.is_announced());
assert!(announcement.is_closed());
}
}
});