Skip to content
Draft
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
53 changes: 53 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# AGENTS.md — beekeeper

Worker-pool library for Rust. Edition 2024, MSRV 1.85. Trunk branch: `main`.

## Build / test / lint

```sh
# build with the canonical feature set
cargo build -F affinity,local-batch,retry

# unit + doc tests (the CI `check` job runs clippy, fmt, doc, and doc-tests)
cargo test -F affinity,local-batch,retry
cargo test -F affinity,local-batch,retry --doc

# lint exactly as CI does (warnings are denied)
cargo clippy --all-targets -F affinity,local-batch,retry -- -D warnings
cargo fmt -- --check
RUSTDOCFLAGS="-D warnings" cargo doc -F affinity,local-batch,retry
```

## Features

`default = ["local-batch"]`. Optional: `affinity`, `local-batch`, `retry`.

The channel-backend features `crossbeam`, `flume`, and `loole` are **mutually
exclusive** — at most one may be enabled, and with none enabled the library uses
`std::sync::mpsc`. `cargo … --all-features` therefore does **not** compile;
build/test each backend separately (CI runs a matrix over `default`, `crossbeam`,
`flume`, `loole`). The canonical feature set used for lint/doc/coverage is
`affinity,local-batch,retry`.

Tests are inline (`#[cfg(test)] mod tests`) per source file; there is no `tests/`
directory. Test modules carry `#[cfg_attr(coverage_nightly, coverage(off))]` so
the test code itself is excluded from coverage.

## Coverage

```sh
cargo llvm-cov -F affinity,local-batch,retry --summary-only # totals
cargo llvm-cov -F affinity,local-batch,retry --show-missing-lines # uncovered lines
cargo llvm-cov -F affinity,local-batch,retry --lcov --output-path lcov.info
```

Coverage needs `llvm-tools` (for `llvm-profdata`/`llvm-cov`). If the active
toolchain lacks them (e.g. a Homebrew-installed `rustc`), run through a rustup
toolchain that has the `llvm-tools` component, e.g.
`rustup run stable cargo llvm-cov …`.

## Release

Releases are automated via `release-plz` (`release-plz.toml`) and changelog
generation via `git-cliff` (`cliff.toml`). Publishing to crates.io is a
maintainer action — do not publish from an agent session.
44 changes: 44 additions & 0 deletions src/bee/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,47 @@ impl TaskMeta {
}
}
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::{Context, TaskMeta};

#[test]
fn test_context_task_id_and_attempt() {
let ctx: Context<usize> = Context::new(TaskMeta::new(7), None);
assert_eq!(ctx.task_id(), 7);
// attempt defaults to 0
assert_eq!(ctx.attempt(), 0);
}

#[test]
fn test_empty_context_is_not_cancelled() {
let ctx: Context<usize> = Context::empty();
assert!(!ctx.is_cancelled());
}

#[test]
fn test_submit_without_local_returns_err() {
// with no `LocalContext`, `submit` cannot enqueue and returns the input back
let ctx: Context<usize> = Context::empty();
assert_eq!(ctx.submit(42), Err(42));
// no subtasks were recorded
let (_meta, subtask_ids) = ctx.into_parts();
assert!(subtask_ids.is_none());
}

#[test]
fn test_task_meta_from_task_id() {
let meta: TaskMeta = TaskMeta::from(13);
assert_eq!(meta.id(), 13);
}

#[cfg(feature = "local-batch")]
#[test]
fn test_task_meta_weight() {
let meta = TaskMeta::with_weight(3, 99);
assert_eq!(meta.id(), 3);
assert_eq!(meta.weight(), 99);
}
}
50 changes: 49 additions & 1 deletion src/bee/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,59 @@ impl<E> From<E> for ApplyRefError<E> {
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::ApplyError;
use super::{ApplyError, ApplyRefError};
use crate::panic::Panic;

type TestError<'a> = ApplyError<usize, &'a str>;

#[test]
fn test_apply_ref_from_error() {
// `From<E>` converts a bare error into a `Fatal` variant
let err: ApplyRefError<&str> = "bork".into();
assert!(matches!(err, ApplyRefError::Fatal("bork")));
}

#[test]
fn test_apply_ref_into_apply_error() {
let fatal: ApplyError<usize, &str> = ApplyRefError::Fatal("bork").into_apply_error(7);
assert!(matches!(
fatal,
ApplyError::Fatal {
input: Some(7),
error: "bork"
}
));

let retryable: ApplyError<usize, &str> =
ApplyRefError::Retryable("bork").into_apply_error(8);
assert!(matches!(
retryable,
ApplyError::Retryable {
input: 8,
error: "bork"
}
));

let cancelled: ApplyError<usize, &str> =
ApplyRefError::<&str>::Cancelled.into_apply_error(9);
assert!(matches!(cancelled, ApplyError::Cancelled { input: 9 }));
}

#[test]
fn test_input_without_value() {
// `Fatal` and `Panic` can hold `None` inputs
let fatal: TestError = ApplyError::Fatal {
input: None,
error: "bork",
};
assert!(fatal.input().is_none());
assert!(fatal.into_input().is_none());

let panic: TestError = ApplyError::panic(None, None);
assert!(panic.input().is_none());
assert!(panic.into_input().is_none());
}

impl<I, E> ApplyError<I, E> {
pub fn panic(input: Option<I>, detail: Option<String>) -> Self {
Self::Panic {
Expand Down
9 changes: 9 additions & 0 deletions src/bee/queen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,13 @@ mod tests {
let worker2 = queen2.create();
assert_eq!(worker1, worker2);
}

#[test]
fn test_clone_queen_default() {
// `CloneQueen` is `Default` when its `Worker` is `Default`
let queen = CloneQueen::<EchoWorker<u32>>::default();
let worker1 = queen.create();
let worker2 = queen.create();
assert_eq!(worker1, worker2);
}
}
13 changes: 13 additions & 0 deletions src/bee/stock/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,19 @@ mod tests {
assert!(matches!(worker.apply(5, &Context::empty()), Ok(6)))
}

#[test]
fn test_callable_deref() {
use std::ops::{Deref, DerefMut};
// `Callable` derefs (immutably and mutably) to the wrapped function
let mut callable: Callable<u8, u8, (), _> = Callable::of(|input: u8| input + 1);
// immutable deref: copy the (Copy) closure out via `Deref`, then call it
let f = *Deref::deref(&callable);
assert_eq!(f(5), 6);
// mutable deref: call the wrapped function in place via `DerefMut`
let f_mut = DerefMut::deref_mut(&mut callable);
assert_eq!(f_mut(7), 8);
}

#[test]
fn test_clone() {
let worker1 = Caller::from(|input: u8| input + 1);
Expand Down
43 changes: 43 additions & 0 deletions src/bee/stock/thunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,47 @@ mod tests {
})
));
}

#[test]
fn test_punk_ok() {
let mut worker = PunkWorker::<u8>::default();
let thunk = Thunk::from(|| 7);
assert_eq!(7, worker.apply(thunk, &Context::empty()).unwrap());
}

#[test]
fn test_punk_catches_panic() {
let mut worker = PunkWorker::<u8>::default();
let thunk = Thunk::from(|| panic!("kaboom"));
let result = worker.apply(thunk, &Context::empty());
assert!(matches!(result, Err(ApplyError::Panic { input: None, .. })));
}

#[test]
fn test_clone() {
// each worker type implements `Clone`; cloning yields an equivalent, usable worker
let mut thunk_worker = ThunkWorker::<u8>::default().clone();
assert_eq!(
3,
thunk_worker
.apply(Thunk::from(|| 3), &Context::empty())
.unwrap()
);

let mut funk_worker = FunkWorker::<u8, String>::default().clone();
assert_eq!(
4,
funk_worker
.apply(Thunk::fallible(|| Ok(4)), &Context::empty())
.unwrap()
);

let mut punk_worker = PunkWorker::<u8>::default().clone();
assert_eq!(
5,
punk_worker
.apply(Thunk::from(|| 5), &Context::empty())
.unwrap()
);
}
}
73 changes: 73 additions & 0 deletions src/bee/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,77 @@ mod tests {
Err(ApplyError::Retryable { input: 0, .. })
));
}

#[test]
fn test_apply_fatal_and_cancelled() {
let mut worker = MyRefWorker;
let ctx = Context::empty();
// Fatal preserves the input
assert!(matches!(
worker.apply(1, &ctx),
Err(ApplyError::Fatal { input: Some(1), .. })
));
// Cancelled preserves the input
assert!(matches!(
worker.apply(2, &ctx),
Err(ApplyError::Cancelled { input: 2 })
));
}

/// A `Worker` whose `apply` returns a `Fatal` error for odd inputs, exercising the
/// `Fatal` arm of the default `Worker::map` implementation's error mapping.
#[derive(Debug)]
struct FallibleWorker;

impl Worker for FallibleWorker {
type Input = u8;
type Output = u8;
type Error = &'static str;

fn apply(&mut self, input: Self::Input, _: &Context<Self::Input>) -> WorkerResult<Self> {
if input % 2 == 0 {
Ok(input)
} else {
Err(ApplyError::Fatal {
input: Some(input),
error: "odd",
})
}
}
}

#[test]
fn test_map_maps_fatal_error() {
let mut worker = FallibleWorker;
let results: Vec<_> = worker.map(0..4).collect();
assert_eq!(results[0], Ok(0));
assert_eq!(results[1], Err("odd"));
assert_eq!(results[2], Ok(2));
assert_eq!(results[3], Err("odd"));
}

/// A `Worker` whose `apply` returns a `Retryable` error, exercising the `Retryable`
/// arm of the default `Worker::map` implementation's error mapping.
#[derive(Debug)]
struct RetryableWorker;

impl Worker for RetryableWorker {
type Input = u8;
type Output = u8;
type Error = &'static str;

fn apply(&mut self, input: Self::Input, _: &Context<Self::Input>) -> WorkerResult<Self> {
Err(ApplyError::Retryable {
input,
error: "retry",
})
}
}

#[test]
fn test_map_maps_retryable_error() {
let mut worker = RetryableWorker;
let results: Vec<_> = worker.map(0..2).collect();
assert_eq!(results, vec![Err("retry"), Err("retry")]);
}
}
22 changes: 22 additions & 0 deletions src/hive/builder/bee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,26 @@ mod tests {
let full_builder = with_fn(bee_builder);
let _hive = full_builder.build();
}

#[test]
fn test_from_config() {
// `From<Config>` uses the default queen
let builder: BeeBuilder<TestQueen> = Config::default().into();
let _hive = builder.with_channel_queues().build();
}

#[test]
fn test_from_queen() {
// `From<Q>` uses the default config
let builder: BeeBuilder<TestQueen> = TestQueen.into();
let _hive = builder.with_channel_queues().build();
}

#[test]
fn test_config_ref() {
// a config setter (`num_threads`) goes through `BeeBuilder`'s `BuilderConfig::config_ref`
use crate::hive::Builder;
let builder = BeeBuilder::<TestQueen>::empty(TestQueen).num_threads(2);
let _hive = builder.with_channel_queues().build();
}
}
16 changes: 16 additions & 0 deletions src/hive/builder/full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,20 @@ mod tests {
let builder = factory(TestQueen);
let _hive = builder.build();
}

#[test]
fn test_from_config() {
// `From<Config>` uses the default queen
let builder: FullBuilder<TestQueen, ChannelTaskQueues<EchoWorker<usize>>> =
Config::default().into();
let _hive = builder.build();
}

#[test]
fn test_from_queen() {
// `From<Q>` uses the default config
let builder: FullBuilder<TestQueen, ChannelTaskQueues<EchoWorker<usize>>> =
TestQueen.into();
let _hive = builder.build();
}
}
15 changes: 15 additions & 0 deletions src/hive/builder/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,21 @@ mod tests {
let _hive = queue_builder.build();
}

#[rstest]
fn test_queen_mut<F, T, W>(
#[values(OpenBuilder::empty, OpenBuilder::default)] factory: F,
#[values(BeeBuilder::with_channel_queues, BeeBuilder::with_workstealing_queues)] with_fn: W,
) where
F: Fn() -> OpenBuilder,
T: TaskQueues<EchoWorker<usize>>,
W: Fn(BeeBuilder<QueenCell<TestQueen>>) -> FullBuilder<QueenCell<TestQueen>, T>,
{
let open_builder = factory();
let bee_builder = open_builder.with_queen_mut(TestQueen);
let queue_builder = with_fn(bee_builder);
let _hive = queue_builder.build();
}

#[rstest]
fn test_queen_mut_default<F, T, W>(
#[values(OpenBuilder::empty, OpenBuilder::default)] factory: F,
Expand Down
Loading
Loading