1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6 ExecutableProgramFingerprint, ProductionChunkId, SharedChunkCandidate, SharedChunkCandidateId,
7 SharedChunkCandidatePlan,
8};
9
10#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
11pub enum ProductionChunkKind {
12 Eager,
13 Root,
14 Shared,
15}
16
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub struct ProductionRootChunkInput {
19 pub activation_root_id: String,
20 pub root_kind: String,
21 pub programs: Vec<ExecutableProgramFingerprint>,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct ProductionChunkRecord {
26 pub id: ProductionChunkId,
27 pub kind: ProductionChunkKind,
28 pub activation_roots: Vec<String>,
29 pub root_kind: Option<String>,
30 pub programs: Vec<ExecutableProgramFingerprint>,
31 pub registration_only: bool,
32 pub provisional_module_filename: String,
33}
34
35#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
36pub struct ProductionChunkDependency {
37 pub dependent_chunk_id: ProductionChunkId,
38 pub dependency_chunk_id: ProductionChunkId,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum ProductionSharedChunkFailurePolicy {
43 FailDependentActivationWithoutRetry,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct ProductionActivationChunkPlan {
48 pub activation_root_id: String,
49 pub root_chunk_id: ProductionChunkId,
50 pub shared_chunk_ids: Vec<ProductionChunkId>,
51 pub shared_chunk_failure_policy: ProductionSharedChunkFailurePolicy,
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct ProductionChunkGraph {
56 pub eager_chunk_id: ProductionChunkId,
57 pub chunks: Vec<ProductionChunkRecord>,
58 pub dependencies: Vec<ProductionChunkDependency>,
59 pub activation_plans: Vec<ProductionActivationChunkPlan>,
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
63pub struct ProductionChunkExtractionReport {
64 pub extracted_candidate_ids: Vec<SharedChunkCandidateId>,
65 pub extracted_program_count: usize,
66 pub root_chunk_count: usize,
67 pub shared_chunk_count: usize,
68}
69
70#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
71pub enum ProductionChunkGraphValidationError {
72 DuplicateActivationRoot(String),
73 DuplicateChunkId(ProductionChunkId),
74 DuplicateDependency(ProductionChunkDependency),
75 EagerChunkHasDependency(ProductionChunkId),
76 ExpectedExactlyOneEagerChunk,
77 InvalidActivationPlanRoot(ProductionChunkId),
78 InvalidChunkDependency(ProductionChunkDependency),
79 InvalidSharedChunkCandidate(SharedChunkCandidateId),
80 InvalidSharedChunkOrder(String),
81 MissingDependencyChunk(ProductionChunkId),
82 ProductionChunkCycle,
83 SharedChunkIsNotRegistrationOnly(ProductionChunkId),
84 UnknownActivationPlanChunk(ProductionChunkId),
85}
86
87#[derive(Clone, Debug, Eq, PartialEq)]
88pub struct ProductionChunkExtractionError {
89 pub validation_errors: Vec<ProductionChunkGraphValidationError>,
90}
91
92#[allow(clippy::too_many_lines)]
99pub fn extract_production_chunk_graph(
100 candidate_plan: &SharedChunkCandidatePlan,
101 roots: &[ProductionRootChunkInput],
102) -> Result<(ProductionChunkGraph, ProductionChunkExtractionReport), ProductionChunkExtractionError>
103{
104 let mut root_inputs = roots.to_vec();
105 root_inputs.sort_by(|left, right| left.activation_root_id.cmp(&right.activation_root_id));
106 let mut extraction_errors = Vec::new();
107 if root_inputs
108 .windows(2)
109 .any(|pair| pair[0].activation_root_id == pair[1].activation_root_id)
110 {
111 extraction_errors.extend(
112 root_inputs
113 .windows(2)
114 .filter(|pair| pair[0].activation_root_id == pair[1].activation_root_id)
115 .map(|pair| {
116 ProductionChunkGraphValidationError::DuplicateActivationRoot(
117 pair[0].activation_root_id.clone(),
118 )
119 }),
120 );
121 }
122
123 let root_programs = root_inputs
124 .iter()
125 .map(|root| {
126 (
127 root.activation_root_id.clone(),
128 root.programs.iter().cloned().collect::<BTreeSet<_>>(),
129 )
130 })
131 .collect::<BTreeMap<_, _>>();
132 let root_ids = root_programs.keys().cloned().collect::<BTreeSet<_>>();
133 let mut candidates = candidate_plan.candidates.clone();
134 candidates.sort_by(|left, right| left.id.cmp(&right.id));
135 let mut extracted_by_root = BTreeMap::<String, BTreeSet<ExecutableProgramFingerprint>>::new();
136 for candidate in &candidates {
137 validate_candidate_consumers(
138 candidate,
139 &root_ids,
140 &root_programs,
141 &mut extracted_by_root,
142 &mut extraction_errors,
143 );
144 }
145 if !extraction_errors.is_empty() {
146 extraction_errors.sort();
147 extraction_errors.dedup();
148 return Err(ProductionChunkExtractionError {
149 validation_errors: extraction_errors,
150 });
151 }
152
153 let eager_chunk_id = ProductionChunkId::eager_runtime_v1();
154 let mut chunks = vec![ProductionChunkRecord {
155 id: eager_chunk_id.clone(),
156 kind: ProductionChunkKind::Eager,
157 activation_roots: vec!["eager-runtime".to_string()],
158 root_kind: None,
159 programs: Vec::new(),
160 registration_only: true,
161 provisional_module_filename: "boot.pending-content-hash.js".to_string(),
162 }];
163 let mut dependencies = Vec::new();
164 let mut shared_chunk_ids_by_root = BTreeMap::<String, Vec<ProductionChunkId>>::new();
165 for candidate in &candidates {
166 let Some(shared_chunk_id) = ProductionChunkId::for_activation_roots_and_programs(
167 "shared",
168 &candidate
169 .consumers
170 .iter()
171 .map(|consumer| consumer.root_id.clone())
172 .collect::<Vec<_>>(),
173 &candidate.programs,
174 ) else {
175 return Err(ProductionChunkExtractionError {
176 validation_errors: vec![
177 ProductionChunkGraphValidationError::InvalidSharedChunkCandidate(
178 candidate.id.clone(),
179 ),
180 ],
181 });
182 };
183 for consumer in &candidate.consumers {
184 shared_chunk_ids_by_root
185 .entry(consumer.root_id.clone())
186 .or_default()
187 .push(shared_chunk_id.clone());
188 }
189 chunks.push(ProductionChunkRecord {
190 id: shared_chunk_id.clone(),
191 kind: ProductionChunkKind::Shared,
192 activation_roots: candidate
193 .consumers
194 .iter()
195 .map(|consumer| consumer.root_id.clone())
196 .collect(),
197 root_kind: None,
198 programs: candidate.programs.clone(),
199 registration_only: true,
200 provisional_module_filename: provisional_filename("shared", &shared_chunk_id),
201 });
202 dependencies.push(ProductionChunkDependency {
203 dependent_chunk_id: shared_chunk_id,
204 dependency_chunk_id: eager_chunk_id.clone(),
205 });
206 }
207
208 let mut activation_plans = Vec::new();
209 for root in root_inputs {
210 let extracted = extracted_by_root
211 .get(&root.activation_root_id)
212 .cloned()
213 .unwrap_or_default();
214 let mut programs = root
215 .programs
216 .into_iter()
217 .filter(|program| !extracted.contains(program))
218 .collect::<Vec<_>>();
219 programs.sort();
220 programs.dedup();
221 let root_chunk_id = ProductionChunkId::for_activation_roots_and_programs(
222 &root.root_kind,
223 std::slice::from_ref(&root.activation_root_id),
224 &programs,
225 )
226 .ok_or_else(|| ProductionChunkExtractionError {
227 validation_errors: vec![
228 ProductionChunkGraphValidationError::InvalidActivationPlanRoot(
229 ProductionChunkId::eager_runtime_v1(),
230 ),
231 ],
232 })?;
233 let mut shared_chunk_ids = shared_chunk_ids_by_root
234 .remove(&root.activation_root_id)
235 .unwrap_or_default();
236 shared_chunk_ids.sort();
237 shared_chunk_ids.dedup();
238 chunks.push(ProductionChunkRecord {
239 id: root_chunk_id.clone(),
240 kind: ProductionChunkKind::Root,
241 activation_roots: vec![root.activation_root_id.clone()],
242 root_kind: Some(root.root_kind.clone()),
243 programs,
244 registration_only: false,
245 provisional_module_filename: provisional_filename(
246 &format!("root.{}", root.root_kind),
247 &root_chunk_id,
248 ),
249 });
250 dependencies.push(ProductionChunkDependency {
251 dependent_chunk_id: root_chunk_id.clone(),
252 dependency_chunk_id: eager_chunk_id.clone(),
253 });
254 dependencies.extend(shared_chunk_ids.iter().cloned().map(|dependency_chunk_id| {
255 ProductionChunkDependency {
256 dependent_chunk_id: root_chunk_id.clone(),
257 dependency_chunk_id,
258 }
259 }));
260 activation_plans.push(ProductionActivationChunkPlan {
261 activation_root_id: root.activation_root_id,
262 root_chunk_id,
263 shared_chunk_ids,
264 shared_chunk_failure_policy:
265 ProductionSharedChunkFailurePolicy::FailDependentActivationWithoutRetry,
266 });
267 }
268 chunks.sort_by(|left, right| left.id.cmp(&right.id));
269 dependencies.sort();
270 dependencies.dedup();
271 activation_plans.sort_by(|left, right| left.activation_root_id.cmp(&right.activation_root_id));
272 let graph = ProductionChunkGraph {
273 eager_chunk_id,
274 chunks,
275 dependencies,
276 activation_plans,
277 };
278 validate_production_chunk_graph(&graph)
279 .map_err(|validation_errors| ProductionChunkExtractionError { validation_errors })?;
280 let report = ProductionChunkExtractionReport {
281 extracted_candidate_ids: candidates
282 .iter()
283 .map(|candidate| candidate.id.clone())
284 .collect(),
285 extracted_program_count: candidates
286 .iter()
287 .map(|candidate| candidate.programs.len())
288 .sum(),
289 root_chunk_count: roots.len(),
290 shared_chunk_count: candidates.len(),
291 };
292 Ok((graph, report))
293}
294
295pub fn validate_production_chunk_graph(
301 graph: &ProductionChunkGraph,
302) -> Result<(), Vec<ProductionChunkGraphValidationError>> {
303 let mut errors = Vec::new();
304 let chunks = graph
305 .chunks
306 .iter()
307 .map(|chunk| (chunk.id.clone(), chunk))
308 .collect::<BTreeMap<_, _>>();
309 if graph.chunks.windows(2).any(|pair| pair[0].id == pair[1].id) {
310 errors.extend(
311 graph
312 .chunks
313 .windows(2)
314 .filter(|pair| pair[0].id == pair[1].id)
315 .map(|pair| {
316 ProductionChunkGraphValidationError::DuplicateChunkId(pair[0].id.clone())
317 }),
318 );
319 }
320 let eager_chunks = graph
321 .chunks
322 .iter()
323 .filter(|chunk| chunk.kind == ProductionChunkKind::Eager)
324 .collect::<Vec<_>>();
325 if eager_chunks.len() != 1
326 || eager_chunks.first().map(|chunk| &chunk.id) != Some(&graph.eager_chunk_id)
327 {
328 errors.push(ProductionChunkGraphValidationError::ExpectedExactlyOneEagerChunk);
329 }
330 errors.extend(
331 graph
332 .chunks
333 .iter()
334 .filter(|chunk| chunk.kind == ProductionChunkKind::Shared && !chunk.registration_only)
335 .map(|chunk| {
336 ProductionChunkGraphValidationError::SharedChunkIsNotRegistrationOnly(
337 chunk.id.clone(),
338 )
339 }),
340 );
341 let mut seen_dependencies = BTreeSet::new();
342 for dependency in &graph.dependencies {
343 if !seen_dependencies.insert(dependency.clone()) {
344 errors.push(ProductionChunkGraphValidationError::DuplicateDependency(
345 dependency.clone(),
346 ));
347 }
348 let Some(dependent) = chunks.get(&dependency.dependent_chunk_id) else {
349 errors.push(ProductionChunkGraphValidationError::MissingDependencyChunk(
350 dependency.dependent_chunk_id.clone(),
351 ));
352 continue;
353 };
354 let Some(target) = chunks.get(&dependency.dependency_chunk_id) else {
355 errors.push(ProductionChunkGraphValidationError::MissingDependencyChunk(
356 dependency.dependency_chunk_id.clone(),
357 ));
358 continue;
359 };
360 let valid = matches!(
361 (dependent.kind, target.kind),
362 (ProductionChunkKind::Shared, ProductionChunkKind::Eager)
363 | (
364 ProductionChunkKind::Root,
365 ProductionChunkKind::Eager | ProductionChunkKind::Shared
366 )
367 );
368 if !valid {
369 errors.push(if dependent.kind == ProductionChunkKind::Eager {
370 ProductionChunkGraphValidationError::EagerChunkHasDependency(dependent.id.clone())
371 } else {
372 ProductionChunkGraphValidationError::InvalidChunkDependency(dependency.clone())
373 });
374 }
375 }
376 errors.extend(validate_activation_plans(graph, &chunks));
377 errors.extend(validate_cycle(graph, &chunks));
378 errors.sort();
379 errors.dedup();
380 errors.is_empty().then_some(()).ok_or(errors)
381}
382
383fn validate_candidate_consumers(
384 candidate: &SharedChunkCandidate,
385 root_ids: &BTreeSet<String>,
386 root_programs: &BTreeMap<String, BTreeSet<ExecutableProgramFingerprint>>,
387 extracted_by_root: &mut BTreeMap<String, BTreeSet<ExecutableProgramFingerprint>>,
388 errors: &mut Vec<ProductionChunkGraphValidationError>,
389) {
390 for consumer in &candidate.consumers {
391 if !root_ids.contains(&consumer.root_id) {
392 errors.push(
393 ProductionChunkGraphValidationError::UnknownActivationPlanChunk(
394 ProductionChunkId::eager_runtime_v1(),
395 ),
396 );
397 continue;
398 }
399 let Some(programs) = root_programs.get(&consumer.root_id) else {
400 continue;
401 };
402 if !candidate
403 .programs
404 .iter()
405 .all(|program| programs.contains(program))
406 {
407 errors.push(
408 ProductionChunkGraphValidationError::InvalidActivationPlanRoot(
409 ProductionChunkId::eager_runtime_v1(),
410 ),
411 );
412 continue;
413 }
414 extracted_by_root
415 .entry(consumer.root_id.clone())
416 .or_default()
417 .extend(candidate.programs.iter().cloned());
418 }
419}
420
421fn validate_activation_plans(
422 graph: &ProductionChunkGraph,
423 chunks: &BTreeMap<ProductionChunkId, &ProductionChunkRecord>,
424) -> Vec<ProductionChunkGraphValidationError> {
425 let mut errors = Vec::new();
426 let mut roots = BTreeSet::new();
427 for plan in &graph.activation_plans {
428 if !roots.insert(plan.activation_root_id.clone()) {
429 errors.push(
430 ProductionChunkGraphValidationError::DuplicateActivationRoot(
431 plan.activation_root_id.clone(),
432 ),
433 );
434 }
435 if chunks.get(&plan.root_chunk_id).map(|chunk| chunk.kind)
436 != Some(ProductionChunkKind::Root)
437 {
438 errors.push(
439 ProductionChunkGraphValidationError::InvalidActivationPlanRoot(
440 plan.root_chunk_id.clone(),
441 ),
442 );
443 }
444 let mut shared = plan.shared_chunk_ids.clone();
445 shared.sort();
446 shared.dedup();
447 if shared != plan.shared_chunk_ids {
448 errors.push(
449 ProductionChunkGraphValidationError::InvalidSharedChunkOrder(
450 plan.activation_root_id.clone(),
451 ),
452 );
453 }
454 for shared_chunk_id in &plan.shared_chunk_ids {
455 match chunks.get(shared_chunk_id) {
456 Some(chunk)
457 if chunk.kind == ProductionChunkKind::Shared && chunk.registration_only => {}
458 Some(_) => errors.push(
459 ProductionChunkGraphValidationError::SharedChunkIsNotRegistrationOnly(
460 shared_chunk_id.clone(),
461 ),
462 ),
463 None => errors.push(
464 ProductionChunkGraphValidationError::UnknownActivationPlanChunk(
465 shared_chunk_id.clone(),
466 ),
467 ),
468 }
469 }
470 }
471 errors
472}
473
474fn validate_cycle(
475 graph: &ProductionChunkGraph,
476 chunks: &BTreeMap<ProductionChunkId, &ProductionChunkRecord>,
477) -> Vec<ProductionChunkGraphValidationError> {
478 fn visit(
479 current: &ProductionChunkId,
480 dependencies: &BTreeMap<ProductionChunkId, Vec<ProductionChunkId>>,
481 visiting: &mut BTreeSet<ProductionChunkId>,
482 visited: &mut BTreeSet<ProductionChunkId>,
483 ) -> bool {
484 if !visiting.insert(current.clone()) {
485 return true;
486 }
487 if let Some(next) = dependencies.get(current) {
488 for dependency in next {
489 if !visited.contains(dependency)
490 && visit(dependency, dependencies, visiting, visited)
491 {
492 return true;
493 }
494 }
495 }
496 visiting.remove(current);
497 visited.insert(current.clone());
498 false
499 }
500
501 let mut dependencies = BTreeMap::<ProductionChunkId, Vec<ProductionChunkId>>::new();
502 for dependency in &graph.dependencies {
503 dependencies
504 .entry(dependency.dependent_chunk_id.clone())
505 .or_default()
506 .push(dependency.dependency_chunk_id.clone());
507 }
508 let mut visiting = BTreeSet::new();
509 let mut visited = BTreeSet::new();
510 for chunk_id in chunks.keys() {
511 if !visited.contains(chunk_id)
512 && visit(chunk_id, &dependencies, &mut visiting, &mut visited)
513 {
514 return vec![ProductionChunkGraphValidationError::ProductionChunkCycle];
515 }
516 }
517 Vec::new()
518}
519
520fn provisional_filename(prefix: &str, chunk_id: &ProductionChunkId) -> String {
521 let short_id = chunk_id
522 .as_str()
523 .rsplit_once(':')
524 .map_or(chunk_id.as_str(), |(_, suffix)| suffix)
525 .chars()
526 .take(12)
527 .collect::<String>();
528 format!("{prefix}.{short_id}.pending-content-hash.js")
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::{plan_shared_lazy_chunk_candidates, SharedChunkProgramOccurrence};
535
536 fn fingerprint(value: &str) -> ExecutableProgramFingerprint {
537 ExecutableProgramFingerprint::for_canonical_opcode_stream(value.as_bytes())
538 }
539
540 fn root(
541 root_id: &str,
542 programs: Vec<ExecutableProgramFingerprint>,
543 ) -> ProductionRootChunkInput {
544 ProductionRootChunkInput {
545 activation_root_id: root_id.to_string(),
546 root_kind: "interaction".to_string(),
547 programs,
548 }
549 }
550
551 fn occurrence(root_id: &str, program: &str) -> SharedChunkProgramOccurrence {
552 SharedChunkProgramOccurrence {
553 root_id: root_id.to_string(),
554 fingerprint: fingerprint(program),
555 canonical_bytes: vec![b'x'; 300],
556 eager_required: false,
557 root_specific: false,
558 captures_mutable_identity: false,
559 registration_only: true,
560 runtime_protocol: "v1".to_string(),
561 }
562 }
563
564 #[test]
565 fn k7_extracts_shared_programs_and_preserves_root_identity() {
566 let shared = fingerprint("shared");
567 let candidates = plan_shared_lazy_chunk_candidates(
568 &[
569 occurrence("root-a", "shared"),
570 occurrence("root-b", "shared"),
571 ],
572 0,
573 0,
574 );
575 let (graph, report) = extract_production_chunk_graph(
576 &candidates,
577 &[
578 root("root-b", vec![shared.clone(), fingerprint("b-only")]),
579 root("root-a", vec![shared.clone(), fingerprint("a-only")]),
580 ],
581 )
582 .expect("eligible candidate extracts");
583 assert_eq!(report.shared_chunk_count, 1);
584 assert_eq!(graph.activation_plans.len(), 2);
585 assert_ne!(
586 graph.activation_plans[0].root_chunk_id,
587 graph.activation_plans[1].root_chunk_id
588 );
589 assert_eq!(
590 graph.activation_plans[0].shared_chunk_failure_policy,
591 ProductionSharedChunkFailurePolicy::FailDependentActivationWithoutRetry
592 );
593 assert_eq!(
594 graph.activation_plans[0].shared_chunk_ids,
595 graph.activation_plans[1].shared_chunk_ids
596 );
597 assert!(graph
598 .chunks
599 .iter()
600 .filter(|chunk| chunk.kind == ProductionChunkKind::Root)
601 .all(|chunk| !chunk.programs.contains(&shared)));
602 assert!(graph.chunks.iter().any(|chunk| {
603 chunk.kind == ProductionChunkKind::Shared
604 && chunk.registration_only
605 && chunk.programs == vec![shared.clone()]
606 }));
607 assert_eq!(
608 graph
609 .chunks
610 .iter()
611 .filter(|chunk| chunk.kind == ProductionChunkKind::Shared)
612 .count(),
613 1
614 );
615 }
616
617 #[test]
618 fn k7_keeps_roots_independent_when_no_candidate_extracts() {
619 let (graph, report) = extract_production_chunk_graph(
620 &SharedChunkCandidatePlan {
621 candidates: Vec::new(),
622 rejections: Vec::new(),
623 },
624 &[
625 root("root-a", vec![fingerprint("a")]),
626 root("root-b", vec![fingerprint("b")]),
627 ],
628 )
629 .expect("roots without sharing remain valid");
630 assert_eq!(report.shared_chunk_count, 0);
631 assert!(graph
632 .activation_plans
633 .iter()
634 .all(|plan| plan.shared_chunk_ids.is_empty()));
635 assert_eq!(
636 graph
637 .chunks
638 .iter()
639 .filter(|chunk| chunk.kind == ProductionChunkKind::Eager)
640 .count(),
641 1
642 );
643 }
644
645 #[test]
646 fn k7_rejects_cycle_and_depth_two_dependencies() {
647 let (mut graph, _) = extract_production_chunk_graph(
648 &SharedChunkCandidatePlan {
649 candidates: Vec::new(),
650 rejections: Vec::new(),
651 },
652 &[root("root-a", vec![fingerprint("a")])],
653 )
654 .expect("base graph");
655 let root_chunk = graph.activation_plans[0].root_chunk_id.clone();
656 graph.dependencies.push(ProductionChunkDependency {
657 dependent_chunk_id: graph.eager_chunk_id.clone(),
658 dependency_chunk_id: root_chunk,
659 });
660 let errors =
661 validate_production_chunk_graph(&graph).expect_err("cycle and eager edge reject");
662 assert!(errors.contains(&ProductionChunkGraphValidationError::ProductionChunkCycle));
663 assert!(errors.iter().any(|error| matches!(
664 error,
665 ProductionChunkGraphValidationError::EagerChunkHasDependency(_)
666 )));
667 }
668
669 #[test]
670 fn k7_rejects_shared_to_shared_depth() {
671 let shared_one = fingerprint("shared-one");
672 let shared_two = fingerprint("shared-two");
673 let candidates = plan_shared_lazy_chunk_candidates(
674 &[
675 occurrence("root-a", "shared-one"),
676 occurrence("root-b", "shared-one"),
677 occurrence("root-b", "shared-two"),
678 occurrence("root-c", "shared-two"),
679 ],
680 0,
681 0,
682 );
683 let (mut graph, _) = extract_production_chunk_graph(
684 &candidates,
685 &[
686 root("root-a", vec![shared_one]),
687 root("root-b", vec![shared_two, fingerprint("shared-one")]),
688 root("root-c", vec![fingerprint("shared-two")]),
689 ],
690 )
691 .expect("two exact candidate root sets");
692 let shared_chunks = graph
693 .chunks
694 .iter()
695 .filter(|chunk| chunk.kind == ProductionChunkKind::Shared)
696 .map(|chunk| chunk.id.clone())
697 .collect::<Vec<_>>();
698 assert_eq!(shared_chunks.len(), 2);
699 graph.dependencies.push(ProductionChunkDependency {
700 dependent_chunk_id: shared_chunks[0].clone(),
701 dependency_chunk_id: shared_chunks[1].clone(),
702 });
703 let errors = validate_production_chunk_graph(&graph).expect_err("depth two rejects");
704 assert!(errors.iter().any(|error| matches!(
705 error,
706 ProductionChunkGraphValidationError::InvalidChunkDependency(_)
707 )));
708 }
709
710 #[test]
711 fn k7_graph_is_deterministic_under_reversed_input_order() {
712 let shared = fingerprint("shared");
713 let candidates = plan_shared_lazy_chunk_candidates(
714 &[
715 occurrence("root-a", "shared"),
716 occurrence("root-b", "shared"),
717 ],
718 0,
719 0,
720 );
721 let first = extract_production_chunk_graph(
722 &candidates,
723 &[
724 root("root-a", vec![shared.clone()]),
725 root("root-b", vec![shared.clone()]),
726 ],
727 );
728 let second = extract_production_chunk_graph(
729 &candidates,
730 &[
731 root("root-b", vec![shared.clone()]),
732 root("root-a", vec![shared]),
733 ],
734 );
735 assert_eq!(first, second);
736 }
737}