1use std::collections::VecDeque;
4
5use fixedbitset::FixedBitSet;
6use rustc_hash::{FxHashMap, FxHashSet};
7
8use crate::resolve::ResolvedReplacedModuleTarget;
9use fallow_types::discover::FileId;
10use fallow_types::extract::ModuleLoadMechanism;
11
12use super::{Edge, ModuleGraph, TestReachabilityIndex};
13
14const PROFILE_COUNT_SAFETY_CAP: usize = 1024;
26
27#[derive(Debug, PartialEq, Eq)]
28pub(super) struct TestReachabilityProfile {
29 masked_targets: Vec<FileId>,
30 roots: Vec<FileId>,
31}
32
33pub(super) enum TestReachabilityPlan<'roots> {
34 NoRoots,
35 Legacy {
36 roots: &'roots FxHashSet<FileId>,
37 capped: bool,
40 },
41 Profiled {
42 profiles: Vec<TestReachabilityProfile>,
43 },
44}
45
46impl<'roots> TestReachabilityPlan<'roots> {
47 pub(super) fn new(
48 test_entry_points: &'roots FxHashSet<FileId>,
49 replaced_module_targets: &[ResolvedReplacedModuleTarget],
50 total_capacity: usize,
51 ) -> Self {
52 if test_entry_points.is_empty() {
53 return Self::NoRoots;
54 }
55
56 let mut targets_by_root: FxHashMap<FileId, Vec<FileId>> = FxHashMap::default();
57 for replacement in replaced_module_targets {
58 if test_entry_points.contains(&replacement.source_file)
59 && (replacement.target_file.0 as usize) < total_capacity
60 {
61 targets_by_root
62 .entry(replacement.source_file)
63 .or_default()
64 .push(replacement.target_file);
65 }
66 }
67 for targets in targets_by_root.values_mut() {
68 targets.sort_unstable_by_key(|target| target.0);
69 targets.dedup();
70 }
71
72 if targets_by_root.is_empty() {
73 return Self::Legacy {
74 roots: test_entry_points,
75 capped: false,
76 };
77 }
78
79 let mut roots_by_mask: FxHashMap<Vec<FileId>, Vec<FileId>> = FxHashMap::default();
80 for &root in test_entry_points {
81 let mask = targets_by_root.remove(&root).unwrap_or_default();
82 roots_by_mask.entry(mask).or_default().push(root);
83 }
84
85 if roots_by_mask.len() > PROFILE_COUNT_SAFETY_CAP {
86 tracing::warn!(
87 profile_count = roots_by_mask.len(),
88 safety_cap = PROFILE_COUNT_SAFETY_CAP,
89 "Test-reachability profile count exceeded its safety cap; \
90 falling back to coarse test reachability without replacement \
91 masking. Mocked modules stay test-reachable (fail-open)."
92 );
93 return Self::Legacy {
94 roots: test_entry_points,
95 capped: true,
96 };
97 }
98
99 let mut profiles: Vec<_> = roots_by_mask
100 .into_iter()
101 .map(|(masked_targets, mut roots)| {
102 roots.sort_unstable_by_key(|root| root.0);
103 roots.dedup();
104 TestReachabilityProfile {
105 masked_targets,
106 roots,
107 }
108 })
109 .collect();
110 profiles.sort_by(|left, right| {
111 left.masked_targets
112 .iter()
113 .map(|file_id| file_id.0)
114 .cmp(right.masked_targets.iter().map(|file_id| file_id.0))
115 });
116
117 Self::Profiled { profiles }
118 }
119
120 pub(super) const fn requires_reference_provenance(&self) -> bool {
121 matches!(self, Self::Profiled { .. })
122 }
123}
124
125enum TestReachability {
126 NoRoots,
127 Legacy {
128 reachable: FixedBitSet,
129 capped: bool,
130 },
131 Profiled {
132 reachable: FixedBitSet,
133 index: TestReachabilityIndex,
134 #[cfg(test)]
135 dirty_word_pops: usize,
136 },
137}
138
139impl TestReachability {
140 fn into_parts(self) -> (Option<FixedBitSet>, TestReachabilityIndex) {
141 match self {
142 Self::NoRoots => (None, TestReachabilityIndex::default()),
143 Self::Legacy { reachable, capped } => (
144 Some(reachable),
145 TestReachabilityIndex {
146 masking_capped: capped,
147 ..TestReachabilityIndex::default()
148 },
149 ),
150 Self::Profiled {
151 reachable, index, ..
152 } => (Some(reachable), index),
153 }
154 }
155
156 #[cfg(test)]
157 const fn dirty_word_pop_count(&self) -> usize {
158 match self {
159 Self::Profiled {
160 dirty_word_pops, ..
161 } => *dirty_word_pops,
162 Self::NoRoots | Self::Legacy { .. } => 0,
163 }
164 }
165}
166
167struct ProfileWorklist {
172 index: TestReachabilityIndex,
173 masked_profiles: FxHashMap<FileId, Vec<u64>>,
174 all_reachable: FixedBitSet,
175 pending_profiles: Vec<u64>,
176 queued: FixedBitSet,
177 queue: VecDeque<(FileId, usize)>,
178 dirty_word_pops: usize,
179}
180
181impl ProfileWorklist {
182 fn new(profiles: &[TestReachabilityProfile], total_capacity: usize) -> Self {
183 let profile_count = profiles.len();
184 let mut index = TestReachabilityIndex::new(total_capacity, profile_count);
185 index.profile_roots = profiles
186 .iter()
187 .map(|profile| profile.roots.clone())
188 .collect();
189 let words_per_file = index.words_per_file;
190 let mut masked_profiles: FxHashMap<FileId, Vec<u64>> = FxHashMap::default();
191
192 for (profile, reachability_profile) in profiles.iter().enumerate() {
193 let word_index = profile / u64::BITS as usize;
194 let profile_bit = 1_u64 << (profile % u64::BITS as usize);
195 for &target in &reachability_profile.masked_targets {
196 masked_profiles
197 .entry(target)
198 .or_insert_with(|| vec![0; words_per_file])[word_index] |= profile_bit;
199 }
200 }
201
202 let pending_profiles = vec![0_u64; index.reachable_profiles.len()];
203 let mut worklist = Self {
204 index,
205 masked_profiles,
206 all_reachable: FixedBitSet::with_capacity(total_capacity),
207 pending_profiles,
208 queued: FixedBitSet::with_capacity(total_capacity.saturating_mul(words_per_file)),
209 queue: VecDeque::new(),
210 dirty_word_pops: 0,
211 };
212
213 for (profile, reachability_profile) in profiles.iter().enumerate() {
214 let word_index = profile / u64::BITS as usize;
215 let profile_bit = 1_u64 << (profile % u64::BITS as usize);
216 for &root in &reachability_profile.roots {
217 let root_index = root.0 as usize;
218 if root_index >= total_capacity {
219 continue;
220 }
221 let Some(slot) = root_index
222 .checked_mul(words_per_file)
223 .and_then(|start| start.checked_add(word_index))
224 else {
225 continue;
226 };
227 let Some(reachable_word) = worklist.index.reachable_profiles.get_mut(slot) else {
228 continue;
229 };
230 if *reachable_word & profile_bit != 0 {
231 continue;
232 }
233 *reachable_word |= profile_bit;
234 worklist.pending_profiles[slot] |= profile_bit;
235 worklist.all_reachable.insert(root_index);
236 if !worklist.queued.contains(slot) {
237 worklist.queued.insert(slot);
238 worklist.queue.push_back((root, word_index));
239 }
240 }
241 }
242
243 worklist
244 }
245
246 fn run(mut self, graph: &ModuleGraph) -> (FixedBitSet, TestReachabilityIndex, usize) {
247 let words_per_file = self.index.words_per_file;
248
249 while let Some((file_id, word_index)) = self.queue.pop_front() {
250 self.dirty_word_pops += 1;
251 let file_index = file_id.0 as usize;
252 let Some(source_slot) = file_index
253 .checked_mul(words_per_file)
254 .and_then(|start| start.checked_add(word_index))
255 else {
256 continue;
257 };
258 self.queued.remove(source_slot);
259 let Some(source_delta) = self.pending_profiles.get_mut(source_slot) else {
260 continue;
261 };
262 let delta_word = *source_delta;
263 *source_delta = 0;
264 if delta_word == 0 {
265 continue;
266 }
267
268 let Some(module) = graph.modules.get(file_index) else {
269 continue;
270 };
271 for edge in &graph.edges[module.edge_range.clone()] {
272 let target_idx = edge.target.0 as usize;
273 if target_idx >= self.all_reachable.len() || !edge.runs_target_code_in_tests() {
274 continue;
275 }
276 let Some(target_slot) = target_idx
277 .checked_mul(words_per_file)
278 .and_then(|start| start.checked_add(word_index))
279 else {
280 continue;
281 };
282 let edge_is_esm_only = edge.symbols.iter().all(|symbol| {
283 !matches!(symbol.mechanism, ModuleLoadMechanism::CommonJsRequire)
284 });
285 let edge_mask = if edge_is_esm_only {
286 self.masked_profiles.get(&edge.target)
287 } else {
288 None
289 };
290
291 let propagated = edge_mask
292 .and_then(|mask| mask.get(word_index))
293 .map_or(delta_word, |mask_word| delta_word & !mask_word);
294 let Some(reachable_word) = self.index.reachable_profiles.get_mut(target_slot)
295 else {
296 continue;
297 };
298 let new_profiles = propagated & !*reachable_word;
299 if new_profiles == 0 {
300 continue;
301 }
302 *reachable_word |= new_profiles;
303 self.pending_profiles[target_slot] |= new_profiles;
304
305 self.all_reachable.insert(target_idx);
306 if !self.queued.contains(target_slot) {
307 self.queued.insert(target_slot);
308 self.queue.push_back((edge.target, word_index));
309 }
310 }
311 }
312
313 self.index.set_sparse_masks(self.masked_profiles);
314 (self.all_reachable, self.index, self.dirty_word_pops)
315 }
316}
317
318impl ModuleGraph {
319 pub(super) fn collect_reachable(
325 &self,
326 entry_points: &FxHashSet<FileId>,
327 total_capacity: usize,
328 ) -> FixedBitSet {
329 self.collect_reachable_along(entry_points, total_capacity, Edge::runs_target_code)
330 }
331
332 fn collect_reachable_along(
334 &self,
335 entry_points: &FxHashSet<FileId>,
336 total_capacity: usize,
337 follows: fn(&Edge) -> bool,
338 ) -> FixedBitSet {
339 #[cfg(test)]
340 tests::record_traversal();
341
342 let mut visited = FixedBitSet::with_capacity(total_capacity);
343 let mut queue = VecDeque::new();
344
345 for &ep_id in entry_points {
346 if (ep_id.0 as usize) < total_capacity {
347 visited.insert(ep_id.0 as usize);
348 queue.push_back(ep_id);
349 }
350 }
351
352 while let Some(file_id) = queue.pop_front() {
353 if (file_id.0 as usize) >= self.modules.len() {
354 continue;
355 }
356 let module = &self.modules[file_id.0 as usize];
357 for edge in &self.edges[module.edge_range.clone()] {
358 if !follows(edge) {
362 continue;
363 }
364 let target_idx = edge.target.0 as usize;
365 if target_idx < total_capacity && !visited.contains(target_idx) {
366 visited.insert(target_idx);
367 queue.push_back(edge.target);
368 }
369 }
370 }
371
372 visited
373 }
374
375 fn collect_profiled_reachable(
376 &self,
377 profiles: &[TestReachabilityProfile],
378 total_capacity: usize,
379 ) -> (FixedBitSet, TestReachabilityIndex, usize) {
380 #[cfg(test)]
381 tests::record_traversal();
382
383 ProfileWorklist::new(profiles, total_capacity).run(self)
384 }
385
386 fn collect_test_reachable(
387 &self,
388 plan: TestReachabilityPlan<'_>,
389 total_capacity: usize,
390 ) -> TestReachability {
391 match plan {
392 TestReachabilityPlan::NoRoots => TestReachability::NoRoots,
393 TestReachabilityPlan::Legacy { roots, capped } => TestReachability::Legacy {
394 reachable: self.collect_reachable_along(
395 roots,
396 total_capacity,
397 Edge::runs_target_code_in_tests,
398 ),
399 capped,
400 },
401 TestReachabilityPlan::Profiled { profiles } => {
402 let (reachable, index, dirty_word_pops) =
403 self.collect_profiled_reachable(&profiles, total_capacity);
404 #[cfg(not(test))]
405 let _ = dirty_word_pops;
406
407 TestReachability::Profiled {
408 reachable,
409 index,
410 #[cfg(test)]
411 dirty_word_pops,
412 }
413 }
414 }
415 }
416
417 pub(super) fn mark_reachable(
423 &mut self,
424 visited: &FixedBitSet,
425 entry_points: &FxHashSet<FileId>,
426 runtime_entry_points: &FxHashSet<FileId>,
427 test_reachability_plan: TestReachabilityPlan<'_>,
428 total_capacity: usize,
429 ) {
430 let runtime_same = runtime_entry_points == entry_points;
431 let runtime_visited = if runtime_same {
432 None
433 } else {
434 Some(self.collect_reachable(runtime_entry_points, total_capacity))
435 };
436
437 let requires_reference_provenance = test_reachability_plan.requires_reference_provenance();
438 let (test_visited, test_reachability_index) = self
439 .collect_test_reachable(test_reachability_plan, total_capacity)
440 .into_parts();
441 debug_assert_eq!(
442 requires_reference_provenance,
443 test_reachability_index.profile_count > 0
444 );
445 self.test_reachability_index = test_reachability_index;
446
447 for (idx, module) in self.modules.iter_mut().enumerate() {
448 module.set_reachable(visited.contains(idx));
449 module.set_runtime_reachable(
450 runtime_visited
451 .as_ref()
452 .map_or_else(|| visited.contains(idx), |rv| rv.contains(idx)),
453 );
454 module.set_test_reachable(test_visited.as_ref().is_some_and(|tv| tv.contains(idx)));
455 }
456 }
457
458 #[must_use]
470 pub fn test_entry_points_reaching(&self, files: &[FileId]) -> FxHashMap<FileId, Vec<FileId>> {
471 let mut reaching: FxHashMap<FileId, Vec<FileId>> =
472 files.iter().map(|&file| (file, Vec::new())).collect();
473 let total_capacity = self.modules.len();
474 let mut roots: Vec<FileId> = self
475 .test_entry_points
476 .iter()
477 .copied()
478 .filter(|root| (root.0 as usize) < total_capacity)
479 .collect();
480 if files.is_empty() || roots.is_empty() {
481 return reaching;
482 }
483 roots.sort_unstable_by_key(|root| root.0);
484 let mut masks = self.test_reachability_index.root_masks();
485 for batch in roots.chunks(u64::BITS as usize) {
486 let profiles: Vec<TestReachabilityProfile> = batch
487 .iter()
488 .map(|&root| TestReachabilityProfile {
489 masked_targets: masks.remove(&root).unwrap_or_default(),
490 roots: vec![root],
491 })
492 .collect();
493 let (_, index, _) = ProfileWorklist::new(&profiles, total_capacity).run(self);
494 for (&file, entries) in &mut reaching {
495 let Some(&word) = index
496 .profiles_for(&index.reachable_profiles, file)
497 .and_then(|words| words.first())
498 else {
499 continue;
500 };
501 entries.extend(
502 batch
503 .iter()
504 .enumerate()
505 .filter(|&(bit, _)| word & (1_u64 << bit) != 0)
506 .map(|(_, &root)| root),
507 );
508 }
509 }
510 reaching
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use std::path::PathBuf;
517
518 use rustc_hash::FxHashSet;
519
520 use super::{TestReachabilityPlan, TestReachabilityProfile};
521 use crate::graph::{ExportNamespace, ExportTestReach, ModuleGraph, TestMaskingState};
522 use crate::resolve::{
523 ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport,
524 ResolvedReplacedModuleTarget,
525 };
526 use fallow_types::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
527 use fallow_types::extract::{
528 ExportName, ImportInfo, ImportedName, ModuleLoadMechanism, ReExportInfo, VisibilityTag,
529 };
530
531 std::thread_local! {
532 static TRAVERSALS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
533 }
534
535 pub(super) fn record_traversal() {
536 TRAVERSALS.with(|count| count.set(count.get() + 1));
537 }
538
539 fn capture_traversals<T>(run: impl FnOnce() -> T) -> (T, usize) {
540 let before = TRAVERSALS.with(std::cell::Cell::get);
541 let result = run();
542 let traversals = TRAVERSALS.with(|count| count.get() - before);
543 (result, traversals)
544 }
545
546 fn build_reachability_graph(
553 file_count: usize,
554 edges_spec: &[(u32, u32)],
555 runtime_eps: &[u32],
556 test_eps: &[u32],
557 ) -> ModuleGraph {
558 let edges: Vec<_> = edges_spec
559 .iter()
560 .map(|&(source, target)| (source, target, false))
561 .collect();
562 build_reachability_graph_with_replacements(file_count, &edges, runtime_eps, test_eps, &[])
563 }
564
565 fn build_reachability_graph_with_replacements(
566 file_count: usize,
567 edges_spec: &[(u32, u32, bool)],
568 runtime_eps: &[u32],
569 test_eps: &[u32],
570 replacements: &[ResolvedReplacedModuleTarget],
571 ) -> ModuleGraph {
572 let edges: Vec<_> = edges_spec
573 .iter()
574 .map(|&(source, target, is_commonjs)| ReachabilityEdge {
575 source,
576 target,
577 is_commonjs,
578 is_type_only: false,
579 })
580 .collect();
581 build_reachability_graph_with_edges(file_count, &edges, runtime_eps, test_eps, replacements)
582 }
583
584 struct ReachabilityEdge {
585 source: u32,
586 target: u32,
587 is_commonjs: bool,
588 is_type_only: bool,
589 }
590
591 #[expect(
592 clippy::cast_possible_truncation,
593 reason = "test file counts are trivially small"
594 )]
595 fn build_reachability_graph_with_edges(
596 file_count: usize,
597 edges_spec: &[ReachabilityEdge],
598 runtime_eps: &[u32],
599 test_eps: &[u32],
600 replacements: &[ResolvedReplacedModuleTarget],
601 ) -> ModuleGraph {
602 let files: Vec<DiscoveredFile> = (0..file_count)
603 .map(|i| DiscoveredFile {
604 id: FileId(i as u32),
605 path: PathBuf::from(format!("/project/file{i}.ts")),
606 size_bytes: 100,
607 })
608 .collect();
609
610 let resolved_modules: Vec<ResolvedModule> = (0..file_count)
611 .map(|i| {
612 let imports: Vec<ResolvedImport> = edges_spec
613 .iter()
614 .filter(|edge| edge.source == i as u32)
615 .map(|edge| ResolvedImport {
616 info: ImportInfo {
617 source: format!("./file{}", edge.target),
618 imported_name: ImportedName::Named("x".to_string()),
619 local_name: "x".to_string(),
620 is_type_only: edge.is_type_only,
621 is_type_only_star: false,
622 from_style: false,
623 span: oxc_span::Span::new(0, 10),
624 source_span: oxc_span::Span::default(),
625 },
626 target: if edge.is_commonjs {
627 ResolveResult::CommonJsInternalModule(FileId(edge.target))
628 } else {
629 ResolveResult::InternalModule(FileId(edge.target))
630 },
631 })
632 .collect();
633
634 ResolvedModule {
635 file_id: FileId(i as u32),
636 path: PathBuf::from(format!("/project/file{i}.ts")),
637 exports: vec![fallow_types::extract::ExportInfo {
638 name: ExportName::Named("x".to_string()),
639 local_name: Some("x".to_string()),
640 is_type_only: false,
641 visibility: VisibilityTag::None,
642 expected_unused_reason: None,
643 span: oxc_span::Span::new(0, 20),
644 members: vec![],
645 is_side_effect_used: false,
646 super_class: None,
647 deprecated: false,
648 deprecated_reason: None,
649 }]
650 .into(),
651 re_exports: vec![],
652 resolved_imports: imports,
653 resolved_dynamic_imports: vec![],
654 resolved_dynamic_patterns: vec![],
655 member_accesses: vec![].into(),
656 semantic_facts: std::sync::Arc::default(),
657 whole_object_uses: std::sync::Arc::default(),
658 has_cjs_exports: false,
659 has_angular_component_template_url: false,
660 unused_import_bindings: FxHashSet::default(),
661 type_referenced_import_bindings: vec![],
662 value_referenced_import_bindings: vec![],
663 namespace_object_aliases: vec![],
664 exported_factory_returns: std::sync::Arc::default(),
665 exported_factory_return_object_shapes: std::sync::Arc::default(),
666 type_member_types: std::sync::Arc::default(),
667 missing_export_targets: vec![],
668 }
669 })
670 .collect();
671
672 let runtime_entry_points: Vec<EntryPoint> = runtime_eps
673 .iter()
674 .map(|&i| EntryPoint {
675 path: PathBuf::from(format!("/project/file{i}.ts")),
676 source: EntryPointSource::PackageJsonMain,
677 })
678 .collect();
679
680 let test_entry_points: Vec<EntryPoint> = test_eps
681 .iter()
682 .map(|&i| EntryPoint {
683 path: PathBuf::from(format!("/project/file{i}.ts")),
684 source: EntryPointSource::TestFile,
685 })
686 .collect();
687
688 let mut all_entry_points = runtime_entry_points.clone();
689 all_entry_points.extend(test_entry_points.iter().cloned());
690
691 ModuleGraph::build_with_reachability_roots_and_replacements(
692 &resolved_modules,
693 replacements,
694 &all_entry_points,
695 &runtime_entry_points,
696 &test_entry_points,
697 &files,
698 )
699 }
700
701 fn build_masked_re_export_graph(
702 commonjs_to_barrel: bool,
703 commonjs_to_target: bool,
704 ) -> ModuleGraph {
705 let files: Vec<_> = (0..3)
706 .map(|id| DiscoveredFile {
707 id: FileId(id),
708 path: PathBuf::from(format!("/project/file{id}.ts")),
709 size_bytes: 100,
710 })
711 .collect();
712 let mut test_imports = vec![ResolvedImport {
713 info: ImportInfo {
714 source: "./barrel".to_string(),
715 imported_name: ImportedName::Named("value".to_string()),
716 local_name: "value".to_string(),
717 is_type_only: false,
718 is_type_only_star: false,
719 from_style: false,
720 span: oxc_span::Span::new(0, 10),
721 source_span: oxc_span::Span::default(),
722 },
723 target: if commonjs_to_barrel {
724 ResolveResult::CommonJsInternalModule(FileId(1))
725 } else {
726 ResolveResult::InternalModule(FileId(1))
727 },
728 }];
729 if commonjs_to_target {
730 test_imports.push(ResolvedImport {
731 info: ImportInfo {
732 source: "./target".to_string(),
733 imported_name: ImportedName::Named("value".to_string()),
734 local_name: "requiredValue".to_string(),
735 is_type_only: false,
736 is_type_only_star: false,
737 from_style: false,
738 span: oxc_span::Span::new(20, 30),
739 source_span: oxc_span::Span::default(),
740 },
741 target: ResolveResult::CommonJsInternalModule(FileId(2)),
742 });
743 }
744 let modules = vec![
745 ResolvedModule {
746 file_id: FileId(0),
747 path: files[0].path.clone(),
748 resolved_imports: test_imports,
749 ..ResolvedModule::default()
750 },
751 ResolvedModule {
752 file_id: FileId(1),
753 path: files[1].path.clone(),
754 re_exports: vec![ResolvedReExport {
755 info: ReExportInfo {
756 source: "./target".to_string(),
757 imported_name: "value".to_string(),
758 exported_name: "value".to_string(),
759 is_type_only: false,
760 span: oxc_span::Span::new(0, 10),
761 statement_span: oxc_span::Span::new(0, 0),
762 source_span: oxc_span::Span::new(0, 0),
763 },
764 target: ResolveResult::InternalModule(FileId(2)),
765 }],
766 ..ResolvedModule::default()
767 },
768 ResolvedModule {
769 file_id: FileId(2),
770 path: files[2].path.clone(),
771 exports: vec![fallow_types::extract::ExportInfo {
772 name: ExportName::Named("value".to_string()),
773 local_name: Some("value".to_string()),
774 is_type_only: false,
775 visibility: VisibilityTag::None,
776 expected_unused_reason: None,
777 span: oxc_span::Span::new(0, 20),
778 members: Vec::new(),
779 is_side_effect_used: false,
780 super_class: None,
781 deprecated: false,
782 deprecated_reason: None,
783 }]
784 .into(),
785 ..ResolvedModule::default()
786 },
787 ];
788 let test_entry_points = vec![EntryPoint {
789 path: files[0].path.clone(),
790 source: EntryPointSource::TestFile,
791 }];
792 ModuleGraph::build_with_reachability_roots_and_replacements(
793 &modules,
794 &[ResolvedReplacedModuleTarget {
795 source_file: FileId(0),
796 target_file: FileId(2),
797 }],
798 &test_entry_points,
799 &[],
800 &test_entry_points,
801 &files,
802 )
803 }
804
805 fn build_masked_pattern_graph(mechanisms: &[ModuleLoadMechanism]) -> ModuleGraph {
806 let files: Vec<_> = (0..2)
807 .map(|id| DiscoveredFile {
808 id: FileId(id),
809 path: PathBuf::from(format!("/project/file{id}.ts")),
810 size_bytes: 100,
811 })
812 .collect();
813 let modules = vec![
814 ResolvedModule {
815 file_id: FileId(0),
816 path: files[0].path.clone(),
817 resolved_dynamic_patterns: mechanisms
818 .iter()
819 .copied()
820 .map(|mechanism| {
821 (
822 fallow_types::extract::DynamicImportPattern {
823 prefix: "./modules/".to_string(),
824 suffix: None,
825 span: oxc_span::Span::new(0, 10),
826 mechanism,
827 excludes: Vec::new(),
828 },
829 vec![FileId(1)],
830 )
831 })
832 .collect(),
833 ..ResolvedModule::default()
834 },
835 ResolvedModule {
836 file_id: FileId(1),
837 path: files[1].path.clone(),
838 exports: vec![fallow_types::extract::ExportInfo {
839 name: ExportName::Named("value".to_string()),
840 local_name: Some("value".to_string()),
841 is_type_only: false,
842 visibility: VisibilityTag::None,
843 expected_unused_reason: None,
844 span: oxc_span::Span::new(0, 20),
845 members: Vec::new(),
846 is_side_effect_used: false,
847 super_class: None,
848 deprecated: false,
849 deprecated_reason: None,
850 }]
851 .into(),
852 ..ResolvedModule::default()
853 },
854 ];
855 let test_entry_points = vec![EntryPoint {
856 path: files[0].path.clone(),
857 source: EntryPointSource::TestFile,
858 }];
859 ModuleGraph::build_with_reachability_roots_and_replacements(
860 &modules,
861 &[ResolvedReplacedModuleTarget {
862 source_file: FileId(0),
863 target_file: FileId(1),
864 }],
865 &test_entry_points,
866 &[],
867 &test_entry_points,
868 &files,
869 )
870 }
871
872 fn intermediate_import(
873 source: &str,
874 target: FileId,
875 imported_name: ImportedName,
876 commonjs: bool,
877 ) -> ResolvedImport {
878 ResolvedImport {
879 info: ImportInfo {
880 source: source.to_string(),
881 imported_name,
882 local_name: "value".to_string(),
883 is_type_only: false,
884 is_type_only_star: false,
885 from_style: false,
886 span: oxc_span::Span::new(0, 10),
887 source_span: oxc_span::Span::default(),
888 },
889 target: if commonjs {
890 ResolveResult::CommonJsInternalModule(target)
891 } else {
892 ResolveResult::InternalModule(target)
893 },
894 }
895 }
896
897 fn intermediate_modules(
898 files: &[DiscoveredFile],
899 commonjs_to_barrel: bool,
900 include_cycle: bool,
901 ) -> Vec<ResolvedModule> {
902 let mut target_re_exports = Vec::new();
903 if include_cycle {
904 target_re_exports.push(ResolvedReExport {
905 info: ReExportInfo {
906 source: "./barrel".to_string(),
907 imported_name: "*".to_string(),
908 exported_name: "*".to_string(),
909 is_type_only: false,
910 span: oxc_span::Span::new(20, 30),
911 statement_span: oxc_span::Span::new(0, 0),
912 source_span: oxc_span::Span::new(0, 0),
913 },
914 target: ResolveResult::InternalModule(FileId(1)),
915 });
916 }
917 vec![
918 ResolvedModule {
919 file_id: FileId(0),
920 path: files[0].path.clone(),
921 resolved_imports: vec![
922 intermediate_import(
923 "./barrel",
924 FileId(1),
925 ImportedName::Named("value".to_string()),
926 commonjs_to_barrel,
927 ),
928 intermediate_import("./alternate", FileId(3), ImportedName::SideEffect, false),
929 ],
930 ..ResolvedModule::default()
931 },
932 ResolvedModule {
933 file_id: FileId(1),
934 path: files[1].path.clone(),
935 re_exports: vec![ResolvedReExport {
936 info: ReExportInfo {
937 source: "./target".to_string(),
938 imported_name: "value".to_string(),
939 exported_name: "value".to_string(),
940 is_type_only: false,
941 span: oxc_span::Span::new(0, 10),
942 statement_span: oxc_span::Span::new(0, 0),
943 source_span: oxc_span::Span::new(0, 0),
944 },
945 target: ResolveResult::InternalModule(FileId(2)),
946 }],
947 ..ResolvedModule::default()
948 },
949 ResolvedModule {
950 file_id: FileId(2),
951 path: files[2].path.clone(),
952 exports: vec![fallow_types::extract::ExportInfo {
953 name: ExportName::Named("value".to_string()),
954 local_name: Some("value".to_string()),
955 is_type_only: false,
956 visibility: VisibilityTag::None,
957 expected_unused_reason: None,
958 span: oxc_span::Span::new(0, 20),
959 members: Vec::new(),
960 is_side_effect_used: false,
961 super_class: None,
962 deprecated: false,
963 deprecated_reason: None,
964 }]
965 .into(),
966 re_exports: target_re_exports,
967 ..ResolvedModule::default()
968 },
969 ResolvedModule {
970 file_id: FileId(3),
971 path: files[3].path.clone(),
972 resolved_imports: vec![intermediate_import(
973 "./target",
974 FileId(2),
975 ImportedName::SideEffect,
976 false,
977 )],
978 ..ResolvedModule::default()
979 },
980 ]
981 }
982
983 fn build_intermediate_replacement_graph(
984 commonjs_to_barrel: bool,
985 include_cycle: bool,
986 mask_final_target: bool,
987 ) -> ModuleGraph {
988 let files: Vec<_> = (0..4)
989 .map(|id| DiscoveredFile {
990 id: FileId(id),
991 path: PathBuf::from(format!("/project/file{id}.ts")),
992 size_bytes: 100,
993 })
994 .collect();
995 let modules = intermediate_modules(&files, commonjs_to_barrel, include_cycle);
996 let test_entry_points = vec![EntryPoint {
997 path: files[0].path.clone(),
998 source: EntryPointSource::TestFile,
999 }];
1000 let mut replacements = vec![ResolvedReplacedModuleTarget {
1001 source_file: FileId(0),
1002 target_file: FileId(1),
1003 }];
1004 if mask_final_target {
1005 replacements.push(ResolvedReplacedModuleTarget {
1006 source_file: FileId(0),
1007 target_file: FileId(2),
1008 });
1009 }
1010
1011 ModuleGraph::build_with_reachability_roots_and_replacements(
1012 &modules,
1013 &replacements,
1014 &test_entry_points,
1015 &[],
1016 &test_entry_points,
1017 &files,
1018 )
1019 }
1020
1021 #[test]
1022 fn entry_point_is_reachable() {
1023 let graph = build_reachability_graph(1, &[], &[0], &[]);
1024 assert!(graph.modules[0].is_reachable());
1025 }
1026
1027 #[test]
1028 fn direct_dependency_is_reachable() {
1029 let graph = build_reachability_graph(2, &[(0, 1)], &[0], &[]);
1030 assert!(graph.modules[0].is_reachable());
1031 assert!(graph.modules[1].is_reachable());
1032 }
1033
1034 #[test]
1035 fn chain_reachability_a_b_c() {
1036 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[0], &[]);
1037 assert!(graph.modules[0].is_reachable());
1038 assert!(graph.modules[1].is_reachable());
1039 assert!(graph.modules[2].is_reachable());
1040 }
1041
1042 #[test]
1043 fn deep_chain_all_reachable() {
1044 let graph = build_reachability_graph(5, &[(0, 1), (1, 2), (2, 3), (3, 4)], &[0], &[]);
1045 for i in 0..5 {
1046 assert!(
1047 graph.modules[i].is_reachable(),
1048 "file{i} should be reachable through chain"
1049 );
1050 }
1051 }
1052
1053 #[test]
1054 fn disconnected_file_is_unreachable() {
1055 let graph = build_reachability_graph(3, &[(0, 1)], &[0], &[]);
1056 assert!(graph.modules[0].is_reachable());
1057 assert!(graph.modules[1].is_reachable());
1058 assert!(!graph.modules[2].is_reachable());
1059 }
1060
1061 #[test]
1062 fn no_entry_points_all_unreachable() {
1063 let graph = build_reachability_graph(2, &[(0, 1)], &[], &[]);
1064 assert!(!graph.modules[0].is_reachable());
1065 assert!(!graph.modules[1].is_reachable());
1066 }
1067
1068 #[test]
1069 fn cycle_both_reachable_when_entry() {
1070 let graph = build_reachability_graph(2, &[(0, 1), (1, 0)], &[0], &[]);
1071 assert!(graph.modules[0].is_reachable());
1072 assert!(graph.modules[1].is_reachable());
1073 }
1074
1075 #[test]
1076 fn three_node_cycle_all_reachable() {
1077 let graph = build_reachability_graph(3, &[(0, 1), (1, 2), (2, 0)], &[0], &[]);
1078 for i in 0..3 {
1079 assert!(
1080 graph.modules[i].is_reachable(),
1081 "file{i} in cycle should be reachable"
1082 );
1083 }
1084 }
1085
1086 #[test]
1087 fn cycle_not_reachable_from_entry() {
1088 let graph = build_reachability_graph(3, &[(1, 2), (2, 1)], &[0], &[]);
1089 assert!(graph.modules[0].is_reachable());
1090 assert!(!graph.modules[1].is_reachable());
1091 assert!(!graph.modules[2].is_reachable());
1092 }
1093
1094 #[test]
1095 fn runtime_reachable_only_from_runtime_entries() {
1096 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0], &[2]);
1097 assert!(graph.modules[0].is_runtime_reachable());
1098 assert!(graph.modules[1].is_runtime_reachable());
1099 assert!(!graph.modules[2].is_runtime_reachable());
1100 assert!(!graph.modules[3].is_runtime_reachable());
1101 }
1102
1103 #[test]
1104 fn test_reachable_only_from_test_entries() {
1105 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0], &[2]);
1106 assert!(!graph.modules[0].is_test_reachable());
1107 assert!(!graph.modules[1].is_test_reachable());
1108 assert!(graph.modules[2].is_test_reachable());
1109 assert!(graph.modules[3].is_test_reachable());
1110 }
1111
1112 fn build_type_only_test_import_graph(type_only: bool, masked: bool) -> ModuleGraph {
1117 let edges = [ReachabilityEdge {
1118 source: 0,
1119 target: 1,
1120 is_commonjs: false,
1121 is_type_only: type_only,
1122 }];
1123 let replacements = if masked {
1124 vec![ResolvedReplacedModuleTarget {
1125 source_file: FileId(0),
1126 target_file: FileId(2),
1127 }]
1128 } else {
1129 Vec::new()
1130 };
1131 build_reachability_graph_with_edges(3, &edges, &[], &[0], &replacements)
1132 }
1133
1134 #[test]
1135 fn type_only_test_import_gives_no_export_coverage() {
1136 for masked in [false, true] {
1137 let graph = build_type_only_test_import_graph(true, masked);
1138 assert_eq!(graph.test_reachability_index.profile_count > 0, masked);
1139 let export = &graph.modules[1].exports[0];
1140 assert_eq!(export.references.len(), 1, "masked: {masked}");
1141 assert_eq!(
1142 export.references[0].namespace,
1143 ExportNamespace::Type,
1144 "masked: {masked}"
1145 );
1146
1147 assert!(
1148 !graph.is_test_reference_covered(export, 0),
1149 "masked: {masked}"
1150 );
1151 assert!(
1152 !graph.is_any_test_reference_covered(export),
1153 "masked: {masked}"
1154 );
1155 }
1156 }
1157
1158 fn build_type_only_test_chain_graph(type_only: bool, masked: bool) -> ModuleGraph {
1162 let edge = |source, target, is_type_only| ReachabilityEdge {
1163 source,
1164 target,
1165 is_commonjs: false,
1166 is_type_only,
1167 };
1168 let edges = [edge(0, 1, type_only), edge(1, 2, false), edge(3, 1, false)];
1169 let replacements = if masked {
1170 vec![ResolvedReplacedModuleTarget {
1171 source_file: FileId(0),
1172 target_file: FileId(4),
1173 }]
1174 } else {
1175 Vec::new()
1176 };
1177 build_reachability_graph_with_edges(5, &edges, &[3], &[0], &replacements)
1178 }
1179
1180 #[test]
1181 fn type_only_test_import_gives_no_file_test_reach() {
1182 for masked in [false, true] {
1183 let graph = build_type_only_test_chain_graph(true, masked);
1184 assert_eq!(graph.test_reachability_index.profile_count > 0, masked);
1185 assert!(graph.modules[0].is_test_reachable(), "masked: {masked}");
1186 assert!(!graph.modules[1].is_test_reachable(), "masked: {masked}");
1187 assert!(!graph.modules[2].is_test_reachable(), "masked: {masked}");
1188 assert!(
1189 graph
1190 .test_entry_points_reaching(&[FileId(1), FileId(2)])
1191 .values()
1192 .all(Vec::is_empty),
1193 "masked: {masked}"
1194 );
1195 assert!(graph.modules[1].is_reachable(), "masked: {masked}");
1197 assert!(graph.modules[2].is_reachable(), "masked: {masked}");
1198 }
1199 }
1200
1201 #[test]
1202 fn value_test_import_gives_file_test_reach() {
1203 for masked in [false, true] {
1204 let graph = build_type_only_test_chain_graph(false, masked);
1205 assert!(graph.modules[1].is_test_reachable(), "masked: {masked}");
1206 assert!(graph.modules[2].is_test_reachable(), "masked: {masked}");
1207 let reaching = graph.test_entry_points_reaching(&[FileId(2)]);
1208 assert_eq!(reaching[&FileId(2)], vec![FileId(0)], "masked: {masked}");
1209 }
1210 }
1211
1212 #[test]
1213 fn value_test_import_gives_export_coverage() {
1214 for masked in [false, true] {
1215 let graph = build_type_only_test_import_graph(false, masked);
1216 let export = &graph.modules[1].exports[0];
1217
1218 assert!(
1219 graph.is_test_reference_covered(export, 0),
1220 "masked: {masked}"
1221 );
1222 }
1223 }
1224
1225 #[test]
1226 fn esm_replacement_masks_target_only_from_test_reachability() {
1227 let graph = build_reachability_graph_with_replacements(
1228 3,
1229 &[(0, 1, false), (1, 2, false)],
1230 &[],
1231 &[0],
1232 &[ResolvedReplacedModuleTarget {
1233 source_file: FileId(0),
1234 target_file: FileId(1),
1235 }],
1236 );
1237
1238 assert!(graph.modules[1].is_reachable());
1239 assert!(!graph.modules[1].is_test_reachable());
1240 assert!(!graph.modules[2].is_test_reachable());
1241 assert_eq!(graph.test_reachability_index.profile_count, 1);
1242 assert!(graph.test_reachability_index.profile_reaches(FileId(0), 0));
1243 assert!(!graph.test_reachability_index.profile_reaches(FileId(1), 0));
1244 }
1245
1246 #[test]
1247 fn commonjs_path_preserves_a_replaced_target() {
1248 let graph = build_reachability_graph_with_replacements(
1249 2,
1250 &[(0, 1, true)],
1251 &[],
1252 &[0],
1253 &[ResolvedReplacedModuleTarget {
1254 source_file: FileId(0),
1255 target_file: FileId(1),
1256 }],
1257 );
1258
1259 assert!(graph.modules[1].is_test_reachable());
1260 }
1261
1262 #[test]
1263 fn mixed_commonjs_and_esm_symbols_preserve_a_replaced_target() {
1264 let graph = build_reachability_graph_with_replacements(
1265 2,
1266 &[(0, 1, false), (0, 1, true)],
1267 &[],
1268 &[0],
1269 &[ResolvedReplacedModuleTarget {
1270 source_file: FileId(0),
1271 target_file: FileId(1),
1272 }],
1273 );
1274
1275 assert!(graph.modules[1].is_test_reachable());
1276 }
1277
1278 #[test]
1279 fn replacement_masks_a_target_behind_an_esm_re_export() {
1280 let graph = build_masked_re_export_graph(false, false);
1281 let target_export = &graph.modules[2].exports[0];
1282 assert!(
1283 !target_export.references.is_empty(),
1284 "barrel consumer reference should propagate"
1285 );
1286
1287 assert!(graph.modules[1].is_test_reachable());
1288 assert!(!graph.modules[2].is_test_reachable());
1289 assert_eq!(
1290 graph.reference_path_hops(target_export, 0)[0].1,
1291 ModuleLoadMechanism::EsModule
1292 );
1293 assert!(!graph.is_test_reference_covered(target_export, 0));
1294 }
1295
1296 #[test]
1297 fn commonjs_loaded_barrel_does_not_bypass_its_esm_re_export() {
1298 let graph = build_masked_re_export_graph(true, false);
1299 let target_export = &graph.modules[2].exports[0];
1300 assert!(
1301 !target_export.references.is_empty(),
1302 "barrel consumer reference should propagate"
1303 );
1304
1305 assert!(graph.modules[1].is_test_reachable());
1306 assert!(!graph.modules[2].is_test_reachable());
1307 assert_eq!(
1308 graph.reference_path_hops(target_export, 0)[0].1,
1309 ModuleLoadMechanism::EsModule
1310 );
1311 assert!(!graph.is_test_reference_covered(target_export, 0));
1312 }
1313
1314 #[test]
1315 fn direct_commonjs_and_esm_re_export_retain_distinct_coverage() {
1316 let graph = build_masked_re_export_graph(false, true);
1317 let export = &graph.modules[2].exports[0];
1318
1319 assert_eq!(export.references.len(), 2);
1320 let esm = (0..export.references.len())
1321 .find(|&index| {
1322 graph.reference_path_hops(export, index)[0].1 == ModuleLoadMechanism::EsModule
1323 })
1324 .expect("ESM re-export reference");
1325 let commonjs = (0..export.references.len())
1326 .find(|&index| {
1327 graph.reference_path_hops(export, index)[0].1
1328 == ModuleLoadMechanism::CommonJsRequire
1329 })
1330 .expect("direct CommonJS reference");
1331 assert!(!graph.is_test_reference_covered(export, esm));
1332 assert!(graph.is_test_reference_covered(export, commonjs));
1333 }
1334
1335 #[test]
1336 fn mocked_intermediate_barrel_blocks_a_reference_despite_an_alternate_target_route() {
1337 let graph = build_intermediate_replacement_graph(false, false, false);
1338 let export = &graph.modules[2].exports[0];
1339 let hops = graph.reference_path_hops(export, 0);
1340
1341 assert!(graph.modules[2].is_test_reachable());
1342 assert_eq!(
1343 hops,
1344 vec![
1345 (FileId(2), ModuleLoadMechanism::EsModule),
1346 (FileId(1), ModuleLoadMechanism::EsModule),
1347 ]
1348 );
1349 assert!(!graph.is_test_reference_covered(export, 0));
1350 }
1351
1352 #[test]
1353 fn mocked_re_export_cycle_member_blocks_the_cyclic_reference_path() {
1354 let graph = build_intermediate_replacement_graph(false, true, false);
1355 let export = &graph.modules[2].exports[0];
1356
1357 assert!(
1358 graph
1359 .re_export_cycles
1360 .iter()
1361 .any(|cycle| cycle.file_ids == vec![FileId(1), FileId(2)])
1362 );
1363 assert!(graph.modules[2].is_test_reachable());
1364 assert!(!graph.is_test_reference_covered(export, 0));
1365 }
1366
1367 #[test]
1368 fn commonjs_barrel_hop_bypasses_only_its_own_replacement() {
1369 let graph = build_intermediate_replacement_graph(true, false, false);
1370 let export = &graph.modules[2].exports[0];
1371
1372 assert_eq!(
1373 graph.reference_path_hops(export, 0),
1374 vec![
1375 (FileId(2), ModuleLoadMechanism::EsModule),
1376 (FileId(1), ModuleLoadMechanism::CommonJsRequire),
1377 ]
1378 );
1379 assert!(graph.is_test_reference_covered(export, 0));
1380 }
1381
1382 #[test]
1383 fn commonjs_barrel_hop_does_not_bypass_a_replaced_esm_target() {
1384 let graph = build_intermediate_replacement_graph(true, false, true);
1385 let export = &graph.modules[2].exports[0];
1386
1387 assert!(!graph.is_test_reference_covered(export, 0));
1388 }
1389
1390 #[test]
1391 fn reference_paths_survive_graph_cache_serialization() {
1392 let graph = build_intermediate_replacement_graph(false, false, false);
1393 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
1394 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
1395 let export = &decoded.modules[2].exports[0];
1396
1397 assert_eq!(
1398 decoded.reference_path_hops(export, 0),
1399 vec![
1400 (FileId(2), ModuleLoadMechanism::EsModule),
1401 (FileId(1), ModuleLoadMechanism::EsModule),
1402 ]
1403 );
1404 assert!(!decoded.is_test_reference_covered(export, 0));
1405 }
1406
1407 #[test]
1408 fn require_context_keeps_a_replaced_target_covered() {
1409 let graph = build_masked_pattern_graph(&[ModuleLoadMechanism::CommonJsRequire]);
1410 let export = &graph.modules[1].exports[0];
1411
1412 assert!(graph.modules[1].is_test_reachable());
1413 assert_eq!(
1414 graph.reference_path_hops(export, 0)[0].1,
1415 ModuleLoadMechanism::CommonJsRequire
1416 );
1417 assert!(graph.is_test_reference_covered(export, 0));
1418 }
1419
1420 #[test]
1421 fn overlapping_patterns_preserve_exact_esm_and_commonjs_coverage() {
1422 let graph = build_masked_pattern_graph(&[
1423 ModuleLoadMechanism::EsModule,
1424 ModuleLoadMechanism::CommonJsRequire,
1425 ]);
1426 let export = &graph.modules[1].exports[0];
1427
1428 assert!(graph.modules[1].is_test_reachable());
1429 assert_eq!(export.references.len(), 2);
1430 let esm = (0..export.references.len())
1431 .find(|&index| {
1432 graph.reference_path_hops(export, index)[0].1 == ModuleLoadMechanism::EsModule
1433 })
1434 .expect("ESM pattern reference");
1435 let commonjs = (0..export.references.len())
1436 .find(|&index| {
1437 graph.reference_path_hops(export, index)[0].1
1438 == ModuleLoadMechanism::CommonJsRequire
1439 })
1440 .expect("CommonJS pattern reference");
1441 assert!(!graph.is_test_reference_covered(export, esm));
1442 assert!(graph.is_test_reference_covered(export, commonjs));
1443 }
1444
1445 #[test]
1446 fn replacement_stops_before_a_masked_cycle_member() {
1447 let graph = build_reachability_graph_with_replacements(
1448 3,
1449 &[(0, 1, false), (1, 2, false), (2, 1, false)],
1450 &[],
1451 &[0],
1452 &[ResolvedReplacedModuleTarget {
1453 source_file: FileId(0),
1454 target_file: FileId(2),
1455 }],
1456 );
1457
1458 assert!(graph.modules[0].is_test_reachable());
1459 assert!(graph.modules[1].is_test_reachable());
1460 assert!(!graph.modules[2].is_test_reachable());
1461 }
1462
1463 #[test]
1464 fn identical_masks_share_one_reachability_profile() {
1465 let graph = build_reachability_graph_with_replacements(
1466 3,
1467 &[(0, 2, false), (1, 2, false)],
1468 &[],
1469 &[0, 1],
1470 &[
1471 ResolvedReplacedModuleTarget {
1472 source_file: FileId(1),
1473 target_file: FileId(2),
1474 },
1475 ResolvedReplacedModuleTarget {
1476 source_file: FileId(0),
1477 target_file: FileId(2),
1478 },
1479 ],
1480 );
1481
1482 assert_eq!(graph.test_reachability_index.profile_count, 1);
1483 assert!(graph.test_reachability_index.profile_masks(FileId(2), 0));
1484 assert!(graph.test_reachability_index.profile_reaches(FileId(0), 0));
1485 assert!(graph.test_reachability_index.profile_reaches(FileId(1), 0));
1486 assert!(!graph.modules[2].is_test_reachable());
1487 }
1488
1489 #[test]
1490 fn an_unmasked_root_keeps_the_shared_target_test_reachable() {
1491 let graph = build_reachability_graph_with_replacements(
1492 3,
1493 &[(0, 2, false), (1, 2, false)],
1494 &[],
1495 &[0, 1],
1496 &[ResolvedReplacedModuleTarget {
1497 source_file: FileId(0),
1498 target_file: FileId(2),
1499 }],
1500 );
1501
1502 assert_eq!(graph.test_reachability_index.profile_count, 2);
1503 assert!(graph.modules[2].is_test_reachable());
1504 assert!(!graph.test_reachability_index.profile_masks(FileId(2), 0));
1505 assert!(graph.test_reachability_index.profile_reaches(FileId(1), 0));
1506 assert!(graph.test_reachability_index.profile_reaches(FileId(2), 0));
1507 assert!(graph.test_reachability_index.profile_masks(FileId(2), 1));
1508 assert!(graph.test_reachability_index.profile_reaches(FileId(0), 1));
1509 let export = &graph.modules[2].exports[0];
1510 let reference_from = |source| {
1511 export
1512 .references
1513 .iter()
1514 .position(|reference| reference.from_file == source)
1515 .expect("both test roots reference the target export")
1516 };
1517 assert!(!graph.is_test_reference_covered(export, reference_from(FileId(0))));
1518 assert!(graph.is_test_reference_covered(export, reference_from(FileId(1))));
1519 }
1520
1521 #[test]
1522 fn replacement_declared_outside_a_test_root_keeps_the_fast_path() {
1523 let graph = build_reachability_graph_with_replacements(
1524 3,
1525 &[(0, 1, false)],
1526 &[],
1527 &[0],
1528 &[ResolvedReplacedModuleTarget {
1529 source_file: FileId(2),
1530 target_file: FileId(1),
1531 }],
1532 );
1533
1534 assert_eq!(graph.test_reachability_index.profile_count, 0);
1535 assert!(graph.modules[1].is_test_reachable());
1536 }
1537
1538 #[test]
1539 fn test_reachability_plan_uses_no_roots_without_test_entries() {
1540 let test_roots = FxHashSet::default();
1541
1542 let plan = TestReachabilityPlan::new(&test_roots, &[], 3);
1543
1544 assert!(matches!(&plan, TestReachabilityPlan::NoRoots));
1545 assert!(!plan.requires_reference_provenance());
1546 }
1547
1548 #[test]
1549 fn test_reachability_plan_ignores_replacements_outside_test_roots() {
1550 let test_roots = FxHashSet::from_iter([FileId(0)]);
1551 let replacements = [ResolvedReplacedModuleTarget {
1552 source_file: FileId(1),
1553 target_file: FileId(2),
1554 }];
1555
1556 let plan = TestReachabilityPlan::new(&test_roots, &replacements, 3);
1557
1558 assert!(matches!(&plan, TestReachabilityPlan::Legacy { .. }));
1559 assert!(!plan.requires_reference_provenance());
1560 }
1561
1562 #[test]
1563 fn test_reachability_plan_ignores_out_of_capacity_targets() {
1564 let test_roots = FxHashSet::from_iter([FileId(0)]);
1565 let replacements = [ResolvedReplacedModuleTarget {
1566 source_file: FileId(0),
1567 target_file: FileId(3),
1568 }];
1569
1570 let plan = TestReachabilityPlan::new(&test_roots, &replacements, 3);
1571
1572 assert!(matches!(&plan, TestReachabilityPlan::Legacy { .. }));
1573 assert!(!plan.requires_reference_provenance());
1574 }
1575
1576 #[test]
1577 fn test_reachability_plan_groups_roots_by_normalized_replacement_mask() {
1578 let test_roots = FxHashSet::from_iter([FileId(0), FileId(1), FileId(2)]);
1579 let replacements = [
1580 ResolvedReplacedModuleTarget {
1581 source_file: FileId(0),
1582 target_file: FileId(4),
1583 },
1584 ResolvedReplacedModuleTarget {
1585 source_file: FileId(0),
1586 target_file: FileId(4),
1587 },
1588 ResolvedReplacedModuleTarget {
1589 source_file: FileId(1),
1590 target_file: FileId(4),
1591 },
1592 ];
1593
1594 let plan = TestReachabilityPlan::new(&test_roots, &replacements, 5);
1595
1596 assert!(plan.requires_reference_provenance());
1597 let TestReachabilityPlan::Profiled { profiles } = plan else {
1598 panic!("valid test-root replacements must use profiled reachability");
1599 };
1600 assert_eq!(
1601 profiles,
1602 vec![
1603 TestReachabilityProfile {
1604 masked_targets: Vec::new(),
1605 roots: vec![FileId(2)],
1606 },
1607 TestReachabilityProfile {
1608 masked_targets: vec![FileId(4)],
1609 roots: vec![FileId(0), FileId(1)],
1610 },
1611 ]
1612 );
1613 }
1614
1615 type UniqueMaskInputs = (
1616 Vec<u32>,
1617 Vec<(u32, u32, bool)>,
1618 Vec<ResolvedReplacedModuleTarget>,
1619 );
1620
1621 fn unique_mask_graph_inputs(count: u32) -> UniqueMaskInputs {
1624 let test_roots: Vec<u32> = (0..count).collect();
1625 let mut edges = Vec::with_capacity(test_roots.len());
1626 let mut replacements = Vec::with_capacity(test_roots.len());
1627 for &root in &test_roots {
1628 let masked_target = count + root;
1629 edges.push((root, masked_target, false));
1630 replacements.push(ResolvedReplacedModuleTarget {
1631 source_file: FileId(root),
1632 target_file: FileId(masked_target),
1633 });
1634 }
1635 (test_roots, edges, replacements)
1636 }
1637
1638 #[test]
1639 #[cfg_attr(
1640 miri,
1641 ignore = "thousands of synthetic profiles are too slow under miri"
1642 )]
1643 fn profile_count_at_the_safety_cap_keeps_profiled_masking() {
1644 let count = u32::try_from(super::PROFILE_COUNT_SAFETY_CAP).expect("cap fits in u32");
1645 let total = count as usize * 2;
1646 let (test_roots, edges, replacements) = unique_mask_graph_inputs(count);
1647 let graph = build_reachability_graph_with_replacements(
1648 total,
1649 &edges,
1650 &[],
1651 &test_roots,
1652 &replacements,
1653 );
1654
1655 assert_eq!(
1656 graph.test_reachability_index.profile_count,
1657 super::PROFILE_COUNT_SAFETY_CAP
1658 );
1659 assert!(
1662 graph
1663 .test_reachability_index
1664 .profile_masks(FileId(count), 0)
1665 );
1666 assert!(graph.modules[0].is_test_reachable());
1667 assert!(!graph.modules[count as usize].is_test_reachable());
1670 assert_eq!(graph.test_masking_state(), TestMaskingState::Active);
1671 }
1672
1673 #[test]
1674 #[cfg_attr(
1675 miri,
1676 ignore = "thousands of synthetic profiles are too slow under miri"
1677 )]
1678 fn profile_count_above_the_safety_cap_falls_back_to_coarse_reachability() {
1679 let count = u32::try_from(super::PROFILE_COUNT_SAFETY_CAP * 4).expect("cap fits in u32");
1680 let total = count as usize * 2;
1681 let (test_roots, edges, replacements) = unique_mask_graph_inputs(count);
1682 let graph = build_reachability_graph_with_replacements(
1683 total,
1684 &edges,
1685 &[],
1686 &test_roots,
1687 &replacements,
1688 );
1689 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1690
1691 let plan = TestReachabilityPlan::new(&root_set, &replacements, total);
1692 assert!(matches!(
1693 &plan,
1694 TestReachabilityPlan::Legacy { capped: true, .. }
1695 ));
1696 assert!(!plan.requires_reference_provenance());
1697
1698 assert_eq!(graph.test_reachability_index.profile_count, 0);
1701 for &root in &test_roots {
1702 assert!(graph.modules[root as usize].is_test_reachable());
1703 assert!(graph.modules[(count + root) as usize].is_test_reachable());
1704 }
1705
1706 assert_eq!(graph.test_masking_state(), TestMaskingState::Capped);
1709 let mocked = &graph.modules[count as usize];
1710 let reach =
1711 graph.export_test_reach(mocked.file_id, &mocked.exports[0], &|file| file.0 < count);
1712 assert_eq!(reach.reach, ExportTestReach::Direct);
1713 }
1714
1715 #[test]
1716 fn replacement_profiles_survive_graph_cache_serialization() {
1717 let graph = build_reachability_graph_with_replacements(
1718 2,
1719 &[(0, 1, false)],
1720 &[],
1721 &[0],
1722 &[ResolvedReplacedModuleTarget {
1723 source_file: FileId(0),
1724 target_file: FileId(1),
1725 }],
1726 );
1727
1728 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
1729 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
1730
1731 assert_eq!(decoded.test_reachability_index.profile_count, 1);
1732 assert!(decoded.test_reachability_index.profile_masks(FileId(1), 0));
1733 assert!(
1734 decoded
1735 .test_reachability_index
1736 .profile_reaches(FileId(0), 0)
1737 );
1738 assert!(
1739 !decoded
1740 .test_reachability_index
1741 .profile_reaches(FileId(1), 0)
1742 );
1743 }
1744
1745 #[test]
1746 fn many_unmasked_roots_use_one_test_traversal() {
1747 let test_roots: Vec<u32> = (0..512).collect();
1748 let graph = build_reachability_graph(512, &[], &[], &test_roots);
1749 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1750
1751 let plan = TestReachabilityPlan::new(&root_set, &[], 512);
1752 let (reachability, traversals) =
1753 capture_traversals(|| graph.collect_test_reachable(plan, 512));
1754
1755 assert_eq!(traversals, 1);
1756 let (reachable, _) = reachability.into_parts();
1757 let reachable = reachable.expect("test roots have a reachability bitset");
1758 assert!(
1759 root_set
1760 .iter()
1761 .all(|root| reachable.contains(root.0 as usize))
1762 );
1763 }
1764
1765 #[test]
1766 fn many_roots_share_one_profile_worklist() {
1767 let test_roots: Vec<u32> = (0..128).collect();
1768 let edges: Vec<_> = test_roots
1769 .iter()
1770 .copied()
1771 .map(|root| (root, 128, false))
1772 .collect();
1773 let replacements: Vec<_> = (0..64)
1774 .map(|root| ResolvedReplacedModuleTarget {
1775 source_file: FileId(root),
1776 target_file: FileId(128),
1777 })
1778 .collect();
1779 let graph = build_reachability_graph_with_replacements(
1780 129,
1781 &edges,
1782 &[],
1783 &test_roots,
1784 &replacements,
1785 );
1786 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1787
1788 let plan = TestReachabilityPlan::new(&root_set, &replacements, 129);
1789 let (reachability, traversals) =
1790 capture_traversals(|| graph.collect_test_reachable(plan, 129));
1791
1792 assert_eq!(traversals, 1);
1793 assert_eq!(reachability.dirty_word_pop_count(), 129);
1794 }
1795
1796 #[test]
1797 fn unique_masks_cross_profile_words_in_one_deterministic_worklist() {
1798 let test_roots: Vec<u32> = (0..65).collect();
1799 let mut edges = Vec::with_capacity(test_roots.len() * 2);
1800 let mut replacements = Vec::with_capacity(test_roots.len());
1801 for &root in &test_roots {
1802 let masked_target = 65 + root;
1803 edges.push((root, masked_target, false));
1804 edges.push((root, 130, false));
1805 replacements.push(ResolvedReplacedModuleTarget {
1806 source_file: FileId(root),
1807 target_file: FileId(masked_target),
1808 });
1809 }
1810 let graph = build_reachability_graph_with_replacements(
1811 131,
1812 &edges,
1813 &[],
1814 &test_roots,
1815 &replacements,
1816 );
1817 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1818
1819 let plan = TestReachabilityPlan::new(&root_set, &replacements, 131);
1820 let (reachability, traversals) =
1821 capture_traversals(|| graph.collect_test_reachable(plan, 131));
1822
1823 assert_eq!(traversals, 1);
1824 assert_eq!(reachability.dirty_word_pop_count(), 67);
1825 assert_eq!(graph.test_reachability_index.profile_count, 65);
1826 assert_eq!(graph.test_reachability_index.words_per_file, 2);
1827 assert_eq!(graph.test_reachability_index.masked_profiles.len(), 65);
1828 assert!(
1829 graph
1830 .test_reachability_index
1831 .profile_reaches(FileId(130), 0)
1832 );
1833 assert!(
1834 graph
1835 .test_reachability_index
1836 .profile_reaches(FileId(130), 64)
1837 );
1838 assert!(graph.test_reachability_index.profile_masks(FileId(65), 0));
1839 assert!(graph.test_reachability_index.profile_masks(FileId(129), 64));
1840 assert!(!graph.test_reachability_index.profile_reaches(FileId(65), 0));
1841 assert!(
1842 !graph
1843 .test_reachability_index
1844 .profile_reaches(FileId(129), 64)
1845 );
1846
1847 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
1848 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
1849 assert_eq!(decoded.test_reachability_index.profile_count, 65);
1850 assert_eq!(decoded.test_reachability_index.words_per_file, 2);
1851 assert_eq!(decoded.test_reachability_index.masked_profiles.len(), 65);
1852 assert!(
1853 decoded
1854 .test_reachability_index
1855 .profile_reaches(FileId(130), 64)
1856 );
1857 assert!(
1858 decoded
1859 .test_reachability_index
1860 .profile_masks(FileId(129), 64)
1861 );
1862 }
1863
1864 #[test]
1865 fn staggered_profile_words_propagate_without_clean_word_scans() {
1866 const PROFILE_COUNT: u32 = 65;
1867 const MASK_START: u32 = PROFILE_COUNT;
1868 const FANOUT_START: u32 = MASK_START + PROFILE_COUNT;
1869 const FANOUT_COUNT: u32 = 8;
1870
1871 let test_roots: Vec<u32> = (0..PROFILE_COUNT).collect();
1872 let mut edges = Vec::new();
1873 for root in 1..PROFILE_COUNT {
1874 edges.push((root, root - 1, false));
1875 }
1876 for target in FANOUT_START..FANOUT_START + FANOUT_COUNT {
1877 edges.push((0, target, false));
1878 }
1879 let replacements: Vec<_> = test_roots
1880 .iter()
1881 .copied()
1882 .map(|root| ResolvedReplacedModuleTarget {
1883 source_file: FileId(root),
1884 target_file: FileId(MASK_START + root),
1885 })
1886 .collect();
1887 let file_count = (FANOUT_START + FANOUT_COUNT) as usize;
1888 let graph = build_reachability_graph_with_replacements(
1889 file_count,
1890 &edges,
1891 &[],
1892 &test_roots,
1893 &replacements,
1894 );
1895 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1896
1897 let plan = TestReachabilityPlan::new(&root_set, &replacements, file_count);
1898 let reachability = graph.collect_test_reachable(plan, file_count);
1899
1900 assert_eq!(graph.test_reachability_index.words_per_file, 2);
1901 let expected_dirty_word_pops =
1902 (PROFILE_COUNT * (PROFILE_COUNT + 1) / 2 + FANOUT_COUNT * PROFILE_COUNT) as usize;
1903 assert_eq!(
1904 reachability.dirty_word_pop_count(),
1905 expected_dirty_word_pops
1906 );
1907 for target in FANOUT_START..FANOUT_START + FANOUT_COUNT {
1908 assert!(
1909 graph
1910 .test_reachability_index
1911 .profile_reaches(FileId(target), 0)
1912 );
1913 assert!(
1914 graph
1915 .test_reachability_index
1916 .profile_reaches(FileId(target), 64)
1917 );
1918 }
1919 }
1920
1921 #[test]
1922 fn no_profile_coverage_omits_unused_reference_provenance() {
1923 const CHAIN_LENGTH: u32 = 128;
1924
1925 let files: Vec<_> = (0..=CHAIN_LENGTH)
1926 .map(|id| DiscoveredFile {
1927 id: FileId(id),
1928 path: PathBuf::from(format!("/project/file{id}.ts")),
1929 size_bytes: 100,
1930 })
1931 .collect();
1932 let mut modules = Vec::with_capacity(files.len());
1933 modules.push(ResolvedModule {
1934 file_id: FileId(0),
1935 path: files[0].path.clone(),
1936 resolved_imports: vec![intermediate_import(
1937 "./file1",
1938 FileId(1),
1939 ImportedName::Named("value".to_string()),
1940 false,
1941 )],
1942 ..ResolvedModule::default()
1943 });
1944 for id in 1..CHAIN_LENGTH {
1945 modules.push(ResolvedModule {
1946 file_id: FileId(id),
1947 path: files[id as usize].path.clone(),
1948 re_exports: vec![ResolvedReExport {
1949 info: ReExportInfo {
1950 source: format!("./file{}", id + 1),
1951 imported_name: "value".to_string(),
1952 exported_name: "value".to_string(),
1953 is_type_only: false,
1954 span: oxc_span::Span::new(0, 10),
1955 statement_span: oxc_span::Span::new(0, 0),
1956 source_span: oxc_span::Span::new(0, 0),
1957 },
1958 target: ResolveResult::InternalModule(FileId(id + 1)),
1959 }],
1960 ..ResolvedModule::default()
1961 });
1962 }
1963 modules.push(ResolvedModule {
1964 file_id: FileId(CHAIN_LENGTH),
1965 path: files[CHAIN_LENGTH as usize].path.clone(),
1966 exports: vec![fallow_types::extract::ExportInfo {
1967 name: ExportName::Named("value".to_string()),
1968 local_name: Some("value".to_string()),
1969 is_type_only: false,
1970 visibility: VisibilityTag::None,
1971 expected_unused_reason: None,
1972 span: oxc_span::Span::new(0, 20),
1973 members: Vec::new(),
1974 is_side_effect_used: false,
1975 super_class: None,
1976 deprecated: false,
1977 deprecated_reason: None,
1978 }]
1979 .into(),
1980 ..ResolvedModule::default()
1981 });
1982 let test_entry_points = vec![EntryPoint {
1983 path: files[0].path.clone(),
1984 source: EntryPointSource::TestFile,
1985 }];
1986 let graph = ModuleGraph::build_with_reachability_roots_and_replacements(
1987 &modules,
1988 &[],
1989 &test_entry_points,
1990 &[],
1991 &test_entry_points,
1992 &files,
1993 );
1994 let export = &graph.modules[CHAIN_LENGTH as usize].exports[0];
1995
1996 assert_eq!(graph.test_reachability_index.profile_count, 0);
1997 assert!(
1998 export.reference_paths.is_empty(),
1999 "the provenance side table must stay unallocated on the fast path"
2000 );
2001 assert!(graph.reference_paths.is_empty());
2002 assert!(graph.is_test_reference_covered(export, 0));
2003 }
2004
2005 #[test]
2006 fn overall_reachable_is_union_of_runtime_and_test() {
2007 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0], &[2]);
2008 for i in 0..4 {
2009 assert!(
2010 graph.modules[i].is_reachable(),
2011 "file{i} should be overall-reachable"
2012 );
2013 }
2014 }
2015
2016 #[test]
2017 fn shared_dependency_is_both_runtime_and_test_reachable() {
2018 let graph = build_reachability_graph(3, &[(0, 2), (1, 2)], &[0], &[1]);
2019 assert!(graph.modules[2].is_runtime_reachable());
2020 assert!(graph.modules[2].is_test_reachable());
2021 assert!(graph.modules[2].is_reachable());
2022 }
2023
2024 #[test]
2025 fn runtime_same_as_overall_reuses_bfs() {
2026 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[0], &[]);
2027 for i in 0..3 {
2028 assert_eq!(
2029 graph.modules[i].is_reachable(),
2030 graph.modules[i].is_runtime_reachable(),
2031 "file{i}: reachable and runtime_reachable should match when runtime==overall"
2032 );
2033 }
2034 }
2035
2036 #[test]
2037 fn empty_test_entries_none_test_reachable() {
2038 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[0], &[]);
2039 for i in 0..3 {
2040 assert!(
2041 !graph.modules[i].is_test_reachable(),
2042 "file{i} should not be test-reachable when no test entries exist"
2043 );
2044 }
2045 }
2046
2047 #[test]
2048 fn only_test_entries_runtime_unreachable() {
2049 let graph = build_reachability_graph(2, &[(0, 1)], &[], &[0]);
2050 assert!(graph.modules[0].is_test_reachable());
2051 assert!(graph.modules[1].is_test_reachable());
2052 assert!(!graph.modules[0].is_runtime_reachable());
2053 assert!(!graph.modules[1].is_runtime_reachable());
2054 assert!(graph.modules[0].is_reachable());
2055 assert!(graph.modules[1].is_reachable());
2056 }
2057
2058 #[test]
2059 fn diamond_dependency_all_reachable() {
2060 let graph = build_reachability_graph(4, &[(0, 1), (0, 2), (1, 3), (2, 3)], &[0], &[]);
2061 for i in 0..4 {
2062 assert!(
2063 graph.modules[i].is_reachable(),
2064 "file{i} in diamond should be reachable"
2065 );
2066 }
2067 }
2068
2069 #[test]
2070 fn multiple_entry_points_reach_disjoint_subtrees() {
2071 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0, 2], &[]);
2072 for i in 0..4 {
2073 assert!(
2074 graph.modules[i].is_reachable(),
2075 "file{i} should be reachable from one of the entry points"
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn empty_graph_no_panics() {
2082 let graph = build_reachability_graph(0, &[], &[], &[]);
2083 assert_eq!(graph.module_count(), 0);
2084 }
2085
2086 fn reaching(graph: &ModuleGraph, file: u32) -> Vec<u32> {
2087 let map = graph.test_entry_points_reaching(&[FileId(file)]);
2088 map[&FileId(file)].iter().map(|root| root.0).collect()
2089 }
2090
2091 #[test]
2092 fn per_entry_reach_applies_the_replacement_mask_of_each_test() {
2093 let graph = build_reachability_graph_with_replacements(
2096 4,
2097 &[(0, 1, false), (1, 2, false), (3, 2, false)],
2098 &[],
2099 &[0, 3],
2100 &[ResolvedReplacedModuleTarget {
2101 source_file: FileId(0),
2102 target_file: FileId(2),
2103 }],
2104 );
2105
2106 assert_eq!(reaching(&graph, 2), vec![3]);
2107 assert_eq!(reaching(&graph, 1), vec![0]);
2108 }
2109
2110 #[test]
2111 fn per_entry_reach_keeps_a_masked_target_behind_a_commonjs_edge() {
2112 let graph = build_reachability_graph_with_replacements(
2113 3,
2114 &[(0, 1, true), (2, 1, false)],
2115 &[],
2116 &[0, 2],
2117 &[ResolvedReplacedModuleTarget {
2118 source_file: FileId(0),
2119 target_file: FileId(1),
2120 }],
2121 );
2122
2123 assert_eq!(reaching(&graph, 1), vec![0, 2]);
2124 }
2125
2126 #[test]
2127 fn per_entry_reach_without_masks_lists_each_test_that_imports_the_file() {
2128 let graph = build_reachability_graph(5, &[(0, 1), (1, 2), (3, 2), (4, 1)], &[], &[0, 3]);
2129
2130 assert_eq!(reaching(&graph, 2), vec![0, 3]);
2131 assert_eq!(reaching(&graph, 1), vec![0]);
2132 assert!(reaching(&graph, 4).is_empty());
2133 }
2134
2135 #[test]
2136 fn per_entry_reach_spans_more_tests_than_one_profile_word() {
2137 let tests: Vec<u32> = (0..70).collect();
2139 let edges: Vec<(u32, u32, bool)> = tests.iter().map(|&test| (test, 70, false)).collect();
2140 let graph = build_reachability_graph_with_replacements(
2141 71,
2142 &edges,
2143 &[],
2144 &tests,
2145 &[ResolvedReplacedModuleTarget {
2146 source_file: FileId(0),
2147 target_file: FileId(70),
2148 }],
2149 );
2150
2151 assert_eq!(reaching(&graph, 70), (1..70).collect::<Vec<u32>>());
2152 assert!(graph.modules[70].is_test_reachable());
2153 }
2154
2155 #[test]
2156 fn per_entry_reach_agrees_with_test_reachability() {
2157 let graph = build_reachability_graph_with_replacements(
2158 6,
2159 &[(0, 1, false), (1, 2, false), (3, 4, false), (4, 2, false)],
2160 &[],
2161 &[0, 3],
2162 &[
2163 ResolvedReplacedModuleTarget {
2164 source_file: FileId(0),
2165 target_file: FileId(2),
2166 },
2167 ResolvedReplacedModuleTarget {
2168 source_file: FileId(3),
2169 target_file: FileId(4),
2170 },
2171 ],
2172 );
2173
2174 let files: Vec<FileId> = (0..6).map(FileId).collect();
2175 let map = graph.test_entry_points_reaching(&files);
2176 for file in &files {
2177 assert_eq!(
2178 !map[file].is_empty(),
2179 graph.modules[file.0 as usize].is_test_reachable(),
2180 "file{}",
2181 file.0
2182 );
2183 }
2184 assert_eq!(map[&FileId(1)], vec![FileId(0)]);
2185 assert!(map[&FileId(2)].is_empty());
2186 }
2187
2188 fn reach_of(
2190 graph: &ModuleGraph,
2191 file: u32,
2192 tests: &[u32],
2193 ) -> crate::graph::ExportTestReachResult {
2194 let module = &graph.modules[file as usize];
2195 graph.export_test_reach(module.file_id, &module.exports[0], &|id| {
2196 tests.contains(&id.0)
2197 })
2198 }
2199
2200 #[test]
2201 fn test_reach_without_mocks_reports_no_mocks() {
2202 let graph = build_reachability_graph(2, &[(0, 1)], &[], &[0]);
2203
2204 assert_eq!(graph.test_masking_state(), TestMaskingState::NoMocks);
2205 let reach = reach_of(&graph, 1, &[0]);
2206 assert_eq!(reach.reach, ExportTestReach::Direct);
2207 assert_eq!(reach.tests, vec![FileId(0)]);
2208 }
2209
2210 #[test]
2211 fn test_reach_through_a_non_test_module_is_transitive() {
2212 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[], &[0]);
2213
2214 assert_eq!(reach_of(&graph, 2, &[0]).reach, ExportTestReach::Transitive);
2215 }
2216
2217 #[test]
2218 fn test_reach_without_a_reference_separates_file_reach() {
2219 let files: Vec<_> = (0..3)
2220 .map(|id| DiscoveredFile {
2221 id: FileId(id),
2222 path: PathBuf::from(format!("/project/file{id}.ts")),
2223 size_bytes: 100,
2224 })
2225 .collect();
2226 let export = || {
2227 vec![fallow_types::extract::ExportInfo {
2228 name: ExportName::Named("value".to_string()),
2229 local_name: Some("value".to_string()),
2230 is_type_only: false,
2231 visibility: VisibilityTag::None,
2232 expected_unused_reason: None,
2233 span: oxc_span::Span::new(0, 20),
2234 members: Vec::new(),
2235 is_side_effect_used: false,
2236 super_class: None,
2237 deprecated: false,
2238 deprecated_reason: None,
2239 }]
2240 .into()
2241 };
2242 let modules = vec![
2243 ResolvedModule {
2244 file_id: FileId(0),
2245 path: files[0].path.clone(),
2246 resolved_imports: vec![intermediate_import(
2247 "./file1",
2248 FileId(1),
2249 ImportedName::SideEffect,
2250 false,
2251 )],
2252 ..ResolvedModule::default()
2253 },
2254 ResolvedModule {
2255 file_id: FileId(1),
2256 path: files[1].path.clone(),
2257 exports: export(),
2258 ..ResolvedModule::default()
2259 },
2260 ResolvedModule {
2261 file_id: FileId(2),
2262 path: files[2].path.clone(),
2263 exports: export(),
2264 ..ResolvedModule::default()
2265 },
2266 ];
2267 let test_entry_points = vec![EntryPoint {
2268 path: files[0].path.clone(),
2269 source: EntryPointSource::TestFile,
2270 }];
2271 let graph = ModuleGraph::build_with_reachability_roots_and_replacements(
2272 &modules,
2273 &[],
2274 &test_entry_points,
2275 &[],
2276 &test_entry_points,
2277 &files,
2278 );
2279
2280 assert_eq!(reach_of(&graph, 1, &[0]).reach, ExportTestReach::None);
2281 assert_eq!(reach_of(&graph, 2, &[0]).reach, ExportTestReach::NoneFile);
2282 }
2283
2284 #[test]
2285 fn test_reach_reports_an_esm_mask_on_a_direct_import() {
2286 let graph = build_reachability_graph_with_replacements(
2287 2,
2288 &[(0, 1, false)],
2289 &[],
2290 &[0],
2291 &[ResolvedReplacedModuleTarget {
2292 source_file: FileId(0),
2293 target_file: FileId(1),
2294 }],
2295 );
2296
2297 assert_eq!(graph.test_masking_state(), TestMaskingState::Active);
2298 assert!(!graph.modules[1].is_test_reachable());
2301 let reach = reach_of(&graph, 1, &[0]);
2302 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2303 assert_eq!(reach.tests, vec![FileId(0)]);
2304 assert_eq!(reach.mocked_targets, vec![FileId(1)]);
2305 }
2306
2307 #[test]
2308 fn test_reach_reports_a_mask_behind_a_barrel() {
2309 let graph = build_masked_re_export_graph(false, false);
2310 let reach = reach_of(&graph, 2, &[0]);
2311
2312 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2313 assert_eq!(reach.mocked_targets, vec![FileId(2)]);
2314 }
2315
2316 #[test]
2317 fn test_reach_names_a_mocked_intermediate_barrel() {
2318 let graph = build_intermediate_replacement_graph(false, false, false);
2319 let reach = reach_of(&graph, 2, &[0]);
2320
2321 assert!(graph.modules[2].is_test_reachable());
2324 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2325 assert_eq!(reach.mocked_targets, vec![FileId(1)]);
2326 }
2327
2328 #[test]
2329 fn test_reach_keeps_a_commonjs_require_unmasked() {
2330 let graph = build_reachability_graph_with_replacements(
2331 2,
2332 &[(0, 1, true)],
2333 &[],
2334 &[0],
2335 &[ResolvedReplacedModuleTarget {
2336 source_file: FileId(0),
2337 target_file: FileId(1),
2338 }],
2339 );
2340
2341 assert_eq!(reach_of(&graph, 1, &[0]).reach, ExportTestReach::Direct);
2342 }
2343
2344 #[test]
2345 fn test_reach_through_a_required_barrel_checks_only_esm_hops() {
2346 let barrel_mocked = build_intermediate_replacement_graph(true, false, false);
2347 assert_eq!(
2348 reach_of(&barrel_mocked, 2, &[0]).reach,
2349 ExportTestReach::Direct
2350 );
2351
2352 let target_mocked = build_intermediate_replacement_graph(true, false, true);
2353 let reach = reach_of(&target_mocked, 2, &[0]);
2354 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2355 assert_eq!(reach.mocked_targets, vec![FileId(2)]);
2356 }
2357
2358 #[test]
2359 fn test_reach_with_an_unmasked_second_root_is_direct() {
2360 let graph = build_reachability_graph_with_replacements(
2361 3,
2362 &[(0, 2, false), (1, 2, false)],
2363 &[],
2364 &[0, 1],
2365 &[ResolvedReplacedModuleTarget {
2366 source_file: FileId(0),
2367 target_file: FileId(2),
2368 }],
2369 );
2370 let reach = reach_of(&graph, 2, &[0, 1]);
2371
2372 assert_eq!(reach.reach, ExportTestReach::Direct);
2373 assert_eq!(reach.tests, vec![FileId(1)]);
2374 }
2375
2376 #[test]
2377 fn test_reach_survives_graph_cache_serialization() {
2378 let graph = build_masked_re_export_graph(false, false);
2379 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
2380 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
2381
2382 assert_eq!(decoded.test_masking_state(), TestMaskingState::Active);
2383 assert_eq!(
2384 reach_of(&decoded, 2, &[0]).reach,
2385 ExportTestReach::MockedOnly
2386 );
2387 }
2388}