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 #[cfg_attr(miri, ignore = "too slow under Miri: a scale test of safe code")]
1747 fn many_unmasked_roots_use_one_test_traversal() {
1748 let test_roots: Vec<u32> = (0..512).collect();
1749 let graph = build_reachability_graph(512, &[], &[], &test_roots);
1750 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1751
1752 let plan = TestReachabilityPlan::new(&root_set, &[], 512);
1753 let (reachability, traversals) =
1754 capture_traversals(|| graph.collect_test_reachable(plan, 512));
1755
1756 assert_eq!(traversals, 1);
1757 let (reachable, _) = reachability.into_parts();
1758 let reachable = reachable.expect("test roots have a reachability bitset");
1759 assert!(
1760 root_set
1761 .iter()
1762 .all(|root| reachable.contains(root.0 as usize))
1763 );
1764 }
1765
1766 #[test]
1767 #[cfg_attr(miri, ignore = "too slow under Miri: a scale test of safe code")]
1768 fn many_roots_share_one_profile_worklist() {
1769 let test_roots: Vec<u32> = (0..128).collect();
1770 let edges: Vec<_> = test_roots
1771 .iter()
1772 .copied()
1773 .map(|root| (root, 128, false))
1774 .collect();
1775 let replacements: Vec<_> = (0..64)
1776 .map(|root| ResolvedReplacedModuleTarget {
1777 source_file: FileId(root),
1778 target_file: FileId(128),
1779 })
1780 .collect();
1781 let graph = build_reachability_graph_with_replacements(
1782 129,
1783 &edges,
1784 &[],
1785 &test_roots,
1786 &replacements,
1787 );
1788 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1789
1790 let plan = TestReachabilityPlan::new(&root_set, &replacements, 129);
1791 let (reachability, traversals) =
1792 capture_traversals(|| graph.collect_test_reachable(plan, 129));
1793
1794 assert_eq!(traversals, 1);
1795 assert_eq!(reachability.dirty_word_pop_count(), 129);
1796 }
1797
1798 #[test]
1799 #[cfg_attr(miri, ignore = "too slow under Miri: a scale test of safe code")]
1800 fn unique_masks_cross_profile_words_in_one_deterministic_worklist() {
1801 let test_roots: Vec<u32> = (0..65).collect();
1802 let mut edges = Vec::with_capacity(test_roots.len() * 2);
1803 let mut replacements = Vec::with_capacity(test_roots.len());
1804 for &root in &test_roots {
1805 let masked_target = 65 + root;
1806 edges.push((root, masked_target, false));
1807 edges.push((root, 130, false));
1808 replacements.push(ResolvedReplacedModuleTarget {
1809 source_file: FileId(root),
1810 target_file: FileId(masked_target),
1811 });
1812 }
1813 let graph = build_reachability_graph_with_replacements(
1814 131,
1815 &edges,
1816 &[],
1817 &test_roots,
1818 &replacements,
1819 );
1820 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1821
1822 let plan = TestReachabilityPlan::new(&root_set, &replacements, 131);
1823 let (reachability, traversals) =
1824 capture_traversals(|| graph.collect_test_reachable(plan, 131));
1825
1826 assert_eq!(traversals, 1);
1827 assert_eq!(reachability.dirty_word_pop_count(), 67);
1828 assert_eq!(graph.test_reachability_index.profile_count, 65);
1829 assert_eq!(graph.test_reachability_index.words_per_file, 2);
1830 assert_eq!(graph.test_reachability_index.masked_profiles.len(), 65);
1831 assert!(
1832 graph
1833 .test_reachability_index
1834 .profile_reaches(FileId(130), 0)
1835 );
1836 assert!(
1837 graph
1838 .test_reachability_index
1839 .profile_reaches(FileId(130), 64)
1840 );
1841 assert!(graph.test_reachability_index.profile_masks(FileId(65), 0));
1842 assert!(graph.test_reachability_index.profile_masks(FileId(129), 64));
1843 assert!(!graph.test_reachability_index.profile_reaches(FileId(65), 0));
1844 assert!(
1845 !graph
1846 .test_reachability_index
1847 .profile_reaches(FileId(129), 64)
1848 );
1849
1850 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
1851 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
1852 assert_eq!(decoded.test_reachability_index.profile_count, 65);
1853 assert_eq!(decoded.test_reachability_index.words_per_file, 2);
1854 assert_eq!(decoded.test_reachability_index.masked_profiles.len(), 65);
1855 assert!(
1856 decoded
1857 .test_reachability_index
1858 .profile_reaches(FileId(130), 64)
1859 );
1860 assert!(
1861 decoded
1862 .test_reachability_index
1863 .profile_masks(FileId(129), 64)
1864 );
1865 }
1866
1867 #[test]
1868 #[cfg_attr(miri, ignore = "too slow under Miri: a scale test of safe code")]
1869 fn staggered_profile_words_propagate_without_clean_word_scans() {
1870 const PROFILE_COUNT: u32 = 65;
1871 const MASK_START: u32 = PROFILE_COUNT;
1872 const FANOUT_START: u32 = MASK_START + PROFILE_COUNT;
1873 const FANOUT_COUNT: u32 = 8;
1874
1875 let test_roots: Vec<u32> = (0..PROFILE_COUNT).collect();
1876 let mut edges = Vec::new();
1877 for root in 1..PROFILE_COUNT {
1878 edges.push((root, root - 1, false));
1879 }
1880 for target in FANOUT_START..FANOUT_START + FANOUT_COUNT {
1881 edges.push((0, target, false));
1882 }
1883 let replacements: Vec<_> = test_roots
1884 .iter()
1885 .copied()
1886 .map(|root| ResolvedReplacedModuleTarget {
1887 source_file: FileId(root),
1888 target_file: FileId(MASK_START + root),
1889 })
1890 .collect();
1891 let file_count = (FANOUT_START + FANOUT_COUNT) as usize;
1892 let graph = build_reachability_graph_with_replacements(
1893 file_count,
1894 &edges,
1895 &[],
1896 &test_roots,
1897 &replacements,
1898 );
1899 let root_set: FxHashSet<_> = test_roots.iter().copied().map(FileId).collect();
1900
1901 let plan = TestReachabilityPlan::new(&root_set, &replacements, file_count);
1902 let reachability = graph.collect_test_reachable(plan, file_count);
1903
1904 assert_eq!(graph.test_reachability_index.words_per_file, 2);
1905 let expected_dirty_word_pops =
1906 (PROFILE_COUNT * (PROFILE_COUNT + 1) / 2 + FANOUT_COUNT * PROFILE_COUNT) as usize;
1907 assert_eq!(
1908 reachability.dirty_word_pop_count(),
1909 expected_dirty_word_pops
1910 );
1911 for target in FANOUT_START..FANOUT_START + FANOUT_COUNT {
1912 assert!(
1913 graph
1914 .test_reachability_index
1915 .profile_reaches(FileId(target), 0)
1916 );
1917 assert!(
1918 graph
1919 .test_reachability_index
1920 .profile_reaches(FileId(target), 64)
1921 );
1922 }
1923 }
1924
1925 #[test]
1926 #[cfg_attr(miri, ignore = "too slow under Miri: a scale test of safe code")]
1927 fn no_profile_coverage_omits_unused_reference_provenance() {
1928 const CHAIN_LENGTH: u32 = 128;
1929
1930 let files: Vec<_> = (0..=CHAIN_LENGTH)
1931 .map(|id| DiscoveredFile {
1932 id: FileId(id),
1933 path: PathBuf::from(format!("/project/file{id}.ts")),
1934 size_bytes: 100,
1935 })
1936 .collect();
1937 let mut modules = Vec::with_capacity(files.len());
1938 modules.push(ResolvedModule {
1939 file_id: FileId(0),
1940 path: files[0].path.clone(),
1941 resolved_imports: vec![intermediate_import(
1942 "./file1",
1943 FileId(1),
1944 ImportedName::Named("value".to_string()),
1945 false,
1946 )],
1947 ..ResolvedModule::default()
1948 });
1949 for id in 1..CHAIN_LENGTH {
1950 modules.push(ResolvedModule {
1951 file_id: FileId(id),
1952 path: files[id as usize].path.clone(),
1953 re_exports: vec![ResolvedReExport {
1954 info: ReExportInfo {
1955 source: format!("./file{}", id + 1),
1956 imported_name: "value".to_string(),
1957 exported_name: "value".to_string(),
1958 is_type_only: false,
1959 span: oxc_span::Span::new(0, 10),
1960 statement_span: oxc_span::Span::new(0, 0),
1961 source_span: oxc_span::Span::new(0, 0),
1962 },
1963 target: ResolveResult::InternalModule(FileId(id + 1)),
1964 }],
1965 ..ResolvedModule::default()
1966 });
1967 }
1968 modules.push(ResolvedModule {
1969 file_id: FileId(CHAIN_LENGTH),
1970 path: files[CHAIN_LENGTH as usize].path.clone(),
1971 exports: vec![fallow_types::extract::ExportInfo {
1972 name: ExportName::Named("value".to_string()),
1973 local_name: Some("value".to_string()),
1974 is_type_only: false,
1975 visibility: VisibilityTag::None,
1976 expected_unused_reason: None,
1977 span: oxc_span::Span::new(0, 20),
1978 members: Vec::new(),
1979 is_side_effect_used: false,
1980 super_class: None,
1981 deprecated: false,
1982 deprecated_reason: None,
1983 }]
1984 .into(),
1985 ..ResolvedModule::default()
1986 });
1987 let test_entry_points = vec![EntryPoint {
1988 path: files[0].path.clone(),
1989 source: EntryPointSource::TestFile,
1990 }];
1991 let graph = ModuleGraph::build_with_reachability_roots_and_replacements(
1992 &modules,
1993 &[],
1994 &test_entry_points,
1995 &[],
1996 &test_entry_points,
1997 &files,
1998 );
1999 let export = &graph.modules[CHAIN_LENGTH as usize].exports[0];
2000
2001 assert_eq!(graph.test_reachability_index.profile_count, 0);
2002 assert!(
2003 export.reference_paths.is_empty(),
2004 "the provenance side table must stay unallocated on the fast path"
2005 );
2006 assert!(graph.reference_paths.is_empty());
2007 assert!(graph.is_test_reference_covered(export, 0));
2008 }
2009
2010 #[test]
2011 fn overall_reachable_is_union_of_runtime_and_test() {
2012 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0], &[2]);
2013 for i in 0..4 {
2014 assert!(
2015 graph.modules[i].is_reachable(),
2016 "file{i} should be overall-reachable"
2017 );
2018 }
2019 }
2020
2021 #[test]
2022 fn shared_dependency_is_both_runtime_and_test_reachable() {
2023 let graph = build_reachability_graph(3, &[(0, 2), (1, 2)], &[0], &[1]);
2024 assert!(graph.modules[2].is_runtime_reachable());
2025 assert!(graph.modules[2].is_test_reachable());
2026 assert!(graph.modules[2].is_reachable());
2027 }
2028
2029 #[test]
2030 fn runtime_same_as_overall_reuses_bfs() {
2031 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[0], &[]);
2032 for i in 0..3 {
2033 assert_eq!(
2034 graph.modules[i].is_reachable(),
2035 graph.modules[i].is_runtime_reachable(),
2036 "file{i}: reachable and runtime_reachable should match when runtime==overall"
2037 );
2038 }
2039 }
2040
2041 #[test]
2042 fn empty_test_entries_none_test_reachable() {
2043 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[0], &[]);
2044 for i in 0..3 {
2045 assert!(
2046 !graph.modules[i].is_test_reachable(),
2047 "file{i} should not be test-reachable when no test entries exist"
2048 );
2049 }
2050 }
2051
2052 #[test]
2053 fn only_test_entries_runtime_unreachable() {
2054 let graph = build_reachability_graph(2, &[(0, 1)], &[], &[0]);
2055 assert!(graph.modules[0].is_test_reachable());
2056 assert!(graph.modules[1].is_test_reachable());
2057 assert!(!graph.modules[0].is_runtime_reachable());
2058 assert!(!graph.modules[1].is_runtime_reachable());
2059 assert!(graph.modules[0].is_reachable());
2060 assert!(graph.modules[1].is_reachable());
2061 }
2062
2063 #[test]
2064 fn diamond_dependency_all_reachable() {
2065 let graph = build_reachability_graph(4, &[(0, 1), (0, 2), (1, 3), (2, 3)], &[0], &[]);
2066 for i in 0..4 {
2067 assert!(
2068 graph.modules[i].is_reachable(),
2069 "file{i} in diamond should be reachable"
2070 );
2071 }
2072 }
2073
2074 #[test]
2075 fn multiple_entry_points_reach_disjoint_subtrees() {
2076 let graph = build_reachability_graph(4, &[(0, 1), (2, 3)], &[0, 2], &[]);
2077 for i in 0..4 {
2078 assert!(
2079 graph.modules[i].is_reachable(),
2080 "file{i} should be reachable from one of the entry points"
2081 );
2082 }
2083 }
2084
2085 #[test]
2086 fn empty_graph_no_panics() {
2087 let graph = build_reachability_graph(0, &[], &[], &[]);
2088 assert_eq!(graph.module_count(), 0);
2089 }
2090
2091 fn reaching(graph: &ModuleGraph, file: u32) -> Vec<u32> {
2092 let map = graph.test_entry_points_reaching(&[FileId(file)]);
2093 map[&FileId(file)].iter().map(|root| root.0).collect()
2094 }
2095
2096 #[test]
2097 fn per_entry_reach_applies_the_replacement_mask_of_each_test() {
2098 let graph = build_reachability_graph_with_replacements(
2101 4,
2102 &[(0, 1, false), (1, 2, false), (3, 2, false)],
2103 &[],
2104 &[0, 3],
2105 &[ResolvedReplacedModuleTarget {
2106 source_file: FileId(0),
2107 target_file: FileId(2),
2108 }],
2109 );
2110
2111 assert_eq!(reaching(&graph, 2), vec![3]);
2112 assert_eq!(reaching(&graph, 1), vec![0]);
2113 }
2114
2115 #[test]
2116 fn per_entry_reach_keeps_a_masked_target_behind_a_commonjs_edge() {
2117 let graph = build_reachability_graph_with_replacements(
2118 3,
2119 &[(0, 1, true), (2, 1, false)],
2120 &[],
2121 &[0, 2],
2122 &[ResolvedReplacedModuleTarget {
2123 source_file: FileId(0),
2124 target_file: FileId(1),
2125 }],
2126 );
2127
2128 assert_eq!(reaching(&graph, 1), vec![0, 2]);
2129 }
2130
2131 #[test]
2132 fn per_entry_reach_without_masks_lists_each_test_that_imports_the_file() {
2133 let graph = build_reachability_graph(5, &[(0, 1), (1, 2), (3, 2), (4, 1)], &[], &[0, 3]);
2134
2135 assert_eq!(reaching(&graph, 2), vec![0, 3]);
2136 assert_eq!(reaching(&graph, 1), vec![0]);
2137 assert!(reaching(&graph, 4).is_empty());
2138 }
2139
2140 #[test]
2141 fn per_entry_reach_spans_more_tests_than_one_profile_word() {
2142 let tests: Vec<u32> = (0..70).collect();
2144 let edges: Vec<(u32, u32, bool)> = tests.iter().map(|&test| (test, 70, false)).collect();
2145 let graph = build_reachability_graph_with_replacements(
2146 71,
2147 &edges,
2148 &[],
2149 &tests,
2150 &[ResolvedReplacedModuleTarget {
2151 source_file: FileId(0),
2152 target_file: FileId(70),
2153 }],
2154 );
2155
2156 assert_eq!(reaching(&graph, 70), (1..70).collect::<Vec<u32>>());
2157 assert!(graph.modules[70].is_test_reachable());
2158 }
2159
2160 #[test]
2161 fn per_entry_reach_agrees_with_test_reachability() {
2162 let graph = build_reachability_graph_with_replacements(
2163 6,
2164 &[(0, 1, false), (1, 2, false), (3, 4, false), (4, 2, false)],
2165 &[],
2166 &[0, 3],
2167 &[
2168 ResolvedReplacedModuleTarget {
2169 source_file: FileId(0),
2170 target_file: FileId(2),
2171 },
2172 ResolvedReplacedModuleTarget {
2173 source_file: FileId(3),
2174 target_file: FileId(4),
2175 },
2176 ],
2177 );
2178
2179 let files: Vec<FileId> = (0..6).map(FileId).collect();
2180 let map = graph.test_entry_points_reaching(&files);
2181 for file in &files {
2182 assert_eq!(
2183 !map[file].is_empty(),
2184 graph.modules[file.0 as usize].is_test_reachable(),
2185 "file{}",
2186 file.0
2187 );
2188 }
2189 assert_eq!(map[&FileId(1)], vec![FileId(0)]);
2190 assert!(map[&FileId(2)].is_empty());
2191 }
2192
2193 fn reach_of(
2195 graph: &ModuleGraph,
2196 file: u32,
2197 tests: &[u32],
2198 ) -> crate::graph::ExportTestReachResult {
2199 let module = &graph.modules[file as usize];
2200 graph.export_test_reach(module.file_id, &module.exports[0], &|id| {
2201 tests.contains(&id.0)
2202 })
2203 }
2204
2205 #[test]
2206 fn test_reach_without_mocks_reports_no_mocks() {
2207 let graph = build_reachability_graph(2, &[(0, 1)], &[], &[0]);
2208
2209 assert_eq!(graph.test_masking_state(), TestMaskingState::NoMocks);
2210 let reach = reach_of(&graph, 1, &[0]);
2211 assert_eq!(reach.reach, ExportTestReach::Direct);
2212 assert_eq!(reach.tests, vec![FileId(0)]);
2213 }
2214
2215 #[test]
2216 fn test_reach_through_a_non_test_module_is_transitive() {
2217 let graph = build_reachability_graph(3, &[(0, 1), (1, 2)], &[], &[0]);
2218
2219 assert_eq!(reach_of(&graph, 2, &[0]).reach, ExportTestReach::Transitive);
2220 }
2221
2222 #[test]
2223 fn test_reach_without_a_reference_separates_file_reach() {
2224 let files: Vec<_> = (0..3)
2225 .map(|id| DiscoveredFile {
2226 id: FileId(id),
2227 path: PathBuf::from(format!("/project/file{id}.ts")),
2228 size_bytes: 100,
2229 })
2230 .collect();
2231 let export = || {
2232 vec![fallow_types::extract::ExportInfo {
2233 name: ExportName::Named("value".to_string()),
2234 local_name: Some("value".to_string()),
2235 is_type_only: false,
2236 visibility: VisibilityTag::None,
2237 expected_unused_reason: None,
2238 span: oxc_span::Span::new(0, 20),
2239 members: Vec::new(),
2240 is_side_effect_used: false,
2241 super_class: None,
2242 deprecated: false,
2243 deprecated_reason: None,
2244 }]
2245 .into()
2246 };
2247 let modules = vec![
2248 ResolvedModule {
2249 file_id: FileId(0),
2250 path: files[0].path.clone(),
2251 resolved_imports: vec![intermediate_import(
2252 "./file1",
2253 FileId(1),
2254 ImportedName::SideEffect,
2255 false,
2256 )],
2257 ..ResolvedModule::default()
2258 },
2259 ResolvedModule {
2260 file_id: FileId(1),
2261 path: files[1].path.clone(),
2262 exports: export(),
2263 ..ResolvedModule::default()
2264 },
2265 ResolvedModule {
2266 file_id: FileId(2),
2267 path: files[2].path.clone(),
2268 exports: export(),
2269 ..ResolvedModule::default()
2270 },
2271 ];
2272 let test_entry_points = vec![EntryPoint {
2273 path: files[0].path.clone(),
2274 source: EntryPointSource::TestFile,
2275 }];
2276 let graph = ModuleGraph::build_with_reachability_roots_and_replacements(
2277 &modules,
2278 &[],
2279 &test_entry_points,
2280 &[],
2281 &test_entry_points,
2282 &files,
2283 );
2284
2285 assert_eq!(reach_of(&graph, 1, &[0]).reach, ExportTestReach::None);
2286 assert_eq!(reach_of(&graph, 2, &[0]).reach, ExportTestReach::NoneFile);
2287 }
2288
2289 #[test]
2290 fn test_reach_reports_an_esm_mask_on_a_direct_import() {
2291 let graph = build_reachability_graph_with_replacements(
2292 2,
2293 &[(0, 1, false)],
2294 &[],
2295 &[0],
2296 &[ResolvedReplacedModuleTarget {
2297 source_file: FileId(0),
2298 target_file: FileId(1),
2299 }],
2300 );
2301
2302 assert_eq!(graph.test_masking_state(), TestMaskingState::Active);
2303 assert!(!graph.modules[1].is_test_reachable());
2306 let reach = reach_of(&graph, 1, &[0]);
2307 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2308 assert_eq!(reach.tests, vec![FileId(0)]);
2309 assert_eq!(reach.mocked_targets, vec![FileId(1)]);
2310 }
2311
2312 #[test]
2313 fn test_reach_reports_a_mask_behind_a_barrel() {
2314 let graph = build_masked_re_export_graph(false, false);
2315 let reach = reach_of(&graph, 2, &[0]);
2316
2317 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2318 assert_eq!(reach.mocked_targets, vec![FileId(2)]);
2319 }
2320
2321 #[test]
2322 fn test_reach_names_a_mocked_intermediate_barrel() {
2323 let graph = build_intermediate_replacement_graph(false, false, false);
2324 let reach = reach_of(&graph, 2, &[0]);
2325
2326 assert!(graph.modules[2].is_test_reachable());
2329 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2330 assert_eq!(reach.mocked_targets, vec![FileId(1)]);
2331 }
2332
2333 #[test]
2334 fn test_reach_keeps_a_commonjs_require_unmasked() {
2335 let graph = build_reachability_graph_with_replacements(
2336 2,
2337 &[(0, 1, true)],
2338 &[],
2339 &[0],
2340 &[ResolvedReplacedModuleTarget {
2341 source_file: FileId(0),
2342 target_file: FileId(1),
2343 }],
2344 );
2345
2346 assert_eq!(reach_of(&graph, 1, &[0]).reach, ExportTestReach::Direct);
2347 }
2348
2349 #[test]
2350 fn test_reach_through_a_required_barrel_checks_only_esm_hops() {
2351 let barrel_mocked = build_intermediate_replacement_graph(true, false, false);
2352 assert_eq!(
2353 reach_of(&barrel_mocked, 2, &[0]).reach,
2354 ExportTestReach::Direct
2355 );
2356
2357 let target_mocked = build_intermediate_replacement_graph(true, false, true);
2358 let reach = reach_of(&target_mocked, 2, &[0]);
2359 assert_eq!(reach.reach, ExportTestReach::MockedOnly);
2360 assert_eq!(reach.mocked_targets, vec![FileId(2)]);
2361 }
2362
2363 #[test]
2364 fn test_reach_with_an_unmasked_second_root_is_direct() {
2365 let graph = build_reachability_graph_with_replacements(
2366 3,
2367 &[(0, 2, false), (1, 2, false)],
2368 &[],
2369 &[0, 1],
2370 &[ResolvedReplacedModuleTarget {
2371 source_file: FileId(0),
2372 target_file: FileId(2),
2373 }],
2374 );
2375 let reach = reach_of(&graph, 2, &[0, 1]);
2376
2377 assert_eq!(reach.reach, ExportTestReach::Direct);
2378 assert_eq!(reach.tests, vec![FileId(1)]);
2379 }
2380
2381 #[test]
2382 fn test_reach_survives_graph_cache_serialization() {
2383 let graph = build_masked_re_export_graph(false, false);
2384 let encoded = postcard::to_allocvec(&graph).expect("encode graph");
2385 let decoded: ModuleGraph = postcard::from_bytes(&encoded).expect("decode graph");
2386
2387 assert_eq!(decoded.test_masking_state(), TestMaskingState::Active);
2388 assert_eq!(
2389 reach_of(&decoded, 2, &[0]).reach,
2390 ExportTestReach::MockedOnly
2391 );
2392 }
2393}