1#![forbid(unsafe_code)]
33#![warn(missing_docs)]
34#![warn(unreachable_pub)]
35#![allow(non_snake_case)]
36
37#[cfg(test)]
38#[macro_use]
39mod test_support;
40
41mod domain;
42mod explain;
43mod phase_timing;
44mod progress;
45mod result;
46
47pub mod engine;
48pub mod language_file;
49pub mod languages;
50pub mod render;
51pub mod warnings;
52
53pub use domain::{Bucket, CountingModel, Keyword, Language, LeveledPair, LineClass, LineClasses,
54 LineContinuation, MultilineString, NestedLanguage, Span, SpanKind, Stats, StringRules};
55pub use engine::config::{EngineConfig, ForcedLanguages, LanguageNames, ScopedByModule, Target,
56 Threads, format_module_scope, split_off_module_scope};
57pub use engine::identity::{Claim, ClaimKind, SettledBy};
58pub use engine::targets::TargetError;
59pub use explain::{Carried, ExplainError, ExplainedLine, FileExplanation, explain_file};
60pub use languages::{LanguageClaims, Languages};
61pub use progress::ScanProgress;
62pub use result::{FaultyFileDetails, FileEntry, FilesPresent, ModuleResult, Performance, RunError,
63 RunResult, ScanSkip, SkippedFiles, SortCriterion, UnreadableDirDetails};
64pub use warnings::{Affects, Code, Warning};
65
66#[cfg(test)]
67pub(crate) use test_support::{languages_claiming, test_paths};
68
69use std::collections::HashMap;
70use std::path::{Path, PathBuf};
71use std::sync::{Arc, Mutex};
72use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
73use std::time::Instant;
74
75use crossbeam_deque::{Injector, Worker};
76
77use engine::modules::{ModuleId, Modules};
78
79pub const LANGUAGE_CONFLICTS_FILE_NAME : &str = "language_conflicts.txt";
87pub const UNNAMED_MODULE_NAME : &str = "(unnamed)";
89
90pub(crate) type FaultyFilesListMut = Arc<Mutex<Vec<FaultyFileDetails>>>;
91pub(crate) type SharedModuleLookups = Arc<engine::identity::ModuleLookups>;
92pub(crate) type StatsMapMut = Arc<Mutex<Vec<HashMap<String,Stats>>>>;
95pub(crate) type NestedLanguageMapMut = Arc<Mutex<Vec<HashMap<String,HashMap<String,Stats>>>>>;
96pub(crate) type FilesPerModuleMut = Arc<Mutex<Vec<HashMap<String, Vec<FileEntry>>>>>;
97
98pub fn run(config: &EngineConfig, languages: Languages) -> Result<RunResult, RunError> {
108 run_watched(config, languages, None, |_| {})
109}
110
111pub fn run_watched(config: &EngineConfig, languages: Languages, progress: Option<Arc<ScanProgress>>,
121 on_traversal_done: impl FnOnce(FilesPresent)) -> Result<RunResult, RunError>
122{
123 let progress = progress.unwrap_or_default();
124 let _walk_ends = WalkDoneGuard(progress.clone());
127 if config.targets.is_empty() {
128 return Err(RunError::NoTargets);
129 }
130 if !languages.describe_the_same_selection_as(config) {
132 return Err(RunError::LanguagesFromAnotherConfig);
133 }
134 let targets = engine::targets::resolve(&config.targets, ObeyedIgnoreFiles::of(config),
136 config.should_search_in_dotted).map_err(RunError::InvalidTargets)?;
137 let config = Arc::new(config.clone());
138 let faulty_files_ref : FaultyFilesListMut = Arc::new(Mutex::new(Vec::with_capacity(10)));
139 let finish_condition_ref = Arc::new(AtomicBool::new(false));
140 let (by_name, lookups, nested_definitions) = languages.into_parts();
141 let language_map_ref = Arc::new(by_name);
142 let nested_definitions = Arc::new(nested_definitions);
143 let modules = Arc::new(Modules::of(&targets));
144 let language_lookups: SharedModuleLookups = Arc::new(lookups.into_lookups_per_module(&modules));
147 let stats_per_module : StatsMapMut =
148 Arc::new(Mutex::new(make_language_stats(&language_map_ref, modules.count())));
149 let nested_per_module : NestedLanguageMapMut =
150 Arc::new(Mutex::new(vec![HashMap::new(); modules.count()]));
151 let files_per_module : FilesPerModuleMut =
152 Arc::new(Mutex::new(vec![HashMap::new(); modules.count()]));
153
154 let mut files_present = FilesPresent::default();
155 let idle_producers = Arc::new(AtomicUsize::new(0));
156 let files_injector = Arc::new(Injector::<ParsableFile>::new());
157 let dirs_injector = Arc::new(Injector::<TraversedDir>::new());
158 let exclude_matcher = Arc::new(engine::targets::build_exclude_matcher(&config.exclude_dirs)
159 .map_err(|_| {
160 let culprit = config.exclude_dirs.iter()
164 .find(|x| engine::targets::build_exclude_matcher(std::slice::from_ref(x)).is_err())
165 .cloned().unwrap_or_default();
166 RunError::InvalidExcludePattern(culprit)
167 })?);
168 queue_the_targets(&config, &targets, &dirs_injector, &files_injector, &mut files_present,
169 &language_lookups, &modules, &progress);
170
171 let files_stats = Arc::new(Mutex::new(files_present));
172 let unreadable_dirs = Arc::new(Mutex::new(Vec::new()));
173
174 let mut producer_handles = Vec::with_capacity(config.threads.producers());
175 let mut consumer_handles = Vec::with_capacity(config.threads.consumers());
176 let producers_total = Arc::new(AtomicUsize::new(usize::MAX));
180 let worker_panics: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
181
182 let parsing_started_instant = Instant::now();
185 let mut last_refusal = None;
186 let workers = (0..config.threads.producers())
189 .map(|_| Worker::<TraversedDir>::new_lifo()).collect::<Vec<_>>();
190 let stealers = Arc::new(workers.iter().map(Worker::stealer).collect::<Vec<_>>());
191 for (i, worker) in workers.into_iter().enumerate() {
192 match engine::producer::start_producer_thread(i, files_injector.clone(), dirs_injector.clone(), worker,
193 stealers.clone(), idle_producers.clone(), language_lookups.clone(), exclude_matcher.clone(),
194 config.clone(), files_stats.clone(), modules.clone(), unreadable_dirs.clone(),
195 producers_total.clone(), worker_panics.clone(), progress.clone()) {
196 Ok(handle) => producer_handles.push(handle),
197 Err(x) => last_refusal = Some(x)
198 }
199 }
200 if producer_handles.is_empty() {
201 return Err(RunError::NoThreadsAvailable { side: "producer", error: last_refusal.unwrap() });
202 }
203 producers_total.store(producer_handles.len(), Ordering::SeqCst);
204
205 let counting_ended = Arc::new(AtomicU64::new(0));
207 let skipped_files: Arc<Mutex<SkippedFiles>> = Arc::new(Mutex::new(SkippedFiles::default()));
209 for i in 0..config.threads.consumers() {
210 match engine::consumer::start_parser_thread(i, files_injector.clone(), faulty_files_ref.clone(), finish_condition_ref.clone(),
211 stats_per_module.clone(), nested_per_module.clone(), files_per_module.clone(),
212 language_map_ref.clone(), nested_definitions.clone(), language_lookups.clone(), config.clone(),
213 parsing_started_instant, counting_ended.clone(), skipped_files.clone(),
214 progress.clone()) {
215 Ok(handle) => consumer_handles.push(handle),
216 Err(x) => last_refusal = Some(x)
217 }
218 }
219 if consumer_handles.is_empty() {
220 for handle in producer_handles {
222 let _ = handle.join();
223 }
224 return Err(RunError::NoThreadsAvailable { side: "consumer", error: last_refusal.unwrap() });
225 }
226
227 let threads_used = Threads::new(producer_handles.len(), consumer_handles.len());
228 for handle in producer_handles {
229 let _ = handle.join();
230 }
231 progress.mark_walk_done();
235 let producers_done_millis = parsing_started_instant.elapsed().as_millis();
236
237 let queued_at_producer_exit = files_injector.len();
238
239 finish_condition_ref.store(true,Ordering::Relaxed);
240
241 let walk_was_whole = worker_panics.lock().unwrap_or_else(std::sync::PoisonError::into_inner).is_empty();
253 let files_present = *files_stats.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
254 if walk_was_whole {
255 on_traversal_done(files_present);
256 }
257
258 for handle in consumer_handles {
259 if let Err(payload) = handle.join() {
260 worker_panics.lock().unwrap().push(panic_message(payload.as_ref()));
261 }
262 }
263 let parsing_duration_millis = u128::from(counting_ended.load(Ordering::Relaxed)).max(producers_done_millis);
269
270 if *phase_timing::ENABLED {
271 eprintln!("[phase] producers alive: {} ms | drain after producers: {} ms | queue size at producer exit: {}",
272 producers_done_millis, parsing_duration_millis - producers_done_millis, queued_at_producer_exit);
273 eprintln!("{}", phase_timing::report(threads_used.consumers(), parsing_duration_millis));
274 }
275
276 let worker_panics = std::mem::take(&mut *worker_panics.lock().unwrap_or_else(std::sync::PoisonError::into_inner));
280 if !worker_panics.is_empty() {
281 return Err(RunError::IncompleteRun { worker_panic: worker_panics.join(" | ") });
282 }
283
284 let mut skipped_files = std::mem::take(&mut *skipped_files.lock().unwrap());
287 for kind in ScanSkip::ALL {
288 skipped_files.get_of_kind_mut(kind).sort_unstable();
289 }
290 let relevant_files_num = files_present.relevant_files;
291 if relevant_files_num == 0 {
292 return Ok(RunResult::of_nothing(files_present,
293 Performance { duration_millis: parsing_duration_millis, threads: threads_used }, &modules,
294 targets.to_vec(), std::mem::take(&mut unreadable_dirs.lock().unwrap())));
295 }
296
297 let mut stats_guard = stats_per_module.lock();
298 let per_module = stats_guard.as_deref_mut().unwrap();
299 let mut nested_guard = nested_per_module.lock();
300 let nested_by_module = nested_guard.as_deref_mut().unwrap();
301 let mut files_guard = files_per_module.lock();
302 let files_by_module = files_guard.as_deref_mut().unwrap();
303
304 let mut per_language = merge_over_modules(per_module);
305 remove_languages_with_0_files(&mut per_language);
309 let total = Stats::total_of(&per_language);
310
311 let nested_languages = merge_nested_over_modules(nested_by_module);
312
313 let modules_result = per_module.iter_mut().enumerate().map(|(id, bucket)| {
314 let mut of_this_module = std::mem::take(bucket);
315 remove_languages_with_0_files(&mut of_this_module);
316 ModuleResult {
319 name: modules.name_of(id as ModuleId).map(str::to_owned),
320 total: Stats::total_of(&of_this_module),
321 per_language: of_this_module,
322 nested_languages: std::mem::take(&mut nested_by_module[id]),
323 files: std::mem::take(&mut files_by_module[id])
324 }
325 }).collect::<Vec<_>>();
326
327 Ok(RunResult {
328 per_language,
329 total,
330 nested_languages,
331 modules: modules_result,
332 faulty_files: std::mem::take(&mut faulty_files_ref.lock().unwrap()),
333 skipped_files,
334 files_present,
335 performance: Performance { duration_millis: parsing_duration_millis, threads: threads_used },
336 targets: targets.to_vec(),
337 unreadable_dirs: std::mem::take(&mut unreadable_dirs.lock().unwrap())
338 })
339}
340
341pub fn prints_phase_timing() -> bool {
347 *phase_timing::ENABLED
348}
349
350struct WalkDoneGuard(Arc<ScanProgress>);
351
352impl Drop for WalkDoneGuard {
353 fn drop(&mut self) {
354 self.0.mark_walk_done();
355 }
356}
357
358pub(crate) fn queue_the_targets(config: &EngineConfig, targets: &engine::targets::Targets,
365 dirs_injector: &Arc<Injector<TraversedDir>>, files_injector: &Arc<Injector<ParsableFile>>,
366 files_present: &mut FilesPresent, language_lookups: &engine::identity::ModuleLookups, modules: &Modules,
367 progress: &ScanProgress)
368{
369 for target in crate::engine::targets::topmost_targets(targets) {
370 let dir_path = Path::new(&target.path);
371 let module = modules.of_target(&target);
372 if dir_path.is_file() {
373 let lookup = language_lookups.get_of_module(module);
374 let Some(lang_name) = lookup.of_path_or_shebang(dir_path) else {
375 continue;
376 };
377 let size = std::fs::metadata(dir_path).map_or(0, |m| m.len());
378 let queued = match targets.was_written_by_hand(dir_path) {
379 true => ParsableFile::written_by_hand(dir_path.to_path_buf(), lang_name, module, size),
380 false => ParsableFile::new(dir_path.to_path_buf(), lang_name, module, size)
381 };
382 files_injector.push(queued.with_extension_rules(lookup.find_extension_rules(dir_path)));
383 files_present.total_files += 1;
384 files_present.relevant_files += 1;
385 progress.record_file_found();
386 } else if dir_path.is_dir() {
387 let gitignore_stack = GitignoreStack::for_root_dir(dir_path, ObeyedIgnoreFiles::of(config));
388 dirs_injector.push(TraversedDir::new(dir_path.to_path_buf(), gitignore_stack, module));
389 }
390 }
391}
392
393pub(crate) fn remove_languages_with_0_files(languages: &mut HashMap<String,Stats>) {
395 languages.retain(|_, stats| stats.files > 0);
396}
397
398pub(crate) fn make_language_stats(languages: &HashMap<String,Language>, modules: usize) -> Vec<HashMap<String,Stats>> {
401 let of_one_module = languages.iter().map(|(name, language)| (name.to_owned(), Stats::from(language)))
402 .collect::<HashMap<_,_>>();
403 vec![of_one_module; modules]
404}
405
406#[derive(Debug,Clone)]
407pub(crate) struct ParsableFile {
408 pub path: PathBuf,
409 pub language_name: Arc<str>,
410 pub module: ModuleId,
411 pub size: u64,
414 pub written_by_hand: bool,
417 pub extension_rules: Option<Arc<engine::identity::ExtensionRules>>
418}
419
420impl ParsableFile {
421 pub(crate) fn new(path: PathBuf, language_name: Arc<str>, module: ModuleId, size: u64) -> Self {
422 ParsableFile {
423 path,
424 language_name,
425 module,
426 size,
427 written_by_hand: false,
428 extension_rules: None
429 }
430 }
431
432 pub(crate) fn written_by_hand(path: PathBuf, language_name: Arc<str>, module: ModuleId, size: u64) -> Self {
433 ParsableFile { written_by_hand: true, ..ParsableFile::new(path, language_name, module, size) }
434 }
435
436 pub(crate) fn with_extension_rules(mut self, extension_rules: Option<Arc<engine::identity::ExtensionRules>>) -> Self {
437 self.extension_rules = extension_rules;
438 self
439 }
440}
441
442#[derive(Debug,Clone)]
443pub(crate) struct TraversedDir {
444 pub path: PathBuf,
445 pub gitignore_stack: Option<Arc<GitignoreStack>>,
446 pub module: ModuleId
447}
448
449impl TraversedDir {
450 pub(crate) fn new(path: PathBuf, gitignore_stack: Option<Arc<GitignoreStack>>, module: ModuleId) -> Self {
451 TraversedDir {
452 path,
453 gitignore_stack,
454 module
455 }
456 }
457}
458
459#[derive(Debug, Clone, Copy)]
463pub(crate) struct ObeyedIgnoreFiles {
464 pub gitignore: bool,
465 pub search_tools: bool
466}
467
468impl ObeyedIgnoreFiles {
469 pub(crate) fn of(config: &EngineConfig) -> ObeyedIgnoreFiles {
470 ObeyedIgnoreFiles { gitignore: !config.no_gitignore, search_tools: !config.no_ignore_files }
471 }
472
473 pub(crate) fn obeys_nothing(self) -> bool {
474 !self.gitignore && !self.search_tools
475 }
476
477 pub(crate) fn get_file_names(self) -> impl Iterator<Item = &'static str> {
481 [(".gitignore", self.gitignore), (".ignore", self.search_tools), (".rgignore", self.search_tools)]
482 .into_iter().filter_map(|(name, obeyed)| obeyed.then_some(name))
483 }
484}
485
486#[derive(Debug)]
490pub(crate) struct GitignoreStack {
491 matcher: ignore::gitignore::Gitignore,
492 parent: Option<Arc<GitignoreStack>>
493}
494
495impl GitignoreStack {
496 pub(crate) fn extend_with_ignore_files(dir: &Path, parent: Option<Arc<GitignoreStack>>, names: &[&str])
499 -> Option<Arc<GitignoreStack>>
500 {
501 if names.is_empty() {
502 return parent;
503 }
504
505 let mut builder = ignore::gitignore::GitignoreBuilder::new(dir);
506 for name in names {
507 let _ = builder.add(dir.join(name));
510 }
511
512 match builder.build() {
513 Ok(matcher) if !matcher.is_empty() => Some(Arc::new(GitignoreStack { matcher, parent })),
514 _ => parent
515 }
516 }
517
518 pub(crate) fn extend_with_dir(dir: &Path, parent: Option<Arc<GitignoreStack>>, obeyed: ObeyedIgnoreFiles)
519 -> Option<Arc<GitignoreStack>>
520 {
521 let present = obeyed.get_file_names().filter(|name| dir.join(name).is_file()).collect::<Vec<_>>();
522 Self::extend_with_ignore_files(dir, parent, &present)
523 }
524
525 fn of_ancestors(dir: &Path, obeyed: ObeyedIgnoreFiles) -> Option<Arc<GitignoreStack>> {
527 if dir.join(".git").exists() {
528 return None;
529 }
530
531 let mut relevant_ancestors: Vec<&Path> = Vec::new();
532 for ancestor in dir.ancestors().skip(1) {
533 relevant_ancestors.push(ancestor);
534 if ancestor.join(".git").exists() {
535 break;
536 }
537 }
538
539 let mut stack = None;
540 for ancestor in relevant_ancestors.iter().rev() {
541 stack = Self::extend_with_dir(ancestor, stack, obeyed);
542 }
543 stack
544 }
545
546 pub(crate) fn for_root_dir(dir: &Path, obeyed: ObeyedIgnoreFiles) -> Option<Arc<GitignoreStack>> {
548 if obeyed.obeys_nothing() {
549 return None;
550 }
551 let stack = Self::of_ancestors(dir, obeyed);
552 if let Some(s) = &stack && s.is_ignored(dir, true) {
553 return None;
554 }
555 stack
556 }
557
558 pub(crate) fn is_path_ignored(path: &Path, obeyed: ObeyedIgnoreFiles) -> bool {
560 if obeyed.obeys_nothing() {
561 return false;
562 }
563 let is_dir = path.is_dir();
564 let Some(parent) = path.parent() else { return false };
565
566 let stack = Self::extend_with_dir(parent, Self::of_ancestors(parent, obeyed), obeyed);
567 match stack {
568 Some(x) => x.is_ignored_with_ancestor_dirs(path, is_dir),
569 None => false
570 }
571 }
572
573 fn is_ignored_with_ancestor_dirs(&self, path: &Path, is_dir: bool) -> bool {
576 let mut node = Some(self);
577 while let Some(stack) = node {
578 match stack.matcher.matched_path_or_any_parents(path, is_dir) {
579 ignore::Match::Ignore(_) => return true,
580 ignore::Match::Whitelist(_) => return false,
581 ignore::Match::None => {}
582 }
583 node = stack.parent.as_deref();
584 }
585
586 false
587 }
588
589 pub(crate) fn is_ignored(&self, path: &Path, is_dir: bool) -> bool {
590 let mut node = Some(self);
591 while let Some(stack) = node {
592 match stack.matcher.matched(path, is_dir) {
593 ignore::Match::Ignore(_) => return true,
594 ignore::Match::Whitelist(_) => return false,
595 ignore::Match::None => {}
596 }
597 node = stack.parent.as_deref();
598 }
599
600 false
601 }
602}
603
604fn merge_over_modules(per_module: &[HashMap<String,Stats>]) -> HashMap<String,Stats> {
605 let mut merged = per_module[0].clone();
606 for of_a_module in &per_module[1..] {
607 for (name, stats) in of_a_module {
608 merged.entry(name.clone()).or_default().add(stats);
609 }
610 }
611
612 merged
613}
614
615fn merge_nested_over_modules(nested_by_module: &[HashMap<String, HashMap<String, Stats>>])
616-> HashMap<String, HashMap<String, Stats>> {
617 let mut merged: HashMap<String, HashMap<String, Stats>> = HashMap::new();
618 for bucket in nested_by_module {
619 for (shell_name, sections) in bucket {
620 let shell_entry = merged.entry(shell_name.clone()).or_default();
621 for (inner_name, stats) in sections {
622 shell_entry.entry(inner_name.clone()).or_default().add(stats);
623 }
624 }
625 }
626
627 merged
628}
629
630pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
633 if let Some(text) = payload.downcast_ref::<&'static str>() {
634 (*text).to_owned()
635 } else if let Some(text) = payload.downcast_ref::<String>() {
636 text.clone()
637 } else {
638 "a worker died with a panic payload that is not text".to_owned()
639 }
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645
646 #[test]
649 fn every_language_gets_a_bucket_in_every_module() {
650 let languages = languages_claiming(&[("Rust", &["rs"]), ("Go", &["go"]), ("Zig", &["zig"])]);
651 let modules = Modules::of(&[crate::engine::config::Target::named("backend", "./api"),
652 crate::engine::config::Target::named("frontend", "./web"),
653 crate::engine::config::Target::of("./docs")]);
654 assert_eq!(3, modules.count());
655
656 let stats = make_language_stats(&languages, modules.count());
657 assert_eq!(modules.count(), stats.len());
658
659 for (id, of_a_module) in stats.iter().enumerate() {
660 for name in languages.keys() {
661 assert!(of_a_module.contains_key(name), "'{name}' has no bucket in module {id}");
662 }
663 }
664 }
665
666 #[test]
667 fn the_total_is_the_languages_added_together() {
668 let counted = |code, comments| LineClasses {
669 words_in_code: code, words_in_comment: comments, ..Default::default() };
670 let languages = hashmap![
671 "a".to_owned() => Stats::new(20, 100_000, 2000, counted(1400, 100), hashmap!["classes".to_owned() => 7]),
672 "b".to_owned() => Stats::new(10, 50_000, 1000, counted(800, 50), hashmap!["classes".to_owned() => 2]),
673 "c".to_owned() => Stats::new(10, 50_000, 1000, counted(800, 50), hashmap!["structs".to_owned() => 5])
674 ];
675 let total = Stats::total_of(&languages);
676
677 assert_eq!(40, total.files);
678 assert_eq!(200_000, total.bytes);
679 assert_eq!(4000, total.lines);
680 assert_eq!(3000, total.calculate_code_lines(CountingModel::Content));
681 assert_eq!(200, total.calculate_comment_lines(CountingModel::Content));
682 assert_eq!(800, total.calculate_extra_lines(CountingModel::Content));
683 assert_eq!(Some(&9), total.keyword_occurences.get("classes"));
685 assert_eq!(Some(&5), total.keyword_occurences.get("structs"));
686
687 assert_eq!(0, Stats::total_of(&HashMap::new()).files);
689 }
690}
691
692#[cfg(test)]
695mod worker_death_tests {
696 use crate::{EngineConfig, Languages, RunError, run, run_watched};
697
698 fn corpus(name: &str) -> (std::path::PathBuf, EngineConfig) {
699 let root = std::env::temp_dir().join(name);
700 let _ = std::fs::remove_dir_all(&root);
701 std::fs::create_dir_all(&root).unwrap();
702 std::fs::write(root.join("a.rs"), "fn a() { let x = 1; }\n").unwrap();
703 let config = EngineConfig {
704 threads: crate::Threads::new(2, 2),
705 ..EngineConfig::new([root.to_string_lossy().replace('\\', "/")])
706 };
707 (root, config)
708 }
709
710 fn languages_for(config: &EngineConfig) -> Languages {
711 let languages = crate::language_file::parse_languages_in_dir(crate::test_paths::LANGUAGES_DIR).unwrap().0;
712 Languages::resolve(config, languages, &Default::default()).0
713 }
714
715 #[test]
716 fn a_dead_consumer_is_an_error_and_not_a_short_count() {
717 let (root, config) = corpus("mezura-dead-consumer");
718
719 let err = run(&config, languages_for(&config));
720 std::fs::remove_dir_all(&root).unwrap();
721 let (clean_root, clean_config) = corpus("mezura-alive-consumer");
722 let clean = run(&clean_config, languages_for(&clean_config));
723 std::fs::remove_dir_all(&clean_root).unwrap();
724
725 let err = err.expect_err("a consumer died and run returned a result anyway");
726 assert!(matches!(&err, RunError::IncompleteRun { worker_panic } if worker_panic.contains("test-induced consumer panic")),
727 "got: {err:?}");
728 assert_eq!(1, clean.unwrap().total.files);
730 }
731
732 #[test]
733 fn a_dead_producer_is_an_error_and_the_run_still_terminates() {
734 let (root, config) = corpus("mezura-dead-producer");
735
736 let err = run(&config, languages_for(&config));
737 std::fs::remove_dir_all(&root).unwrap();
738 let (clean_root, clean_config) = corpus("mezura-alive-producer");
739 let clean = run(&clean_config, languages_for(&clean_config));
740 std::fs::remove_dir_all(&clean_root).unwrap();
741
742 let err = err.expect_err("a producer died and run returned a result anyway");
743 assert!(matches!(&err, RunError::IncompleteRun { worker_panic } if worker_panic.contains("test-induced producer panic")),
744 "got: {err:?}");
745 assert_eq!(1, clean.unwrap().total.files);
746 }
747
748 #[test]
753 fn a_walk_whose_own_thread_died_is_never_announced() {
754 let (root, config) = corpus("mezura-dead-producer-announce");
755 let mut announced = Vec::new();
756 let outcome = run_watched(&config, languages_for(&config), None, |scan| announced.push(scan));
757 std::fs::remove_dir_all(&root).unwrap();
758
759 assert!(matches!(&outcome, Err(RunError::IncompleteRun { .. })), "got: {outcome:?}");
761 assert!(announced.is_empty(), "a walk that lost a thread was announced anyway: {announced:?}");
762
763 let (clean_root, clean_config) = corpus("mezura-alive-producer-announce");
765 let mut announced = Vec::new();
766 let clean = run_watched(&clean_config, languages_for(&clean_config), None, |scan| announced.push(scan));
767 std::fs::remove_dir_all(&clean_root).unwrap();
768 assert_eq!(1, clean.unwrap().total.files);
769 assert_eq!(1, announced.len(), "an intact walk was not announced");
770 }
771
772 #[test]
778 fn a_callback_that_finishes_before_the_counting_takes_nothing_off_the_duration() {
779 const SLEPT_PER_FILE : u128 = 40;
780 const FILES : u128 = 10;
781 let callback_holds = std::time::Duration::from_millis(150);
782
783 let root = std::env::temp_dir().join("mezura-slow-consumer-clock");
784 let _ = std::fs::remove_dir_all(&root);
785 std::fs::create_dir_all(&root).unwrap();
786 for i in 0..FILES {
787 std::fs::write(root.join(format!("f{i}.rs")), "fn a() { let x = 1; }\n").unwrap();
788 }
789 let config = EngineConfig {
792 threads: crate::Threads::new(1, 1),
793 ..EngineConfig::new([root.to_string_lossy().replace('\\', "/")])
794 };
795
796 let counted = run_watched(&config, languages_for(&config), None,
797 |_| std::thread::sleep(callback_holds)).unwrap();
798 std::fs::remove_dir_all(&root).unwrap();
799
800 assert_eq!(FILES as usize, counted.total.files);
801 let counting_took = SLEPT_PER_FILE * FILES;
802 assert!(counted.performance.duration_millis >= counting_took,
805 "{} ms of counting under a callback that held {} ms was reported as {} ms, so the \
806 callback was taken off a run it never delayed", counting_took,
807 callback_holds.as_millis(), counted.performance.duration_millis);
808 }
809}