1use std::sync::Arc;
4
5use itertools::{EitherOrBoth, Itertools};
6use tokio::sync::RwLock;
7
8use crate::{
9 ExecOutcome, ExecutorContext,
10 execution::{
11 EnvironmentRef, ExecutorSettings, annotations,
12 memory::Stack,
13 state::{self as exec_state, ModuleInfoMap},
14 },
15 parsing::ast::types::{Annotation, Node, Program},
16 walk::Node as WalkNode,
17};
18
19lazy_static::lazy_static! {
20 static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
22 static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
24}
25
26pub(super) async fn read_old_ast() -> Option<GlobalState> {
28 let old_ast = OLD_AST.read().await;
29 old_ast.clone()
30}
31
32pub(super) async fn write_old_ast(old_state: GlobalState) {
33 let mut old_ast = OLD_AST.write().await;
34 *old_ast = Some(old_state);
35}
36
37pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
38 let old_mem = PREV_MEMORY.read().await;
39 old_mem.clone()
40}
41
42pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
43 let mut old_mem = PREV_MEMORY.write().await;
44 *old_mem = Some(mem);
45}
46
47pub async fn bust_cache() {
48 let mut old_ast = OLD_AST.write().await;
49 *old_ast = None;
50}
51
52pub async fn clear_mem_cache() {
53 let mut old_mem = PREV_MEMORY.write().await;
54 *old_mem = None;
55}
56
57#[derive(Debug, Clone)]
59pub struct CacheInformation<'a> {
60 pub ast: &'a Node<Program>,
61 pub settings: &'a ExecutorSettings,
62}
63
64#[derive(Debug, Clone)]
66pub(super) struct GlobalState {
67 pub(super) main: ModuleState,
68 pub(super) exec_state: exec_state::GlobalState,
70 pub(super) settings: ExecutorSettings,
72}
73
74impl GlobalState {
75 pub fn new(
76 state: exec_state::ExecState,
77 settings: ExecutorSettings,
78 ast: Node<Program>,
79 result_env: EnvironmentRef,
80 ) -> Self {
81 Self {
82 main: ModuleState {
83 ast,
84 exec_state: state.mod_local,
85 result_env,
86 },
87 exec_state: state.global,
88 settings,
89 }
90 }
91
92 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
93 self.settings = settings;
94 self
95 }
96
97 pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
98 exec_state::ExecState {
99 global: self.exec_state.clone(),
100 mod_local: self.main.exec_state.clone(),
101 }
102 }
103
104 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
105 ExecOutcome {
108 variables: self.main.exec_state.variables(self.main.result_env),
109 filenames: self.exec_state.filenames(),
110 #[cfg(feature = "artifact-graph")]
111 operations: self.exec_state.root_module_artifacts.operations,
112 #[cfg(feature = "artifact-graph")]
113 artifact_graph: self.exec_state.artifacts.graph,
114 #[cfg(feature = "artifact-graph")]
115 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
116 #[cfg(feature = "artifact-graph")]
117 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
118 #[cfg(feature = "artifact-graph")]
119 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
120 errors: self.exec_state.errors,
121 default_planes: ctx.engine.get_default_planes().read().await.clone(),
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
128pub(super) struct ModuleState {
129 pub(super) ast: Node<Program>,
131 pub(super) exec_state: exec_state::ModuleState,
133 pub(super) result_env: EnvironmentRef,
135}
136
137#[derive(Debug, Clone, PartialEq)]
139#[allow(clippy::large_enum_variant)]
140pub(super) enum CacheResult {
141 ReExecute {
142 clear_scene: bool,
144 reapply_settings: bool,
146 program: Node<Program>,
148 },
149 CheckImportsOnly {
153 reapply_settings: bool,
155 ast: Node<Program>,
157 },
158 NoAction(bool),
160}
161
162pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
170 let mut reapply_settings = false;
171
172 if old.settings != new.settings {
175 reapply_settings = true;
178 }
179
180 if old.ast == new.ast {
183 if !old.ast.has_import_statements() {
187 return CacheResult::NoAction(reapply_settings);
188 }
189
190 return CacheResult::CheckImportsOnly {
192 reapply_settings,
193 ast: old.ast.clone(),
194 };
195 }
196
197 let mut old_ast = old.ast.clone();
199 let mut new_ast = new.ast.clone();
200
201 old_ast.compute_digest();
204 new_ast.compute_digest();
205
206 if old_ast.digest == new_ast.digest {
208 if !old.ast.has_import_statements() {
212 return CacheResult::NoAction(reapply_settings);
213 }
214
215 return CacheResult::CheckImportsOnly {
217 reapply_settings,
218 ast: old.ast.clone(),
219 };
220 }
221
222 if !old_ast
224 .inner_attrs
225 .iter()
226 .filter(annotations::is_significant)
227 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
228 .all(|pair| {
229 match pair {
230 EitherOrBoth::Both(old, new) => {
231 let Annotation { name, properties, .. } = &old.inner;
234 let Annotation {
235 name: new_name,
236 properties: new_properties,
237 ..
238 } = &new.inner;
239
240 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
241 && properties
242 .as_ref()
243 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
244 == new_properties
245 .as_ref()
246 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
247 }
248 _ => false,
249 }
250 })
251 {
252 return CacheResult::ReExecute {
256 clear_scene: true,
257 reapply_settings: true,
258 program: new.ast.clone(),
259 };
260 }
261
262 generate_changed_program(old_ast, new_ast, reapply_settings)
264}
265
266fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
278 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
279 let old_node: WalkNode = old.into();
280 let new_node: WalkNode = new.into();
281 old_node.digest() == new_node.digest()
282 }) {
283 return CacheResult::ReExecute {
289 clear_scene: true,
290 reapply_settings,
291 program: new_ast,
292 };
293 }
294
295 match new_ast.body.len().cmp(&old_ast.body.len()) {
299 std::cmp::Ordering::Less => {
300 CacheResult::ReExecute {
309 clear_scene: true,
310 reapply_settings,
311 program: new_ast,
312 }
313 }
314 std::cmp::Ordering::Greater => {
315 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
323
324 CacheResult::ReExecute {
325 clear_scene: false,
326 reapply_settings,
327 program: new_ast,
328 }
329 }
330 std::cmp::Ordering::Equal => {
331 CacheResult::NoAction(reapply_settings)
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use pretty_assertions::assert_eq;
348
349 use super::*;
350 use crate::execution::{ExecTestResults, parse_execute, parse_execute_with_project_dir};
351
352 #[tokio::test(flavor = "multi_thread")]
353 async fn test_get_changed_program_same_code() {
354 let new = r#"// Remove the end face for the extrusion.
355firstSketch = startSketchOn(XY)
356 |> startProfile(at = [-12, 12])
357 |> line(end = [24, 0])
358 |> line(end = [0, -24])
359 |> line(end = [-24, 0])
360 |> close()
361 |> extrude(length = 6)
362
363// Remove the end face for the extrusion.
364shell(firstSketch, faces = [END], thickness = 0.25)"#;
365
366 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
367
368 let result = get_changed_program(
369 CacheInformation {
370 ast: &program.ast,
371 settings: &exec_ctxt.settings,
372 },
373 CacheInformation {
374 ast: &program.ast,
375 settings: &exec_ctxt.settings,
376 },
377 )
378 .await;
379
380 assert_eq!(result, CacheResult::NoAction(false));
381 exec_ctxt.close().await;
382 }
383
384 #[tokio::test(flavor = "multi_thread")]
385 async fn test_get_changed_program_same_code_changed_whitespace() {
386 let old = r#" // Remove the end face for the extrusion.
387firstSketch = startSketchOn(XY)
388 |> startProfile(at = [-12, 12])
389 |> line(end = [24, 0])
390 |> line(end = [0, -24])
391 |> line(end = [-24, 0])
392 |> close()
393 |> extrude(length = 6)
394
395// Remove the end face for the extrusion.
396shell(firstSketch, faces = [END], thickness = 0.25) "#;
397
398 let new = r#"// Remove the end face for the extrusion.
399firstSketch = startSketchOn(XY)
400 |> startProfile(at = [-12, 12])
401 |> line(end = [24, 0])
402 |> line(end = [0, -24])
403 |> line(end = [-24, 0])
404 |> close()
405 |> extrude(length = 6)
406
407// Remove the end face for the extrusion.
408shell(firstSketch, faces = [END], thickness = 0.25)"#;
409
410 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
411
412 let program_new = crate::Program::parse_no_errs(new).unwrap();
413
414 let result = get_changed_program(
415 CacheInformation {
416 ast: &program.ast,
417 settings: &exec_ctxt.settings,
418 },
419 CacheInformation {
420 ast: &program_new.ast,
421 settings: &exec_ctxt.settings,
422 },
423 )
424 .await;
425
426 assert_eq!(result, CacheResult::NoAction(false));
427 exec_ctxt.close().await;
428 }
429
430 #[tokio::test(flavor = "multi_thread")]
431 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
432 let old = r#" // Removed the end face for the extrusion.
433firstSketch = startSketchOn(XY)
434 |> startProfile(at = [-12, 12])
435 |> line(end = [24, 0])
436 |> line(end = [0, -24])
437 |> line(end = [-24, 0])
438 |> close()
439 |> extrude(length = 6)
440
441// Remove the end face for the extrusion.
442shell(firstSketch, faces = [END], thickness = 0.25) "#;
443
444 let new = r#"// Remove the end face for the extrusion.
445firstSketch = startSketchOn(XY)
446 |> startProfile(at = [-12, 12])
447 |> line(end = [24, 0])
448 |> line(end = [0, -24])
449 |> line(end = [-24, 0])
450 |> close()
451 |> extrude(length = 6)
452
453// Remove the end face for the extrusion.
454shell(firstSketch, faces = [END], thickness = 0.25)"#;
455
456 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
457
458 let program_new = crate::Program::parse_no_errs(new).unwrap();
459
460 let result = get_changed_program(
461 CacheInformation {
462 ast: &program.ast,
463 settings: &exec_ctxt.settings,
464 },
465 CacheInformation {
466 ast: &program_new.ast,
467 settings: &exec_ctxt.settings,
468 },
469 )
470 .await;
471
472 assert_eq!(result, CacheResult::NoAction(false));
473 exec_ctxt.close().await;
474 }
475
476 #[tokio::test(flavor = "multi_thread")]
477 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
478 let old = r#"@foo(whatever = whatever)
479@bar
480// Removed the end face for the extrusion.
481firstSketch = startSketchOn(XY)
482 |> startProfile(at = [-12, 12])
483 |> line(end = [24, 0])
484 |> line(end = [0, -24])
485 |> line(end = [-24, 0]) // my thing
486 |> close()
487 |> extrude(length = 6)
488
489// Remove the end face for the extrusion.
490shell(firstSketch, faces = [END], thickness = 0.25) "#;
491
492 let new = r#"@foo(whatever = 42)
493@baz
494// Remove the end face for the extrusion.
495firstSketch = startSketchOn(XY)
496 |> startProfile(at = [-12, 12])
497 |> line(end = [24, 0])
498 |> line(end = [0, -24])
499 |> line(end = [-24, 0])
500 |> close()
501 |> extrude(length = 6)
502
503// Remove the end face for the extrusion.
504shell(firstSketch, faces = [END], thickness = 0.25)"#;
505
506 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
507
508 let program_new = crate::Program::parse_no_errs(new).unwrap();
509
510 let result = get_changed_program(
511 CacheInformation {
512 ast: &program.ast,
513 settings: &exec_ctxt.settings,
514 },
515 CacheInformation {
516 ast: &program_new.ast,
517 settings: &exec_ctxt.settings,
518 },
519 )
520 .await;
521
522 assert_eq!(result, CacheResult::NoAction(false));
523 exec_ctxt.close().await;
524 }
525
526 #[tokio::test(flavor = "multi_thread")]
528 async fn test_get_changed_program_same_code_but_different_grid_setting() {
529 let new = r#"// Remove the end face for the extrusion.
530firstSketch = startSketchOn(XY)
531 |> startProfile(at = [-12, 12])
532 |> line(end = [24, 0])
533 |> line(end = [0, -24])
534 |> line(end = [-24, 0])
535 |> close()
536 |> extrude(length = 6)
537
538// Remove the end face for the extrusion.
539shell(firstSketch, faces = [END], thickness = 0.25)"#;
540
541 let ExecTestResults {
542 program, mut exec_ctxt, ..
543 } = parse_execute(new).await.unwrap();
544
545 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
547
548 let result = get_changed_program(
549 CacheInformation {
550 ast: &program.ast,
551 settings: &Default::default(),
552 },
553 CacheInformation {
554 ast: &program.ast,
555 settings: &exec_ctxt.settings,
556 },
557 )
558 .await;
559
560 assert_eq!(result, CacheResult::NoAction(true));
561 exec_ctxt.close().await;
562 }
563
564 #[tokio::test(flavor = "multi_thread")]
566 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
567 let new = r#"// Remove the end face for the extrusion.
568firstSketch = startSketchOn(XY)
569 |> startProfile(at = [-12, 12])
570 |> line(end = [24, 0])
571 |> line(end = [0, -24])
572 |> line(end = [-24, 0])
573 |> close()
574 |> extrude(length = 6)
575
576// Remove the end face for the extrusion.
577shell(firstSketch, faces = [END], thickness = 0.25)"#;
578
579 let ExecTestResults {
580 program, mut exec_ctxt, ..
581 } = parse_execute(new).await.unwrap();
582
583 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
585
586 let result = get_changed_program(
587 CacheInformation {
588 ast: &program.ast,
589 settings: &Default::default(),
590 },
591 CacheInformation {
592 ast: &program.ast,
593 settings: &exec_ctxt.settings,
594 },
595 )
596 .await;
597
598 assert_eq!(result, CacheResult::NoAction(true));
599
600 let old_settings = exec_ctxt.settings.clone();
602 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
603
604 let result = get_changed_program(
605 CacheInformation {
606 ast: &program.ast,
607 settings: &old_settings,
608 },
609 CacheInformation {
610 ast: &program.ast,
611 settings: &exec_ctxt.settings,
612 },
613 )
614 .await;
615
616 assert_eq!(result, CacheResult::NoAction(true));
617
618 let old_settings = exec_ctxt.settings.clone();
620 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
621
622 let result = get_changed_program(
623 CacheInformation {
624 ast: &program.ast,
625 settings: &old_settings,
626 },
627 CacheInformation {
628 ast: &program.ast,
629 settings: &exec_ctxt.settings,
630 },
631 )
632 .await;
633
634 assert_eq!(result, CacheResult::NoAction(true));
635 exec_ctxt.close().await;
636 }
637
638 #[tokio::test(flavor = "multi_thread")]
641 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
642 let old_code = r#"@settings(defaultLengthUnit = in)
643startSketchOn(XY)
644"#;
645 let new_code = r#"@settings(defaultLengthUnit = mm)
646startSketchOn(XY)
647"#;
648
649 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
650
651 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
652 new_program.compute_digest();
653
654 let result = get_changed_program(
655 CacheInformation {
656 ast: &program.ast,
657 settings: &exec_ctxt.settings,
658 },
659 CacheInformation {
660 ast: &new_program.ast,
661 settings: &exec_ctxt.settings,
662 },
663 )
664 .await;
665
666 assert_eq!(
667 result,
668 CacheResult::ReExecute {
669 clear_scene: true,
670 reapply_settings: true,
671 program: new_program.ast,
672 }
673 );
674 exec_ctxt.close().await;
675 }
676
677 #[tokio::test(flavor = "multi_thread")]
680 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
681 let old_code = r#"@settings(defaultLengthUnit = in)
682startSketchOn(XY)
683"#;
684 let new_code = r#"
685startSketchOn(XY)
686"#;
687
688 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
689
690 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
691 new_program.compute_digest();
692
693 let result = get_changed_program(
694 CacheInformation {
695 ast: &program.ast,
696 settings: &exec_ctxt.settings,
697 },
698 CacheInformation {
699 ast: &new_program.ast,
700 settings: &exec_ctxt.settings,
701 },
702 )
703 .await;
704
705 assert_eq!(
706 result,
707 CacheResult::ReExecute {
708 clear_scene: true,
709 reapply_settings: true,
710 program: new_program.ast,
711 }
712 );
713 exec_ctxt.close().await;
714 }
715
716 #[tokio::test(flavor = "multi_thread")]
717 async fn test_multi_file_no_changes_does_not_reexecute() {
718 let code = r#"import "toBeImported.kcl" as importedCube
719
720importedCube
721
722sketch001 = startSketchOn(XZ)
723profile001 = startProfile(sketch001, at = [-134.53, -56.17])
724 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
725 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
726 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
727 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
728 |> close()
729extrude001 = extrude(profile001, length = 100)
730sketch003 = startSketchOn(extrude001, face = seg02)
731sketch002 = startSketchOn(extrude001, face = seg01)
732"#;
733
734 let other_file = (
735 std::path::PathBuf::from("toBeImported.kcl"),
736 r#"sketch001 = startSketchOn(XZ)
737profile001 = startProfile(sketch001, at = [281.54, 305.81])
738 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
739 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
740 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
741 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
742 |> close()
743extrude(profile001, length = 100)"#
744 .to_string(),
745 );
746
747 let tmp_dir = std::env::temp_dir();
748 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
749
750 let tmp_file = tmp_dir.join(other_file.0);
752 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
753 std::fs::write(tmp_file, other_file.1).unwrap();
754
755 let ExecTestResults { program, exec_ctxt, .. } =
756 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
757 .await
758 .unwrap();
759
760 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
761 new_program.compute_digest();
762
763 let result = get_changed_program(
764 CacheInformation {
765 ast: &program.ast,
766 settings: &exec_ctxt.settings,
767 },
768 CacheInformation {
769 ast: &new_program.ast,
770 settings: &exec_ctxt.settings,
771 },
772 )
773 .await;
774
775 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
776 panic!("Expected CheckImportsOnly, got {result:?}");
777 };
778
779 assert_eq!(reapply_settings, false);
780 exec_ctxt.close().await;
781 }
782
783 #[tokio::test(flavor = "multi_thread")]
784 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
785 let code = r#"import "toBeImported.kcl" as importedCube
786
787importedCube
788
789sketch001 = startSketchOn(XZ)
790profile001 = startProfile(sketch001, at = [-134.53, -56.17])
791 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
792 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
793 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
794 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
795 |> close()
796extrude001 = extrude(profile001, length = 100)
797sketch003 = startSketchOn(extrude001, face = seg02)
798sketch002 = startSketchOn(extrude001, face = seg01)
799"#;
800
801 let other_file = (
802 std::path::PathBuf::from("toBeImported.kcl"),
803 r#"sketch001 = startSketchOn(XZ)
804profile001 = startProfile(sketch001, at = [281.54, 305.81])
805 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
806 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
807 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
808 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
809 |> close()
810extrude(profile001, length = 100)"#
811 .to_string(),
812 );
813
814 let other_file2 = (
815 std::path::PathBuf::from("toBeImported.kcl"),
816 r#"sketch001 = startSketchOn(XZ)
817profile001 = startProfile(sketch001, at = [281.54, 305.81])
818 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
819 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
820 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
821 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
822 |> close()
823extrude(profile001, length = 100)
824|> translate(z=100)
825"#
826 .to_string(),
827 );
828
829 let tmp_dir = std::env::temp_dir();
830 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
831
832 let tmp_file = tmp_dir.join(other_file.0);
834 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
835 std::fs::write(&tmp_file, other_file.1).unwrap();
836
837 let ExecTestResults { program, exec_ctxt, .. } =
838 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
839 .await
840 .unwrap();
841
842 std::fs::write(tmp_file, other_file2.1).unwrap();
844
845 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
846 new_program.compute_digest();
847
848 let result = get_changed_program(
849 CacheInformation {
850 ast: &program.ast,
851 settings: &exec_ctxt.settings,
852 },
853 CacheInformation {
854 ast: &new_program.ast,
855 settings: &exec_ctxt.settings,
856 },
857 )
858 .await;
859
860 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
861 panic!("Expected CheckImportsOnly, got {result:?}");
862 };
863
864 assert_eq!(reapply_settings, false);
865 exec_ctxt.close().await;
866 }
867}