curfew 0.0.5

A deadline-aware fork-join model where multiple tasks run in parallel, but only those that finish before a specified absolute clock time are joined and their results collected.
Documentation
use curfew::{join_with_deadline, DeadlineJoiner, JoinOutcome, TaskOutcome};
use std::thread;
use std::time::{Duration, Instant};

#[test]
fn collects_results_before_deadline() {
    let deadline = Instant::now() + Duration::from_millis(200);
    let mut joiner = DeadlineJoiner::new(deadline);

    for value in 0..3u32 {
        joiner.spawn(move || {
            thread::sleep(Duration::from_millis(40));
            value * 2
        });
    }

    let summary = joiner.join_ready();
    assert_eq!(summary.completed_count(), 3);
    assert_eq!(summary.timed_out_count(), 0);

    let mut outputs: Vec<_> = summary
        .completed
        .into_iter()
        .map(|result| match result.outcome {
            TaskOutcome::Completed(v) => v,
            TaskOutcome::Panicked(info) => panic!("unexpected panic: {info}"),
        })
        .collect();
    outputs.sort_unstable();
    assert_eq!(outputs, vec![0, 2, 4]);
}

#[test]
fn deadline_skips_late_tasks() {
    let deadline = Instant::now() + Duration::from_millis(150);
    let mut joiner = DeadlineJoiner::new(deadline);

    joiner.spawn(|| {
        thread::sleep(Duration::from_millis(50));
        10
    });
    joiner.spawn(|| {
        thread::sleep(Duration::from_millis(200));
        20
    });
    joiner.spawn(|| {
        thread::sleep(Duration::from_millis(210));
        30
    });

    let summary = joiner.join_ready();
    assert_eq!(summary.completed_count(), 1);
    assert_eq!(summary.timed_out_count(), 2);
}

#[test]
fn propagates_panics_as_metadata() {
    let deadline = Instant::now() + Duration::from_millis(150);
    let mut joiner = DeadlineJoiner::new(deadline);

    joiner.spawn(|| {
        thread::sleep(Duration::from_millis(20));
        1
    });
    joiner.spawn(|| panic!("boom"));

    let summary = joiner.join_ready();
    assert_eq!(summary.completed_count(), 2);

    let mut saw_panic = false;
    for result in summary.completed {
        match result.outcome {
            TaskOutcome::Completed(_) => {}
            TaskOutcome::Panicked(info) => {
                saw_panic = true;
                assert!(info.message().contains("boom"));
            }
        }
    }
    assert!(saw_panic, "expected at least one panic to be recorded");
}

#[test]
fn join_with_deadline_finishes_in_time() {
    let handle = thread::spawn(|| {
        thread::sleep(Duration::from_millis(20));
        42
    });

    let outcome = join_with_deadline(
        handle,
        Instant::now() + Duration::from_millis(200),
    );
    match outcome {
        JoinOutcome::Completed(result) => match result.outcome {
            TaskOutcome::Completed(value) => assert_eq!(value, 42),
            TaskOutcome::Panicked(info) => panic!("unexpected panic: {info}"),
        },
        JoinOutcome::TimedOut(_) => {
            panic!("thread should have completed in time")
        }
    }
}

#[test]
fn join_with_deadline_can_recover_late_result() {
    let handle = thread::spawn(|| {
        thread::sleep(Duration::from_millis(150));
        99
    });

    let deadline = Instant::now() + Duration::from_millis(50);
    match join_with_deadline(handle, deadline) {
        JoinOutcome::Completed(_) => {
            panic!("thread should not have met the deadline")
        }
        JoinOutcome::TimedOut(late) => {
            let result = late.wait();
            match result.outcome {
                TaskOutcome::Completed(value) => assert_eq!(value, 99),
                TaskOutcome::Panicked(info) => {
                    panic!("unexpected panic: {info}")
                }
            }
        }
    }
}