From 99a2c869a800eafed77760c9d79f8df6746efbfc Mon Sep 17 00:00:00 2001 From: jdidion Date: Sun, 14 Jun 2026 08:09:00 -0700 Subject: [PATCH] test: add coverage for untested non-trivial branches; add AGENTS.md Add 49 unit tests targeting previously-uncovered branches identified via cargo-llvm-cov, plus an AGENTS.md documenting build/test/coverage conventions. Coverage (features affinity,local-batch,retry): region 93.31% -> 95.26%, function 93.87% -> 96.30%, line 93.88% -> 96.05% New tests cover: - panic.rs: payload() accessor and PartialEq/Eq - bee/error.rs: From for ApplyRefError, into_apply_error arms, None inputs - bee/context.rs: task_id/attempt, submit() without local ctx, From - bee/worker.rs: Worker::map Fatal/Retryable arms, apply_ref Fatal/Cancelled - bee/stock/thunk.rs: PunkWorker ok + panic-catch, worker Clone impls - bee/stock/call.rs: Callable Deref/DerefMut - bee/queen.rs: Default for CloneQueen - hive/builder/{bee,full,open}.rs: From/From, with_queen_mut, config_ref - hive/outcome/impl.rs: error(), subtask_ids(), try_into_input WithSubtasks - hive/outcome/iter.rs: select_*_results/outputs, into_results/outputs, drop-unrequested - hive/outcome/queue.rs: outcomes_deref_mut path - hive/outcome/store.rs: len, remove_all, OwnedOutcomes group (unwrap, ok_or_unwrap_errors, into_unprocessed ordered/unordered, iter_*) No production code or build-system changes. clippy -D warnings, fmt --check, doc-tests, and the full suite (377 lib + 26 doc) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 53 ++++++++++++++ src/bee/context.rs | 44 ++++++++++++ src/bee/error.rs | 50 +++++++++++++- src/bee/queen.rs | 9 +++ src/bee/stock/call.rs | 13 ++++ src/bee/stock/thunk.rs | 43 ++++++++++++ src/bee/worker.rs | 73 ++++++++++++++++++++ src/hive/builder/bee.rs | 22 ++++++ src/hive/builder/full.rs | 16 +++++ src/hive/builder/open.rs | 15 ++++ src/hive/outcome/impl.rs | 66 ++++++++++++++++++ src/hive/outcome/iter.rs | 68 +++++++++++++++++- src/hive/outcome/queue.rs | 20 ++++++ src/hive/outcome/store.rs | 141 ++++++++++++++++++++++++++++++++++++++ src/panic.rs | 22 ++++++ 15 files changed, 653 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..18c8f81 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/src/bee/context.rs b/src/bee/context.rs index ea7c8ba..52678c3 100644 --- a/src/bee/context.rs +++ b/src/bee/context.rs @@ -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 = 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 = 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 = 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); + } +} diff --git a/src/bee/error.rs b/src/bee/error.rs index 0cc5138..c01ae60 100644 --- a/src/bee/error.rs +++ b/src/bee/error.rs @@ -93,11 +93,59 @@ impl From for ApplyRefError { #[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; + #[test] + fn test_apply_ref_from_error() { + // `From` 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 = ApplyRefError::Fatal("bork").into_apply_error(7); + assert!(matches!( + fatal, + ApplyError::Fatal { + input: Some(7), + error: "bork" + } + )); + + let retryable: ApplyError = + ApplyRefError::Retryable("bork").into_apply_error(8); + assert!(matches!( + retryable, + ApplyError::Retryable { + input: 8, + error: "bork" + } + )); + + let cancelled: ApplyError = + 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 ApplyError { pub fn panic(input: Option, detail: Option) -> Self { Self::Panic { diff --git a/src/bee/queen.rs b/src/bee/queen.rs index c0c93c8..3c790bf 100644 --- a/src/bee/queen.rs +++ b/src/bee/queen.rs @@ -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::>::default(); + let worker1 = queen.create(); + let worker2 = queen.create(); + assert_eq!(worker1, worker2); + } } diff --git a/src/bee/stock/call.rs b/src/bee/stock/call.rs index 54e9c00..d70a56e 100644 --- a/src/bee/stock/call.rs +++ b/src/bee/stock/call.rs @@ -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 = 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); diff --git a/src/bee/stock/thunk.rs b/src/bee/stock/thunk.rs index d8f90c6..d6ec673 100644 --- a/src/bee/stock/thunk.rs +++ b/src/bee/stock/thunk.rs @@ -148,4 +148,47 @@ mod tests { }) )); } + + #[test] + fn test_punk_ok() { + let mut worker = PunkWorker::::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::::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::::default().clone(); + assert_eq!( + 3, + thunk_worker + .apply(Thunk::from(|| 3), &Context::empty()) + .unwrap() + ); + + let mut funk_worker = FunkWorker::::default().clone(); + assert_eq!( + 4, + funk_worker + .apply(Thunk::fallible(|| Ok(4)), &Context::empty()) + .unwrap() + ); + + let mut punk_worker = PunkWorker::::default().clone(); + assert_eq!( + 5, + punk_worker + .apply(Thunk::from(|| 5), &Context::empty()) + .unwrap() + ); + } } diff --git a/src/bee/worker.rs b/src/bee/worker.rs index 6fc3902..a1302b6 100644 --- a/src/bee/worker.rs +++ b/src/bee/worker.rs @@ -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) -> WorkerResult { + 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) -> WorkerResult { + 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")]); + } } diff --git a/src/hive/builder/bee.rs b/src/hive/builder/bee.rs index a393b84..5907f58 100644 --- a/src/hive/builder/bee.rs +++ b/src/hive/builder/bee.rs @@ -292,4 +292,26 @@ mod tests { let full_builder = with_fn(bee_builder); let _hive = full_builder.build(); } + + #[test] + fn test_from_config() { + // `From` uses the default queen + let builder: BeeBuilder = Config::default().into(); + let _hive = builder.with_channel_queues().build(); + } + + #[test] + fn test_from_queen() { + // `From` uses the default config + let builder: BeeBuilder = 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::::empty(TestQueen).num_threads(2); + let _hive = builder.with_channel_queues().build(); + } } diff --git a/src/hive/builder/full.rs b/src/hive/builder/full.rs index 4865420..f8dfd8c 100644 --- a/src/hive/builder/full.rs +++ b/src/hive/builder/full.rs @@ -115,4 +115,20 @@ mod tests { let builder = factory(TestQueen); let _hive = builder.build(); } + + #[test] + fn test_from_config() { + // `From` uses the default queen + let builder: FullBuilder>> = + Config::default().into(); + let _hive = builder.build(); + } + + #[test] + fn test_from_queen() { + // `From` uses the default config + let builder: FullBuilder>> = + TestQueen.into(); + let _hive = builder.build(); + } } diff --git a/src/hive/builder/open.rs b/src/hive/builder/open.rs index 5600870..86b947a 100644 --- a/src/hive/builder/open.rs +++ b/src/hive/builder/open.rs @@ -350,6 +350,21 @@ mod tests { let _hive = queue_builder.build(); } + #[rstest] + fn test_queen_mut( + #[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>, + W: Fn(BeeBuilder>) -> FullBuilder, 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( #[values(OpenBuilder::empty, OpenBuilder::default)] factory: F, diff --git a/src/hive/outcome/impl.rs b/src/hive/outcome/impl.rs index 79cf339..783a988 100644 --- a/src/hive/outcome/impl.rs +++ b/src/hive/outcome/impl.rs @@ -438,6 +438,72 @@ mod tests { .try_into_error(); } + #[test] + fn test_error_accessor() { + // `Failure` and `FailureWithSubtasks` expose their error + let failure = WorkerOutcome::Failure { + input: Some(1), + error: (), + task_id: 1, + }; + assert_eq!(failure.error(), Some(&())); + + let failure_sub = WorkerOutcome::FailureWithSubtasks { + input: Some(1), + error: (), + task_id: 1, + subtask_ids: vec![2, 3], + }; + assert_eq!(failure_sub.error(), Some(&())); + + // non-error outcomes have no error + let success = WorkerOutcome::Success { + value: 1, + task_id: 1, + }; + assert_eq!(success.error(), None); + } + + #[test] + fn test_subtask_ids_accessor() { + // the `*WithSubtasks` variants return their subtask IDs + let success_sub = WorkerOutcome::SuccessWithSubtasks { + value: 1, + task_id: 1, + subtask_ids: vec![2, 3, 4], + }; + assert_eq!(success_sub.subtask_ids(), Some(&vec![2, 3, 4])); + // its task_id accessor works too + assert_eq!(success_sub.task_id(), &1); + + let unprocessed_sub = WorkerOutcome::UnprocessedWithSubtasks { + input: 9, + task_id: 5, + subtask_ids: vec![6], + }; + assert_eq!(unprocessed_sub.subtask_ids(), Some(&vec![6])); + // try_into_input on the WithSubtasks unprocessed variant returns the input + assert_eq!(unprocessed_sub.try_into_input(), Some(9)); + + // a plain Success has no subtasks + let success = WorkerOutcome::Success { + value: 1, + task_id: 1, + }; + assert_eq!(success.subtask_ids(), None); + } + + #[test] + fn test_try_into_input_failure_with_subtasks() { + let failure_sub = WorkerOutcome::FailureWithSubtasks { + input: Some(42), + error: (), + task_id: 1, + subtask_ids: vec![2], + }; + assert_eq!(failure_sub.try_into_input(), Some(42)); + } + #[test] fn test_eq() { let outcome1 = WorkerOutcome::Success { diff --git a/src/hive/outcome/iter.rs b/src/hive/outcome/iter.rs index abad145..3153648 100644 --- a/src/hive/outcome/iter.rs +++ b/src/hive/outcome/iter.rs @@ -252,13 +252,79 @@ impl>> OutcomeIteratorExt for T #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { - use super::{OrderedOutcomeIterator, UnorderedOutcomeIterator}; + use super::{OrderedOutcomeIterator, OutcomeIteratorExt, UnorderedOutcomeIterator}; use crate::bee::stock::EchoWorker; use crate::hive::Outcome; type Worker = EchoWorker; type WorkerOutcome = Outcome; + fn successes() -> Vec { + vec![ + WorkerOutcome::Success { + value: 20, + task_id: 2, + }, + WorkerOutcome::Success { + value: 10, + task_id: 1, + }, + WorkerOutcome::Success { + value: 0, + task_id: 0, + }, + ] + } + + #[test] + fn test_drop_unrequested_outcomes() { + // task_id 1 is not requested, so it is silently dropped by the unordered iterator + let outcomes: Vec<_> = UnorderedOutcomeIterator::new(successes(), [0, 2]) + .map(|o| *o.task_id()) + .collect(); + assert_eq!(outcomes.len(), 2); + assert!(outcomes.contains(&0)); + assert!(outcomes.contains(&2)); + assert!(!outcomes.contains(&1)); + } + + #[test] + fn test_select_results_unordered() { + let mut results: Vec<_> = successes().select_unordered_results(0..3).collect(); + results.sort(); + assert_eq!(results, vec![Ok(0), Ok(10), Ok(20)]); + } + + #[test] + fn test_select_results_ordered() { + let results: Vec<_> = successes().select_ordered_results(0..3).collect(); + assert_eq!(results, vec![Ok(0), Ok(10), Ok(20)]); + } + + #[test] + fn test_select_outputs_unordered() { + let mut outputs: Vec<_> = successes().select_unordered_outputs(0..3).collect(); + outputs.sort(); + assert_eq!(outputs, vec![0, 10, 20]); + } + + #[test] + fn test_select_outputs_ordered() { + let outputs: Vec<_> = successes().select_ordered_outputs(0..3).collect(); + assert_eq!(outputs, vec![0, 10, 20]); + } + + #[test] + fn test_into_results_and_outputs() { + let mut results: Vec<_> = successes().into_results().collect(); + results.sort(); + assert_eq!(results, vec![Ok(0), Ok(10), Ok(20)]); + + let mut outputs: Vec<_> = successes().into_outputs().collect(); + outputs.sort(); + assert_eq!(outputs, vec![0, 10, 20]); + } + #[test] fn test_unordered_missing() { let outcomes = vec![ diff --git a/src/hive/outcome/queue.rs b/src/hive/outcome/queue.rs index 9ed7b70..7572537 100644 --- a/src/hive/outcome/queue.rs +++ b/src/hive/outcome/queue.rs @@ -115,4 +115,24 @@ mod tests { ); assert_eq!(outcomes[&4], Outcome::Missing { task_id: 4 }) } + + #[test] + fn test_deref_mut() { + let mut queue = OutcomeQueue::>::default(); + queue.push(Outcome::Success { + value: 42, + task_id: 1, + }); + queue.push(Outcome::Success { + value: 43, + task_id: 2, + }); + // `remove` goes through `outcomes_deref_mut`, which flushes the queue into the map + assert!(matches!( + queue.remove(1), + Some(Outcome::Success { value: 42, .. }) + )); + assert_eq!(queue.len(), 1); + assert!(queue.remove(99).is_none()); + } } diff --git a/src/hive/outcome/store.rs b/src/hive/outcome/store.rs index 45b5733..cff12dd 100644 --- a/src/hive/outcome/store.rs +++ b/src/hive/outcome/store.rs @@ -525,6 +525,147 @@ mod tests { assert_eq!(vec![(1, 2)], store.remove_all_unprocessed()); assert_eq!(2, store.remove_all_failures().len()); } + + #[test] + fn test_len() { + let store = make_batch(); + assert_eq!(store.len(), 4); + let empty: OutcomeBatch = OutcomeBatch::empty(); + assert_eq!(empty.len(), 0); + } + + #[test] + fn test_counts() { + let store = make_batch(); + assert_eq!(store.num_successes(), 1); + assert_eq!(store.num_unprocessed(), 1); + assert_eq!(store.num_failures(), 2); + } + + #[test] + fn test_remove_all_drain() { + // `remove_all` drains every outcome, sorted by task_id + let mut store = make_batch(); + let removed = store.remove_all(); + assert_eq!(removed.len(), 4); + let ids: Vec<_> = removed.iter().map(|o| *o.task_id()).collect(); + assert_eq!(ids, vec![0, 1, 2, 3]); + assert!(store.is_empty()); + } + + #[test] + fn test_into_iter_owned() { + let store = make_batch(); + let count = OutcomeStore::into_iter(store).count(); + assert_eq!(count, 4); + } + + #[test] + fn test_unwrap_all_successes() { + let mut store: OutcomeBatch = OutcomeBatch::empty(); + store.insert(Outcome::Success { + value: 1, + task_id: 0, + }); + store.insert(Outcome::Success { + value: 2, + task_id: 1, + }); + let mut outputs = store.unwrap(); + outputs.sort(); + assert_eq!(outputs, vec![1, 2]); + } + + #[test] + #[should_panic] + fn test_unwrap_panics_on_failure() { + let store = make_batch(); + let _ = store.unwrap(); + } + + #[test] + fn test_ok_or_unwrap_errors_ok() { + let mut store: OutcomeBatch = OutcomeBatch::empty(); + store.insert(Outcome::Success { + value: 7, + task_id: 0, + }); + let result = store.ok_or_unwrap_errors(true); + assert_eq!(result, Ok(vec![7])); + } + + #[test] + fn test_ok_or_unwrap_errors_err() { + let mut store: OutcomeBatch = OutcomeBatch::empty(); + store.insert(Outcome::Success { + value: 1, + task_id: 0, + }); + store.insert(Outcome::Failure { + input: Some(2), + error: (), + task_id: 1, + }); + // drop_unprocessed=true; there is a failure so we get the errors back + let result = store.ok_or_unwrap_errors(true); + assert_eq!(result, Err(vec![()])); + } + + #[test] + #[should_panic] + fn test_ok_or_unwrap_errors_panics_on_unprocessed() { + let mut store: OutcomeBatch = OutcomeBatch::empty(); + store.insert(Outcome::Unprocessed { + input: 1, + task_id: 0, + }); + // drop_unprocessed=false with an unprocessed outcome => panic + let _ = store.ok_or_unwrap_errors(false); + } + + fn make_unprocessed_batch() -> OutcomeBatch { + let mut store: OutcomeBatch = OutcomeBatch::empty(); + store.insert(Outcome::Unprocessed { + input: 30, + task_id: 2, + }); + store.insert(Outcome::Unprocessed { + input: 10, + task_id: 0, + }); + store.insert(Outcome::Unprocessed { + input: 20, + task_id: 1, + }); + store.insert(Outcome::Success { + value: 99, + task_id: 3, + }); + store + } + + #[test] + fn test_into_unprocessed_ordered() { + // ordered: inputs returned in task_id order + let ordered = make_unprocessed_batch().into_unprocessed(true); + assert_eq!(ordered, vec![10, 20, 30]); + } + + #[test] + fn test_into_unprocessed_unordered() { + // unordered: same set, order unspecified + let mut unordered = make_unprocessed_batch().into_unprocessed(false); + unordered.sort(); + assert_eq!(unordered, vec![10, 20, 30]); + } + + #[test] + fn test_iter_groups() { + let store = make_batch(); + assert_eq!(store.iter_unprocessed().count(), 1); + assert_eq!(store.iter_successes().count(), 1); + assert_eq!(store.iter_failures().count(), 2); + } } #[cfg(all(test, feature = "retry"))] diff --git a/src/panic.rs b/src/panic.rs index 06f76ad..2dce924 100644 --- a/src/panic.rs +++ b/src/panic.rs @@ -77,6 +77,28 @@ mod tests { assert_eq!(*panic.detail().unwrap(), "test"); } + #[test] + fn test_payload() { + let result = Panic::try_call(Some("detail".to_string()), || panic!("boom")); + let panic = result.unwrap_err(); + // the payload is the `&str` passed to `panic!` + let payload = panic.payload(); + assert_eq!(payload.downcast_ref::<&str>(), Some(&"boom")); + } + + #[test] + fn test_eq() { + // same payload type and same detail => equal + let a = Panic::::new("boom", Some("d1".to_string())); + let b = Panic::::new("kaboom", Some("d1".to_string())); + assert_eq!(a, b); + // same payload type but different detail => not equal + let c = Panic::::new("boom", Some("d2".to_string())); + assert_ne!(a, c); + // a `Panic` always equals itself (reflexive) + assert!(a == a); + } + #[test] #[should_panic] fn test_resume_panic() {