mezura-core 1.1.1

The fast, multithreaded counting engine behind mezura: identifies each file's language, splits every line into code, comments and everything else, and counts user-defined keywords like classes and structs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
use std::fs;
use std::fs::DirEntry;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::thread::JoinHandle;
use std::time::Duration;

use crossbeam_deque::{Injector, Steal, Stealer, Worker};

use crate::{EngineConfig, FilesPresent, GitignoreStack, ObeyedIgnoreFiles, ParsableFile, ScanProgress,
        SharedModuleLookups, TraversedDir, UnreadableDirDetails};
use crate::engine::identity::ModuleLookups;
use crate::engine::modules::{ModuleId, Modules};

// A panic is caught here rather than read back from 'join', because these threads stop by counting
// how many of them have gone idle against how many started: one that dies without ever going idle
// makes that count unreachable and the rest wait on it forever. The catch marks the dead one idle on
// its way out and records what killed it, which 'run' turns into an error after the joins.
pub(crate) fn start_producer_thread(id: usize, files_injector: Arc<Injector<ParsableFile>>, dirs_injector: Arc<Injector<TraversedDir>>, worker: Worker<TraversedDir>,
        stealers: Arc<Vec<Stealer<TraversedDir>>>, idle_producers: Arc<AtomicUsize>, language_lookups: SharedModuleLookups,
        exclude_matcher: Arc<globset::GlobSet>,
        config: Arc<EngineConfig>, files_stats: Arc<Mutex<FilesPresent>>, modules: Arc<Modules>,
        unreadable_dirs: Arc<Mutex<Vec<UnreadableDirDetails>>>, producers_total: Arc<AtomicUsize>,
        worker_panics: Arc<Mutex<Vec<String>>>, progress: Arc<ScanProgress>)
-> std::io::Result<JoinHandle<()>>
{
    thread::Builder::new().name(format!("producer-{id}")).spawn(move || {
        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(||
                search_for_files(id, files_injector, dirs_injector, worker, &stealers, idle_producers.clone(),
                        language_lookups, exclude_matcher, config, modules, &producers_total, &progress)));
        match outcome {
            Ok((found, unreadable)) => {
                if !unreadable.is_empty() {
                    unreadable_dirs.lock().unwrap().extend(unreadable);
                }
                let mut file_stats_guard = files_stats.lock().unwrap();
                file_stats_guard.total_files += found.total_files;
                file_stats_guard.relevant_files += found.relevant_files;
                file_stats_guard.excluded_files += found.excluded_files;
            },
            Err(payload) => {
                worker_panics.lock().unwrap().push(crate::panic_message(payload.as_ref()));
                idle_producers.fetch_add(1, Ordering::SeqCst);
            }
        }
    })
}

fn search_for_files(id: usize, files_injector: Arc<Injector<ParsableFile>>, dirs_injector: Arc<Injector<TraversedDir>>, worker: Worker<TraversedDir>,
        stealers: &[Stealer<TraversedDir>], idle_producers: Arc<AtomicUsize>,
        language_lookups: SharedModuleLookups, exclude_matcher: Arc<globset::GlobSet>, config: Arc<EngineConfig>, modules: Arc<Modules>,
        producers_total: &AtomicUsize, progress: &ScanProgress)
-> (FilesPresent, Vec<UnreadableDirDetails>)
{
    let mut files_present = FilesPresent::default();
    let mut unreadable_dirs = Vec::new();
    let mut should_terminate = false;

    loop {
        // Newest of its own subdirectories first, so a thread finishes the branch it is in before it
        // starts another.
        let next_dir = worker.pop().or_else(|| steal_a_dir(id, &dirs_injector, stealers, &worker));

        if let Some(dir) = &next_dir {
            if should_terminate {
                should_terminate = false;
                idle_producers.fetch_sub(1, Ordering::SeqCst);
            }
            // The twin of the hook in the consumer, keyed on a name for the same reason: the walk
            // tests below drive this loop directly and in parallel, so shared state would be a race.
            // Below the reset above, or a thread that had already counted itself idle is counted
            // again by the handler that catches this, and the survivors see one dead thread as two.
            #[cfg(test)]
            if dir.path.to_string_lossy().contains("mezura-dead-producer") {
                panic!("test-induced producer panic");
            }

            match fs::read_dir(&dir.path) {
                Ok(entries) => {
                    let entries = entries.flatten().collect::<Vec<_>>();
                    let present = find_ignore_files_among(&entries, ObeyedIgnoreFiles::of(&config));
                    let gitignore_stack = GitignoreStack::extend_with_ignore_files(&dir.path,
                            dir.gitignore_stack.clone(), &present);
                    traverse_dir(&files_injector, entries, &worker, &language_lookups, &exclude_matcher, &gitignore_stack,
                            &config, &modules, dir.module, &dir.path, &mut files_present, progress)
                },
                // Everything under it goes uncounted and reaches no total, not even the number of
                // files looked at, and nothing else would say so. The reason travels with the path,
                // or a permission and a directory that went away arrive as the same sentence.
                Err(error) => unreadable_dirs.push(UnreadableDirDetails {
                    path: crate::engine::targets::normalise_separators(&dir.path.to_string_lossy()).into_owned(),
                    error_msg: error.to_string()
                })
            }
        } else {
            if !should_terminate {
                should_terminate = true;
                idle_producers.fetch_add(1, Ordering::SeqCst);
            }
            if idle_producers.load(Ordering::SeqCst) == producers_total.load(Ordering::SeqCst) {
                break;
            }

            thread::sleep(Duration::from_micros(50));
        }
    }

    (files_present, unreadable_dirs)
}

fn steal_a_dir(id: usize, dirs_injector: &Injector<TraversedDir>, stealers: &[Stealer<TraversedDir>],
        worker: &Worker<TraversedDir>)
-> Option<TraversedDir>
{
    loop {
        match dirs_injector.steal_batch_and_pop(worker) {
            Steal::Success(dir) => return Some(dir),
            Steal::Retry => thread::yield_now(),
            Steal::Empty => break
        }
    }

    for (at, stealer) in stealers.iter().enumerate() {
        if at == id { continue; }
        if let Steal::Success(dir) = stealer.steal_batch_and_pop(worker) {
            return Some(dir);
        }
    }

    None
}

fn find_ignore_files_among(entries: &[DirEntry], obeyed: ObeyedIgnoreFiles) -> Vec<&'static str> {
    let mut present = Vec::new();
    for entry in entries {
        let file_name = entry.file_name();
        // Matched without regard to case wherever the filesystem ignores it, since the name the
        // listing spells is the same file there and the directory scan obeyed it above this level.
        let Some(name) = obeyed.get_file_names().find(|name| match cfg!(any(windows, target_os = "macos")) {
            true => file_name.as_encoded_bytes().eq_ignore_ascii_case(name.as_bytes()),
            false => file_name == **name
        }) else { continue };
        // A listing says a symbolic link is a link and not what it points at, and an ignore file
        // reached through one is still read, so that case alone asks the disk.
        let is_a_file = entry.file_type().is_ok_and(|file_type| match file_type.is_symlink() {
            true => entry.path().is_file(),
            false => file_type.is_file()
        });
        if is_a_file {
            present.push(name);
        }
    }

    // Back into the order they overrule each other in, which a listing has no reason to hold them in
    obeyed.get_file_names().filter(|name| present.contains(name)).collect()
}

// 'module' is decided when the directory is queued and its entries inherit it. The two lookups below
// only happen in a run with a target inside another target.
fn traverse_dir(files_injector: &Injector<ParsableFile>, entries: Vec<DirEntry>, dirs_worker: &Worker<TraversedDir>,
        language_lookups: &ModuleLookups, exclude_matcher: &globset::GlobSet, gitignore_stack: &Option<Arc<GitignoreStack>>,
        config: &EngineConfig, modules: &Modules, module: ModuleId, dir_path: &Path,
        files_present: &mut FilesPresent, progress: &ScanProgress)
{
    let mut local_total_files = 0;
    let mut local_relevant_files = 0;
    let mut local_excluded_files = 0;
    let (dir_boundaries, file_boundaries) = (modules.has_dir_boundaries(), modules.has_file_boundaries());
    for e in entries {
        if let Ok(ft) = e.file_type() {
            // A link is where files already counted elsewhere get counted again. Tested before the
            // two arms below and not inside them, because the second is reached by everything that
            // is not a file: on Windows a junction answers no to both 'is_file' and 'is_dir', and a
            // link to one file lands there too, fails to open and vanishes without a word. A target
            // named explicitly is still followed; only what the scan finds by itself must not
            // double back.
            if ft.is_symlink() {
                continue;
            }
            if ft.is_file() {
                local_total_files += 1;
                let file_name = e.file_name();
                let name = Path::new(&file_name);
                // Which module the file is in is settled before its language is named, and not
                // after, because a module can be given rules of its own: a file that is a target
                // itself, sitting inside a directory target of another module, would otherwise be
                // identified by the rules of the module it is only passing through.
                let of_module = file_boundaries.then(|| dir_path.join(&file_name));
                let module = match &of_module {
                    Some(path) => modules.at_file(path, module),
                    None => module
                };
                let language_lookup = language_lookups.get_of_module(module);
                let claimed = language_lookup.of_name(name);
                if claimed.is_none() && !language_lookup.needs_a_shebang_probe(name) {
                    continue;
                }
                let path_buf = of_module.unwrap_or_else(|| dir_path.join(&file_name));
                // The ignore checks sit between the name lookup and the probe, so a covered file
                // is never opened. Only a claimed file counts as excluded; an unclaimed candidate
                // was never identified, so it stays in the uncounted remainder.
                if (!exclude_matcher.is_empty() && exclude_matcher.is_match(&path_buf))
                        || gitignore_stack.as_ref().is_some_and(|stack| stack.is_ignored(&path_buf, false)) {
                    if claimed.is_some() {
                        local_excluded_files += 1;
                    }
                    continue;
                }
                let Some(lang_name) = claimed.or_else(|| language_lookup.of_shebang(&path_buf)) else {
                    continue;
                };
                local_relevant_files += 1;
                // Free on Windows, where the directory listing carries it, and one call per file elsewhere.
                let size = e.metadata().map_or(0, |m| m.len());
                files_injector.push(ParsableFile::new(path_buf, lang_name, module, size)
                        .with_extension_rules(language_lookup.find_extension_rules(name)));
                progress.record_file_found();
            } else {
                // Read lossily, and only to ask whether it is dotted, which a lossy reading answers
                // correctly since a leading '.' is ASCII and survives any replacement. Demanding
                // valid UTF-8 skips the whole directory over a name used for nothing else.
                let file_name = e.file_name();
                let dir_name = file_name.to_string_lossy();
                // '--search-in-dotted' opens the directories somebody made, and git's object database
                // is not one: nothing in it is source. Tested by name at every depth, so a submodule
                // or a nested clone is covered too.
                if dir_name == ".git" { continue; }
                if !config.should_search_in_dotted && dir_name.starts_with('.') { continue; }

                let pathbuf = dir_path.join(&file_name);
                if !exclude_matcher.is_empty() && exclude_matcher.is_match(&pathbuf) {
                    continue;
                }
                if let Some(stack) = gitignore_stack && stack.is_ignored(&pathbuf, true) {
                    continue;
                }
                let module = if dir_boundaries {modules.at_dir(&pathbuf, module)} else {module};
                dirs_worker.push(TraversedDir::new(pathbuf, gitignore_stack.clone(), module));
            }
        }
    }

    files_present.total_files += local_total_files;
    files_present.relevant_files += local_relevant_files;
    files_present.excluded_files += local_excluded_files;
}

// These drive the traversal directly instead of going through 'run': what they are about is what the
// walk queued and under which module, which a result has already folded into buckets.
#[cfg(test)]
mod tests {
    use std::path::{Path, PathBuf};

    use super::*;
    use crate::engine::config::Target;
    use crate::engine::targets::build_exclude_matcher;
    use crate::queue_the_targets;
    use crate::test_paths::LANGUAGES_DIR;
    use crate::engine::identity::{ClaimKind, LanguageLookup, ModuleLookups, build_extension_language_map,
            build_language_map_by};

    fn count_files_of(target: &str, extra_args: &str) -> (usize, usize, usize, Vec<String>) {
        let (total, relevant, excluded, found) = walk(target, extra_args);
        (total, relevant, excluded, found.into_iter().map(|(name, _)| name).collect())
    }

    fn walk(target: &str, extra_args: &str) -> (usize, usize, usize, Vec<(String, Option<String>)>) {
        // The rule the real parser applies: whitespace separates one target from the next only once a
        // module has been named, so a path with a space in it survives while nothing is named.
        let declares_a_module = target.contains('=');
        let pieces = if declares_a_module {target.split_whitespace().collect::<Vec<_>>()}
                else {target.split(',').collect::<Vec<_>>()};
        let declared = pieces.into_iter().map(str::trim).filter(|x| !x.is_empty())
                .map(|piece| match piece.split_once('=').filter(|_| declares_a_module) {
                    Some((name, path)) => Target::named(name.trim(), path.trim()),
                    None => Target::of(piece)
                }).collect::<Vec<_>>();
        let config = EngineConfig {
            targets: declared,
            threads: crate::Threads::new(1, 1),
            no_gitignore: extra_args.contains("--no-gitignore"),
            no_ignore_files: extra_args.contains("--no-ignore-files"),
            should_search_in_dotted: extra_args.contains("--search-in-dotted"),
            ..Default::default()
        };
        // The same first step 'run' takes, with the flags the walk is about to obey
        let targets = crate::engine::targets::resolve(&config.targets, crate::ObeyedIgnoreFiles::of(&config),
                config.should_search_in_dotted).unwrap();
        let config = Arc::new(config);
        let language_map = Arc::new(crate::languages::keyed_by_name(
                crate::language_file::parse_languages_in_dir(LANGUAGES_DIR).unwrap().0));
        let files_injector = Arc::new(Injector::new());
        let dirs_injector = Arc::new(Injector::new());
        let idle_producers = Arc::new(AtomicUsize::new(0));
        let language_lookups: SharedModuleLookups = Arc::new(ModuleLookups::OfTheWholeRun(LanguageLookup {
                        by_extension: build_extension_language_map(&language_map, &Default::default(), &Default::default()).0,
                        by_shebang: build_language_map_by(ClaimKind::Shebang, &language_map, &Default::default(), &Default::default()).0,
                        ..Default::default() }));
        let modules = Arc::new(Modules::of(&targets));
        let mut files_present = FilesPresent::default();
        queue_the_targets(&config, &targets, &dirs_injector, &files_injector, &mut files_present, &language_lookups, &modules,
                &ScanProgress::default());

        let exclude_matcher = Arc::new(build_exclude_matcher(&config.exclude_dirs).unwrap());
        let (found, _) = search_for_files(0, files_injector.clone(), dirs_injector,
                Worker::new_lifo(), &[], idle_producers, language_lookups, exclude_matcher, config, modules.clone(),
                &AtomicUsize::new(1), &ScanProgress::default());

        let mut found_files = Vec::new();
        while let Steal::Success(f) = files_injector.steal() {
            found_files.push((f.path.file_name().unwrap().to_str().unwrap().to_owned(),
                    modules.name_of(f.module).map(str::to_owned)));
        }
        found_files.sort();

        (found.total_files, found.relevant_files, found.excluded_files, found_files)
    }

    // Reproduced with a queued path that does not exist, which fails in 'read_dir' the same way a
    // directory deleted or made unreadable between being queued and being opened does.
    #[test]
    fn a_directory_that_cannot_be_read_is_reported_and_not_silently_dropped() {
        let root = std::env::temp_dir().join("mezura_unreadable_dir_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(&root).unwrap();
        fs::write(root.join("a.rs"), "fn main() {}\n").unwrap();
        let root_str = root.to_str().unwrap().replace('\\', "/");
        let vanished = format!("{root_str}/gone");

        let config = EngineConfig { threads: crate::Threads::new(1, 1), ..EngineConfig::new([&root_str]) };
        let targets = crate::engine::targets::resolve(&config.targets, crate::ObeyedIgnoreFiles::of(&config),
                config.should_search_in_dotted).unwrap();
        let config = Arc::new(config);
        let language_map = Arc::new(crate::languages::keyed_by_name(
                crate::language_file::parse_languages_in_dir(LANGUAGES_DIR).unwrap().0));
        let language_lookups: SharedModuleLookups = Arc::new(ModuleLookups::OfTheWholeRun(
                LanguageLookup { by_extension: build_extension_language_map(&language_map, &Default::default(), &Default::default()).0,
                        ..Default::default() }));
        let modules = Arc::new(Modules::of(&targets));
        let (files_injector, dirs_injector) = (Arc::new(Injector::new()), Arc::new(Injector::new()));
        let mut files_present = FilesPresent::default();
        queue_the_targets(&config, &targets, &dirs_injector, &files_injector,
                &mut files_present, &language_lookups, &modules, &ScanProgress::default());
        dirs_injector.push(TraversedDir::new(std::path::PathBuf::from(&vanished), None, 0));

        let exclude_matcher = Arc::new(build_exclude_matcher(&config.exclude_dirs).unwrap());
        let (found, unreadable) = search_for_files(0, files_injector, dirs_injector,
                Worker::new_lifo(), &[], Arc::new(AtomicUsize::new(0)), language_lookups, exclude_matcher,
                config, modules, &AtomicUsize::new(1), &ScanProgress::default());

        fs::remove_dir_all(&root).unwrap();

        assert_eq!((1, 1), (found.total_files, found.relevant_files));
        assert_eq!(vec![vanished], unreadable.iter().map(|x| x.path.clone()).collect::<Vec<_>>(),
                "the directory that could not be read went unreported");
        assert!(!unreadable[0].error_msg.is_empty(), "the reason it could not be read was dropped");
    }

    #[test]
    fn the_git_directory_is_never_walked_even_when_dotted_ones_are() {
        let root = std::env::temp_dir().join("mezura_git_skip_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join(".git").join("hooks")).unwrap();
        fs::create_dir_all(root.join("sub").join(".git")).unwrap();
        fs::create_dir_all(root.join(".github")).unwrap();
        fs::write(root.join("a.rs"), "fn main() {}
").unwrap();
        fs::write(root.join(".git").join("hooks").join("pre.rs"), "fn main() {}
").unwrap();
        fs::write(root.join("sub").join(".git").join("nested.rs"), "fn main() {}
").unwrap();
        fs::write(root.join(".github").join("deploy.rs"), "fn main() {}
").unwrap();
        let root_str = root.to_str().unwrap().replace('\\', "/");

        let (_, _, _, found) = count_files_of(&root_str, "");
        assert_eq!(vec!["a.rs"], found, "the dotted rule alone should have kept all three out");

        // The flag opens the ones somebody created, at either depth, and still not the one git keeps
        let (_, _, _, found) = count_files_of(&root_str, "--search-in-dotted");
        assert_eq!(vec!["a.rs", "deploy.rs"], found, "the object database was walked");

        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn overlapping_and_globbed_targets_count_every_file_once() {
        let root = std::env::temp_dir().join("mezura_overlap_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("sub").join("deep")).unwrap();
        fs::write(root.join("a.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("sub").join("b.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("sub").join("deep").join("c.rs"), "fn main() {}\n").unwrap();
        let root = root.to_str().unwrap().replace('\\', "/");

        let (_, _, _, found_files) = count_files_of(&root, "");
        assert_eq!(vec!["a.rs", "b.rs", "c.rs"], found_files);

        let (_, _, _, found_files) = count_files_of(&format!("{root},{root}/sub,{root}/sub/deep"), "");
        assert_eq!(vec!["a.rs", "b.rs", "c.rs"], found_files);

        let (_, _, _, found_files) = count_files_of(&format!("{root}/sub/deep,{root}/sub"), "");
        assert_eq!(vec!["b.rs", "c.rs"], found_files);

        let (_, _, _, found_files) = count_files_of(&format!("{root}/*,{root}/**/*.rs"), "");
        assert_eq!(vec!["a.rs", "b.rs", "c.rs"], found_files);

        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn every_file_is_attributed_to_exactly_one_module() {
        let root = std::env::temp_dir().join("mezura_modules_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("api").join("tests").join("deep")).unwrap();
        fs::create_dir_all(root.join("web")).unwrap();
        fs::create_dir_all(root.join("loose")).unwrap();
        fs::write(root.join("api").join("server.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("api").join("tests").join("api_test.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("api").join("tests").join("deep").join("nested_test.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("web").join("app.js"), "let x = 1;\n").unwrap();
        fs::write(root.join("loose").join("script.py"), "x = 1\n").unwrap();
        let root = root.to_str().unwrap().replace('\\', "/");

        let of = |name: &str| Some(name.to_owned());
        let (_, _, _, found) = walk(&format!("backend={root}/api frontend={root}/web"), "");
        assert_eq!(vec![("api_test.rs".to_owned(), of("backend")), ("app.js".to_owned(), of("frontend")),
                        ("nested_test.rs".to_owned(), of("backend")), ("server.rs".to_owned(), of("backend"))], found);

        // The nested target is not walked a second time, and its files still leave the module around it
        let (_, _, _, found) = walk(&format!("backend={root}/api tests={root}/api/tests"), "");
        assert_eq!(vec![("api_test.rs".to_owned(), of("tests")), ("nested_test.rs".to_owned(), of("tests")),
                        ("server.rs".to_owned(), of("backend"))], found);

        // An unnamed target next to a named one keeps everything it holds, minus what the named one took
        let (_, _, _, found) = walk(&format!("{root} tests={root}/api/tests"), "");
        assert_eq!(vec![("api_test.rs".to_owned(), of("tests")), ("app.js".to_owned(), None),
                        ("nested_test.rs".to_owned(), of("tests")), ("script.py".to_owned(), None),
                        ("server.rs".to_owned(), None)], found);

        // A file named on its own is a boundary like any other
        let (_, _, _, found) = walk(&format!("{root}/api entry={root}/api/server.rs"), "");
        assert_eq!(vec![("api_test.rs".to_owned(), None), ("nested_test.rs".to_owned(), None),
                        ("server.rs".to_owned(), of("entry"))], found);

        // and the order the two were written in changes nothing, since the more specific path wins
        let (_, _, _, found) = walk(&format!("tests={root}/api/tests backend={root}/api"), "");
        assert_eq!(vec![("api_test.rs".to_owned(), of("tests")), ("nested_test.rs".to_owned(), of("tests")),
                        ("server.rs".to_owned(), of("backend"))], found);

        fs::remove_dir_all(&root).unwrap();
    }

    // A junction needs no privilege, where a real symbolic link on Windows does, so it is what the
    // test makes there. It is also the harder of the two: it answers no to both 'is_file' and 'is_dir'.
    #[cfg(windows)]
    fn link_dir(original: &Path, link: &Path) {
        let output = std::process::Command::new("cmd")
                .args(["/C", "mklink", "/J", &link.to_string_lossy(), &original.to_string_lossy()])
                .output().expect("mklink is part of the shell on every Windows");
        assert!(link.exists(), "could not create a junction: {}", String::from_utf8_lossy(&output.stderr));
    }

    #[cfg(unix)]
    fn link_dir(original: &Path, link: &Path) {
        std::os::unix::fs::symlink(original, link).unwrap();
    }

    #[test]
    fn a_link_found_during_the_walk_is_not_followed_but_one_that_was_asked_for_is() {
        let root = std::env::temp_dir().join("mezura_symlink_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("real")).unwrap();
        fs::write(root.join("real").join("a.rs"), "fn main() {}\nlet x = 1;\n").unwrap();
        link_dir(&root.join("real"), &root.join("linked"));

        let root_str = root.to_str().unwrap().replace('\\', "/");
        let (_, relevant, _, found) = count_files_of(&root_str, "");
        assert_eq!(1, relevant, "the file under the link was counted a second time");
        assert_eq!(vec!["a.rs"], found);

        // and the link named as the target is walked, since that is what was asked for
        let (_, relevant, _, found) = count_files_of(&format!("{root_str}/linked"), "");
        assert_eq!(1, relevant);
        assert_eq!(vec!["a.rs"], found);

        // A pattern is not a name: what it matched was found by the program, which is already why a
        // match that a .gitignore ignores is dropped.
        let (_, relevant, _, found) = count_files_of(&format!("{root_str}/*"), "");
        assert_eq!(1, relevant, "the link was reached through a pattern and counted a second time");
        assert_eq!(vec!["a.rs"], found);

        // A link to a single file would otherwise take the directory arm, fail to open and disappear
        #[cfg(unix)]
        {
            std::os::unix::fs::symlink(root.join("real").join("a.rs"), root.join("real").join("b.rs")).unwrap();
            let (_, relevant, _, found) = count_files_of(&root_str, "");
            assert_eq!(1, relevant);
            assert_eq!(vec!["a.rs"], found);
        }

        fs::remove_dir_all(&root).unwrap();
    }

    // The fixtures sit under the temporary directory, whose path carries the account name, so an
    // account with a space in it is the ordinary way in. Naming a module changes the rule and needs
    // the path quoted, which is the parser's job and is tested where the parser lives.
    #[test]
    fn a_target_whose_path_contains_a_space_is_one_target() {
        let root = std::env::temp_dir().join("mezura space test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join("sub")).unwrap();
        fs::write(root.join("a.rs"), "fn main() {}
").unwrap();
        fs::write(root.join("sub").join("b.rs"), "fn main() {}
").unwrap();
        let root = root.to_str().unwrap().replace('\\', "/");

        let (_, relevant, _, found) = count_files_of(&root, "");
        assert_eq!(2, relevant, "a path with a space in it was read as two targets");
        assert_eq!(vec!["a.rs", "b.rs"], found);

        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn an_extensionless_script_is_claimed_through_its_first_line() {
        let root = std::env::temp_dir().join("mezura_shebang_walk_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join(".git")).unwrap();
        fs::write(root.join(".gitignore"), "ignored-deploy\n").unwrap();
        fs::write(root.join("deploy"), "#!/usr/bin/env bash\necho hi\n").unwrap();
        fs::write(root.join("ignored-deploy"), "#!/bin/sh\necho hi\n").unwrap();
        fs::write(root.join("LICENSE"), "MIT License\n").unwrap();
        fs::write(root.join("script.xyz"), "#!/bin/sh\necho hi\n").unwrap();
        fs::write(root.join("a.rs"), "fn main() {}\n").unwrap();
        let root_str = root.to_str().unwrap().replace('\\', "/");

        let (total, relevant, excluded, found) = count_files_of(&root_str, "");
        assert_eq!(vec!["a.rs", "deploy"], found, "the shebang did not claim the script");
        // 'LICENSE', '.gitignore', the '.xyz' carrying a shebang and the ignored script all stay in
        // the remainder: an extension keeps a file out of the probe whatever its first line says,
        // and a file the ignore checks cover was never identified, so it is not 'excluded' either
        assert_eq!((6, 2, 0), (total, relevant, excluded));

        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn what_a_gitignore_names_is_left_out_of_the_walk() {
        let root = std::env::temp_dir().join("mezura_gitignore_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join(".git")).unwrap();
        fs::create_dir_all(root.join("ignored_dir")).unwrap();
        fs::create_dir_all(root.join("sub")).unwrap();
        fs::write(root.join(".gitignore"), "*.py\nignored_dir/\n!keep.py\n").unwrap();
        fs::write(root.join("a.py"), "x = 1\n").unwrap();
        fs::write(root.join("keep.py"), "x = 1\n").unwrap();
        fs::write(root.join("b.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("ignored_dir").join("c.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("sub").join(".gitignore"), "*.rs\n").unwrap();
        fs::write(root.join("sub").join("d.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("sub").join("e.py"), "x = 1\n").unwrap();

        let root_str = root.to_str().unwrap().replace('\\', "/");

        let (total, relevant, excluded, found_files) = count_files_of(&root_str, "");
        assert_eq!((7, 2, 3), (total, relevant, excluded));
        assert_eq!(vec!["b.rs", "keep.py"], found_files);

        let (total, relevant, excluded, found_files) = count_files_of(&root_str, "--no-gitignore");
        assert_eq!((8, 6, 0), (total, relevant, excluded));
        assert_eq!(vec!["a.py", "b.rs", "c.rs", "d.rs", "e.py", "keep.py"], found_files);

        let (_, relevant, _, found_files) = count_files_of(&format!("{root_str}/ignored_dir"), "");
        assert_eq!(1, relevant);
        assert_eq!(vec!["c.rs"], found_files);

        fs::remove_dir_all(&root).unwrap();
    }

    // A '.ignore' is what somebody writes to hide a vendored dependency from their search tools
    // while git keeps it, so obeying only the '.gitignore' counts the whole of it. The two flags are
    // separate because the two files answer different questions, and the case that proves they are
    // not one flag is the file each one alone brings back.
    #[test]
    fn the_ignore_files_git_does_not_read_are_obeyed_and_are_turned_off_on_their_own() {
        let root = std::env::temp_dir().join("mezura_ignore_files_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(root.join(".git")).unwrap();
        fs::create_dir_all(root.join("vendor")).unwrap();
        fs::write(root.join(".gitignore"), "built.rs\n").unwrap();
        fs::write(root.join(".ignore"), "vendor/\nbundle.rs\n").unwrap();
        // Last word to the narrowest file: '.rgignore' brings the bundle back over the '.ignore'
        fs::write(root.join(".rgignore"), "!bundle.rs\n").unwrap();
        fs::write(root.join("mine.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("built.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("bundle.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("vendor").join("dep.rs"), "fn main() {}\n").unwrap();

        let root_str = root.to_str().unwrap().replace('\\', "/");

        let (_, _, _, found_files) = count_files_of(&root_str, "");
        assert_eq!(vec!["bundle.rs", "mine.rs"], found_files);

        // Only the search tools' files off: what the '.gitignore' hides is still hidden
        let (_, _, _, found_files) = count_files_of(&root_str, "--no-ignore-files");
        assert_eq!(vec!["bundle.rs", "dep.rs", "mine.rs"], found_files);

        // Only the '.gitignore' off: what the '.ignore' hides is still hidden
        let (_, _, _, found_files) = count_files_of(&root_str, "--no-gitignore");
        assert_eq!(vec!["built.rs", "bundle.rs", "mine.rs"], found_files);

        let (_, _, _, found_files) = count_files_of(&root_str, "--no-gitignore --no-ignore-files");
        assert_eq!(vec!["built.rs", "bundle.rs", "dep.rs", "mine.rs"], found_files);

        fs::remove_dir_all(&root).unwrap();
    }

    #[test]
    fn a_producer_out_of_work_takes_a_directory_from_a_peer_and_never_from_its_own_queue() {
        let (mine, peer) = (Worker::new_lifo(), Worker::new_lifo());
        let stealers = [mine.stealer(), peer.stealer()];
        let peers_dir = PathBuf::from("mezura-a-peers-directory");
        peer.push(TraversedDir::new(peers_dir.clone(), None, 0));

        assert_eq!(Some(peers_dir), steal_a_dir(0, &Injector::new(), &stealers, &mine).map(|dir| dir.path));

        mine.push(TraversedDir::new(PathBuf::from("mezura-my-own-directory"), None, 0));
        assert!(steal_a_dir(0, &Injector::new(), &stealers, &mine).is_none(),
                "a producer stole from its own queue");
    }

    #[test]
    #[cfg(any(windows, target_os = "macos"))]
    fn an_ignore_file_is_obeyed_however_its_name_is_spelled() {
        let root = std::env::temp_dir().join("mezura_ignore_file_case_test");
        let _ = fs::remove_dir_all(&root);
        fs::create_dir_all(&root).unwrap();
        fs::write(root.join(".GITIGNORE"), "hide.rs\n").unwrap();
        fs::write(root.join("hide.rs"), "fn main() {}\n").unwrap();
        fs::write(root.join("keep.rs"), "fn main() {}\n").unwrap();

        let root_str = root.to_str().unwrap().replace('\\', "/");
        let (_, _, _, found_files) = count_files_of(&root_str, "");
        assert_eq!(vec!["keep.rs"], found_files);

        fs::remove_dir_all(&root).unwrap();
    }
}