use super::*;
use std::error::Error;
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::Builder;
#[test]
fn concurrent_errors_message_variants_display_without_source() {
let producer = ConcurrentErrors::Producer("Child thread panicked".to_owned());
let receiver = ConcurrentErrors::Receiver("worker panicked".to_owned());
assert_eq!(
producer.to_string(),
"producer thread failed: Child thread panicked",
);
assert_eq!(
receiver.to_string(),
"consumer thread failed: worker panicked"
);
assert!(producer.source().is_none());
assert!(receiver.source().is_none());
}
#[test]
fn concurrent_errors_thread_variant_carries_io_error_source() {
let io_err = std::io::Error::other("spawn failed");
let err = ConcurrentErrors::Thread(Box::new(io_err));
assert!(
err.to_string()
.starts_with("failed to spawn a worker thread:"),
"unexpected Display: {err}",
);
let source = err.source().expect("Thread must expose a source");
assert!(
source.downcast_ref::<std::io::Error>().is_some(),
"Thread source must downcast to io::Error",
);
assert_eq!(source.to_string(), "spawn failed");
}
#[test]
fn concurrent_errors_sender_variant_carries_send_error_source() {
let (sender, receiver): (JobSender<()>, JobReceiver<()>) = unbounded();
drop(receiver);
let send_err = sender
.send(None)
.expect_err("send must fail once the receiver is dropped");
let err = ConcurrentErrors::Sender(Box::new(send_err));
assert!(
err.to_string()
.starts_with("failed to send a file to a worker:"),
"unexpected Display: {err}",
);
assert!(err.source().is_some(), "Sender must expose a source");
}
#[test]
fn concurrent_errors_is_usable_as_boxed_std_error() {
fn returns_boxed() -> Result<(), Box<dyn Error>> {
Err(ConcurrentErrors::Producer("boom".to_owned()))?;
Ok(())
}
let boxed = returns_boxed().expect_err("must propagate as boxed error");
assert_eq!(boxed.to_string(), "producer thread failed: boom");
}
#[test]
fn consumer_terminates_on_poison_pill() {
let (sender, receiver): (JobSender<()>, JobReceiver<()>) = unbounded();
let invocations = Arc::new(AtomicUsize::new(0));
let invocations_for_closure = Arc::clone(&invocations);
let func = Arc::new(move |_path: PathBuf, _cfg: &()| {
invocations_for_closure.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let handle = thread::spawn(move || consumer(receiver, func));
sender.send(None).expect("send should succeed");
handle.join().expect("consumer thread should not panic");
assert_eq!(
invocations.load(Ordering::SeqCst),
0,
"consumer must not invoke the closure for the poison-pill",
);
}
#[test]
fn consumer_processes_jobs_then_terminates_on_poison_pill() {
let (sender, receiver): (JobSender<()>, JobReceiver<()>) = unbounded();
let invocations = Arc::new(AtomicUsize::new(0));
let invocations_for_closure = Arc::clone(&invocations);
let func = Arc::new(move |_path: PathBuf, _cfg: &()| {
invocations_for_closure.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let handle = thread::spawn(move || consumer(receiver, func));
let cfg = Arc::new(());
for name in ["a.rs", "b.rs", "c.rs"] {
sender
.send(Some(JobItem {
path: PathBuf::from(name),
cfg: Arc::clone(&cfg),
}))
.expect("send should succeed");
}
sender.send(None).expect("send should succeed");
handle.join().expect("consumer thread should not panic");
assert_eq!(
invocations.load(Ordering::SeqCst),
3,
"all three real jobs must be processed before the poison-pill",
);
}
#[test]
fn consumer_continues_past_processing_errors_then_terminates() {
let (sender, receiver): (JobSender<()>, JobReceiver<()>) = unbounded();
let invocations = Arc::new(AtomicUsize::new(0));
let invocations_for_closure = Arc::clone(&invocations);
let func = Arc::new(move |path: PathBuf, _cfg: &()| {
invocations_for_closure.fetch_add(1, Ordering::SeqCst);
let kind = if path == Path::new("pipe.rs") {
ErrorKind::BrokenPipe
} else {
ErrorKind::PermissionDenied
};
Err(std::io::Error::new(kind, "simulated"))
});
let handle = thread::spawn(move || consumer(receiver, func));
let cfg = Arc::new(());
for name in ["pipe.rs", "denied.rs"] {
sender
.send(Some(JobItem {
path: PathBuf::from(name),
cfg: Arc::clone(&cfg),
}))
.expect("send should succeed");
}
sender.send(None).expect("send should succeed");
handle.join().expect("consumer thread should not panic");
assert_eq!(
invocations.load(Ordering::SeqCst),
2,
"both jobs must be processed despite each returning an error",
);
}
#[test]
fn per_file_error_swallows_broken_pipe() {
let err = std::io::Error::new(ErrorKind::BrokenPipe, "broken pipe");
assert_eq!(
per_file_error_message(Path::new("ok.rs"), &err),
None,
"BrokenPipe must be swallowed silently",
);
}
#[test]
fn per_file_error_display_formats_other_errors() {
let err = std::io::Error::new(ErrorKind::PermissionDenied, "permission denied");
let message = per_file_error_message(Path::new("locked.rs"), &err)
.expect("a non-BrokenPipe error must produce a diagnostic");
assert_eq!(message, "error processing locked.rs: permission denied");
assert!(
!message.contains("kind:") && !message.contains("Os {") && !message.contains('{'),
"diagnostic must be Display-formatted, not a Debug struct: {message}",
);
}
#[test]
fn run_dispatches_every_file_in_the_terminal_list() {
let tmp = Builder::new()
.prefix("visible-run")
.tempdir()
.expect("tempdir");
let root = tmp.path();
let a = root.join("a.rs");
let b = root.join("b.py");
std::fs::write(&a, b"// a").expect("write a");
std::fs::write(&b, b"# b").expect("write b");
let processed = Arc::new(AtomicUsize::new(0));
let processed_for_closure = Arc::clone(&processed);
let runner = ConcurrentRunner::new(4, move |_path: PathBuf, _cfg: &()| {
processed_for_closure.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let files_data = FilesData { paths: vec![a, b] };
runner.run((), files_data).expect("run should succeed");
assert_eq!(processed.load(Ordering::SeqCst), 2);
}
#[test]
fn run_skips_directories_and_missing_paths_without_walking() {
let tmp = Builder::new()
.prefix("visible-skip")
.tempdir()
.expect("tempdir");
let root = tmp.path();
let file = root.join("keep.rs");
std::fs::write(&file, b"// keep").expect("write keep");
let subdir = root.join("sub");
std::fs::create_dir(&subdir).expect("mkdir sub");
std::fs::write(subdir.join("nested.rs"), b"// nested").expect("write nested");
let missing = root.join("does-not-exist.rs");
let processed = Arc::new(AtomicUsize::new(0));
let processed_for_closure = Arc::clone(&processed);
let runner = ConcurrentRunner::new(4, move |_path: PathBuf, _cfg: &()| {
processed_for_closure.fetch_add(1, Ordering::SeqCst);
Ok(())
});
let files_data = FilesData {
paths: vec![file, subdir, missing],
};
runner.run((), files_data).expect("run should succeed");
assert_eq!(processed.load(Ordering::SeqCst), 1);
}
#[test]
fn num_jobs_parses_auto_case_insensitively() {
assert_eq!(NumJobs::from_str("auto").expect("auto"), NumJobs::Auto);
assert_eq!(NumJobs::from_str("AUTO").expect("AUTO"), NumJobs::Auto);
assert_eq!(NumJobs::from_str("Auto").expect("Auto"), NumJobs::Auto);
}
#[test]
fn num_jobs_parses_positive_integer() {
let parsed = NumJobs::from_str("4").expect("4 must parse");
assert_eq!(
parsed,
NumJobs::Explicit(NonZeroUsize::new(4).expect("4 is non-zero"))
);
}
#[test]
fn num_jobs_rejects_invalid_string_naming_the_token() {
let err = NumJobs::from_str("not-a-number").expect_err("non-numeric must be rejected");
assert!(
matches!(&err, ParseNumJobsError::NotAPositiveInteger { .. }),
"non-numeric input must be the `NotAPositiveInteger` variant, got: {err:?}"
);
assert_eq!(err.input(), "not-a-number");
assert!(
err.to_string().contains("not-a-number"),
"error message must name the bad token, got: {err}"
);
}
#[test]
fn num_jobs_rejects_zero() {
let err = NumJobs::from_str("0").expect_err("zero must be rejected");
assert!(
matches!(&err, ParseNumJobsError::Zero { .. }),
"in-range zero must be the `Zero` variant, got: {err:?}"
);
assert_eq!(err.input(), "0");
assert!(
err.to_string().contains(">= 1"),
"zero error must mention the >= 1 floor, got: {err}"
);
}
#[test]
fn num_jobs_resolve_is_at_least_one() {
assert!(NumJobs::Auto.resolve() >= 1);
assert_eq!(
NumJobs::Explicit(NonZeroUsize::new(3).expect("3 is non-zero")).resolve(),
3
);
}
#[test]
fn num_jobs_default_is_auto() {
assert_eq!(NumJobs::default(), NumJobs::Auto);
}