chore: initial release v0.1.0
Runtime-agnostic oneshot broadcast channel for Rust.
This commit is contained in:
64 files changed
+23966
No files matched your search
@@ -0,0 +1,4 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
@@ -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
@@ -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/)
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user