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 front::Object,
16 parsing::ast::types::{Annotation, Node, Program},
17 walk::Node as WalkNode,
18};
19
20lazy_static::lazy_static! {
21 static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
23 static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
25}
26
27pub(super) async fn read_old_ast() -> Option<GlobalState> {
29 let old_ast = OLD_AST.read().await;
30 old_ast.clone()
31}
32
33pub(super) async fn write_old_ast(old_state: GlobalState) {
34 let mut old_ast = OLD_AST.write().await;
35 *old_ast = Some(old_state);
36}
37
38pub(crate) async fn read_old_memory() -> Option<SketchModeState> {
39 let old_mem = PREV_MEMORY.read().await;
40 old_mem.clone()
41}
42
43pub(crate) async fn write_old_memory(mem: SketchModeState) {
44 let mut old_mem = PREV_MEMORY.write().await;
45 *old_mem = Some(mem);
46}
47
48pub async fn bust_cache() {
49 let mut old_ast = OLD_AST.write().await;
50 *old_ast = None;
51}
52
53pub async fn clear_mem_cache() {
54 let mut old_mem = PREV_MEMORY.write().await;
55 *old_mem = None;
56}
57
58#[derive(Debug, Clone)]
60pub struct CacheInformation<'a> {
61 pub ast: &'a Node<Program>,
62 pub settings: &'a ExecutorSettings,
63}
64
65#[derive(Debug, Clone)]
67pub(super) struct GlobalState {
68 pub(super) main: ModuleState,
69 pub(super) exec_state: exec_state::GlobalState,
71 pub(super) settings: ExecutorSettings,
73}
74
75impl GlobalState {
76 pub fn new(
77 state: exec_state::ExecState,
78 settings: ExecutorSettings,
79 ast: Node<Program>,
80 result_env: EnvironmentRef,
81 ) -> Self {
82 Self {
83 main: ModuleState {
84 ast,
85 exec_state: state.mod_local,
86 result_env,
87 },
88 exec_state: state.global,
89 settings,
90 }
91 }
92
93 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
94 self.settings = settings;
95 self
96 }
97
98 pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
99 exec_state::ExecState {
100 global: self.exec_state.clone(),
101 mod_local: self.main.exec_state.clone(),
102 }
103 }
104
105 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
106 ExecOutcome {
109 variables: self.main.exec_state.variables(self.main.result_env),
110 filenames: self.exec_state.filenames(),
111 #[cfg(feature = "artifact-graph")]
112 operations: self.exec_state.root_module_artifacts.operations,
113 #[cfg(feature = "artifact-graph")]
114 artifact_graph: self.exec_state.artifacts.graph,
115 #[cfg(feature = "artifact-graph")]
116 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
117 #[cfg(feature = "artifact-graph")]
118 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
119 #[cfg(feature = "artifact-graph")]
120 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
121 errors: self.exec_state.errors,
122 default_planes: ctx.engine.get_default_planes().read().await.clone(),
123 }
124 }
125}
126
127#[derive(Debug, Clone)]
129pub(super) struct ModuleState {
130 pub(super) ast: Node<Program>,
132 pub(super) exec_state: exec_state::ModuleState,
134 pub(super) result_env: EnvironmentRef,
136}
137
138#[derive(Debug, Clone)]
140pub(crate) struct SketchModeState {
141 pub stack: Stack,
143 pub module_infos: ModuleInfoMap,
145 #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
147 pub scene_objects: Vec<Object>,
148}
149
150#[derive(Debug, Clone, PartialEq)]
152#[allow(clippy::large_enum_variant)]
153pub(super) enum CacheResult {
154 ReExecute {
155 clear_scene: bool,
157 reapply_settings: bool,
159 program: Node<Program>,
161 },
162 CheckImportsOnly {
166 reapply_settings: bool,
168 ast: Node<Program>,
170 },
171 NoAction(bool),
173}
174
175pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
183 let mut reapply_settings = false;
184
185 if old.settings != new.settings {
188 reapply_settings = true;
191 }
192
193 if old.ast == new.ast {
196 if !old.ast.has_import_statements() {
200 return CacheResult::NoAction(reapply_settings);
201 }
202
203 return CacheResult::CheckImportsOnly {
205 reapply_settings,
206 ast: old.ast.clone(),
207 };
208 }
209
210 let mut old_ast = old.ast.clone();
212 let mut new_ast = new.ast.clone();
213
214 old_ast.compute_digest();
217 new_ast.compute_digest();
218
219 if old_ast.digest == new_ast.digest {
221 if !old.ast.has_import_statements() {
225 return CacheResult::NoAction(reapply_settings);
226 }
227
228 return CacheResult::CheckImportsOnly {
230 reapply_settings,
231 ast: old.ast.clone(),
232 };
233 }
234
235 if !old_ast
237 .inner_attrs
238 .iter()
239 .filter(annotations::is_significant)
240 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
241 .all(|pair| {
242 match pair {
243 EitherOrBoth::Both(old, new) => {
244 let Annotation { name, properties, .. } = &old.inner;
247 let Annotation {
248 name: new_name,
249 properties: new_properties,
250 ..
251 } = &new.inner;
252
253 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
254 && properties
255 .as_ref()
256 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
257 == new_properties
258 .as_ref()
259 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
260 }
261 _ => false,
262 }
263 })
264 {
265 return CacheResult::ReExecute {
269 clear_scene: true,
270 reapply_settings: true,
271 program: new.ast.clone(),
272 };
273 }
274
275 generate_changed_program(old_ast, new_ast, reapply_settings)
277}
278
279fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
291 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
292 let old_node: WalkNode = old.into();
293 let new_node: WalkNode = new.into();
294 old_node.digest() == new_node.digest()
295 }) {
296 return CacheResult::ReExecute {
302 clear_scene: true,
303 reapply_settings,
304 program: new_ast,
305 };
306 }
307
308 match new_ast.body.len().cmp(&old_ast.body.len()) {
312 std::cmp::Ordering::Less => {
313 CacheResult::ReExecute {
322 clear_scene: true,
323 reapply_settings,
324 program: new_ast,
325 }
326 }
327 std::cmp::Ordering::Greater => {
328 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
336
337 CacheResult::ReExecute {
338 clear_scene: false,
339 reapply_settings,
340 program: new_ast,
341 }
342 }
343 std::cmp::Ordering::Equal => {
344 CacheResult::NoAction(reapply_settings)
354 }
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use pretty_assertions::assert_eq;
361
362 use super::*;
363 use crate::execution::{ExecTestResults, parse_execute, parse_execute_with_project_dir};
364
365 #[tokio::test(flavor = "multi_thread")]
366 async fn test_get_changed_program_same_code() {
367 let new = r#"// Remove the end face for the extrusion.
368firstSketch = startSketchOn(XY)
369 |> startProfile(at = [-12, 12])
370 |> line(end = [24, 0])
371 |> line(end = [0, -24])
372 |> line(end = [-24, 0])
373 |> close()
374 |> extrude(length = 6)
375
376// Remove the end face for the extrusion.
377shell(firstSketch, faces = [END], thickness = 0.25)"#;
378
379 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
380
381 let result = get_changed_program(
382 CacheInformation {
383 ast: &program.ast,
384 settings: &exec_ctxt.settings,
385 },
386 CacheInformation {
387 ast: &program.ast,
388 settings: &exec_ctxt.settings,
389 },
390 )
391 .await;
392
393 assert_eq!(result, CacheResult::NoAction(false));
394 exec_ctxt.close().await;
395 }
396
397 #[tokio::test(flavor = "multi_thread")]
398 async fn test_get_changed_program_same_code_changed_whitespace() {
399 let old = r#" // Remove the end face for the extrusion.
400firstSketch = startSketchOn(XY)
401 |> startProfile(at = [-12, 12])
402 |> line(end = [24, 0])
403 |> line(end = [0, -24])
404 |> line(end = [-24, 0])
405 |> close()
406 |> extrude(length = 6)
407
408// Remove the end face for the extrusion.
409shell(firstSketch, faces = [END], thickness = 0.25) "#;
410
411 let new = r#"// Remove the end face for the extrusion.
412firstSketch = startSketchOn(XY)
413 |> startProfile(at = [-12, 12])
414 |> line(end = [24, 0])
415 |> line(end = [0, -24])
416 |> line(end = [-24, 0])
417 |> close()
418 |> extrude(length = 6)
419
420// Remove the end face for the extrusion.
421shell(firstSketch, faces = [END], thickness = 0.25)"#;
422
423 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
424
425 let program_new = crate::Program::parse_no_errs(new).unwrap();
426
427 let result = get_changed_program(
428 CacheInformation {
429 ast: &program.ast,
430 settings: &exec_ctxt.settings,
431 },
432 CacheInformation {
433 ast: &program_new.ast,
434 settings: &exec_ctxt.settings,
435 },
436 )
437 .await;
438
439 assert_eq!(result, CacheResult::NoAction(false));
440 exec_ctxt.close().await;
441 }
442
443 #[tokio::test(flavor = "multi_thread")]
444 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
445 let old = r#" // Removed the end face for the extrusion.
446firstSketch = startSketchOn(XY)
447 |> startProfile(at = [-12, 12])
448 |> line(end = [24, 0])
449 |> line(end = [0, -24])
450 |> line(end = [-24, 0])
451 |> close()
452 |> extrude(length = 6)
453
454// Remove the end face for the extrusion.
455shell(firstSketch, faces = [END], thickness = 0.25) "#;
456
457 let new = r#"// Remove the end face for the extrusion.
458firstSketch = startSketchOn(XY)
459 |> startProfile(at = [-12, 12])
460 |> line(end = [24, 0])
461 |> line(end = [0, -24])
462 |> line(end = [-24, 0])
463 |> close()
464 |> extrude(length = 6)
465
466// Remove the end face for the extrusion.
467shell(firstSketch, faces = [END], thickness = 0.25)"#;
468
469 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
470
471 let program_new = crate::Program::parse_no_errs(new).unwrap();
472
473 let result = get_changed_program(
474 CacheInformation {
475 ast: &program.ast,
476 settings: &exec_ctxt.settings,
477 },
478 CacheInformation {
479 ast: &program_new.ast,
480 settings: &exec_ctxt.settings,
481 },
482 )
483 .await;
484
485 assert_eq!(result, CacheResult::NoAction(false));
486 exec_ctxt.close().await;
487 }
488
489 #[tokio::test(flavor = "multi_thread")]
490 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
491 let old = r#"@foo(whatever = whatever)
492@bar
493// Removed the end face for the extrusion.
494firstSketch = startSketchOn(XY)
495 |> startProfile(at = [-12, 12])
496 |> line(end = [24, 0])
497 |> line(end = [0, -24])
498 |> line(end = [-24, 0]) // my thing
499 |> close()
500 |> extrude(length = 6)
501
502// Remove the end face for the extrusion.
503shell(firstSketch, faces = [END], thickness = 0.25) "#;
504
505 let new = r#"@foo(whatever = 42)
506@baz
507// Remove the end face for the extrusion.
508firstSketch = startSketchOn(XY)
509 |> startProfile(at = [-12, 12])
510 |> line(end = [24, 0])
511 |> line(end = [0, -24])
512 |> line(end = [-24, 0])
513 |> close()
514 |> extrude(length = 6)
515
516// Remove the end face for the extrusion.
517shell(firstSketch, faces = [END], thickness = 0.25)"#;
518
519 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
520
521 let program_new = crate::Program::parse_no_errs(new).unwrap();
522
523 let result = get_changed_program(
524 CacheInformation {
525 ast: &program.ast,
526 settings: &exec_ctxt.settings,
527 },
528 CacheInformation {
529 ast: &program_new.ast,
530 settings: &exec_ctxt.settings,
531 },
532 )
533 .await;
534
535 assert_eq!(result, CacheResult::NoAction(false));
536 exec_ctxt.close().await;
537 }
538
539 #[tokio::test(flavor = "multi_thread")]
541 async fn test_get_changed_program_same_code_but_different_grid_setting() {
542 let new = r#"// Remove the end face for the extrusion.
543firstSketch = startSketchOn(XY)
544 |> startProfile(at = [-12, 12])
545 |> line(end = [24, 0])
546 |> line(end = [0, -24])
547 |> line(end = [-24, 0])
548 |> close()
549 |> extrude(length = 6)
550
551// Remove the end face for the extrusion.
552shell(firstSketch, faces = [END], thickness = 0.25)"#;
553
554 let ExecTestResults {
555 program, mut exec_ctxt, ..
556 } = parse_execute(new).await.unwrap();
557
558 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
560
561 let result = get_changed_program(
562 CacheInformation {
563 ast: &program.ast,
564 settings: &Default::default(),
565 },
566 CacheInformation {
567 ast: &program.ast,
568 settings: &exec_ctxt.settings,
569 },
570 )
571 .await;
572
573 assert_eq!(result, CacheResult::NoAction(true));
574 exec_ctxt.close().await;
575 }
576
577 #[tokio::test(flavor = "multi_thread")]
579 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
580 let new = r#"// Remove the end face for the extrusion.
581firstSketch = startSketchOn(XY)
582 |> startProfile(at = [-12, 12])
583 |> line(end = [24, 0])
584 |> line(end = [0, -24])
585 |> line(end = [-24, 0])
586 |> close()
587 |> extrude(length = 6)
588
589// Remove the end face for the extrusion.
590shell(firstSketch, faces = [END], thickness = 0.25)"#;
591
592 let ExecTestResults {
593 program, mut exec_ctxt, ..
594 } = parse_execute(new).await.unwrap();
595
596 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
598
599 let result = get_changed_program(
600 CacheInformation {
601 ast: &program.ast,
602 settings: &Default::default(),
603 },
604 CacheInformation {
605 ast: &program.ast,
606 settings: &exec_ctxt.settings,
607 },
608 )
609 .await;
610
611 assert_eq!(result, CacheResult::NoAction(true));
612
613 let old_settings = exec_ctxt.settings.clone();
615 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
616
617 let result = get_changed_program(
618 CacheInformation {
619 ast: &program.ast,
620 settings: &old_settings,
621 },
622 CacheInformation {
623 ast: &program.ast,
624 settings: &exec_ctxt.settings,
625 },
626 )
627 .await;
628
629 assert_eq!(result, CacheResult::NoAction(true));
630
631 let old_settings = exec_ctxt.settings.clone();
633 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
634
635 let result = get_changed_program(
636 CacheInformation {
637 ast: &program.ast,
638 settings: &old_settings,
639 },
640 CacheInformation {
641 ast: &program.ast,
642 settings: &exec_ctxt.settings,
643 },
644 )
645 .await;
646
647 assert_eq!(result, CacheResult::NoAction(true));
648 exec_ctxt.close().await;
649 }
650
651 #[tokio::test(flavor = "multi_thread")]
654 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
655 let old_code = r#"@settings(defaultLengthUnit = in)
656startSketchOn(XY)
657"#;
658 let new_code = r#"@settings(defaultLengthUnit = mm)
659startSketchOn(XY)
660"#;
661
662 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
663
664 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
665 new_program.compute_digest();
666
667 let result = get_changed_program(
668 CacheInformation {
669 ast: &program.ast,
670 settings: &exec_ctxt.settings,
671 },
672 CacheInformation {
673 ast: &new_program.ast,
674 settings: &exec_ctxt.settings,
675 },
676 )
677 .await;
678
679 assert_eq!(
680 result,
681 CacheResult::ReExecute {
682 clear_scene: true,
683 reapply_settings: true,
684 program: new_program.ast,
685 }
686 );
687 exec_ctxt.close().await;
688 }
689
690 #[tokio::test(flavor = "multi_thread")]
693 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
694 let old_code = r#"@settings(defaultLengthUnit = in)
695startSketchOn(XY)
696"#;
697 let new_code = r#"
698startSketchOn(XY)
699"#;
700
701 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
702
703 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
704 new_program.compute_digest();
705
706 let result = get_changed_program(
707 CacheInformation {
708 ast: &program.ast,
709 settings: &exec_ctxt.settings,
710 },
711 CacheInformation {
712 ast: &new_program.ast,
713 settings: &exec_ctxt.settings,
714 },
715 )
716 .await;
717
718 assert_eq!(
719 result,
720 CacheResult::ReExecute {
721 clear_scene: true,
722 reapply_settings: true,
723 program: new_program.ast,
724 }
725 );
726 exec_ctxt.close().await;
727 }
728
729 #[tokio::test(flavor = "multi_thread")]
730 async fn test_multi_file_no_changes_does_not_reexecute() {
731 let code = r#"import "toBeImported.kcl" as importedCube
732
733importedCube
734
735sketch001 = startSketchOn(XZ)
736profile001 = startProfile(sketch001, at = [-134.53, -56.17])
737 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
738 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
739 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
740 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
741 |> close()
742extrude001 = extrude(profile001, length = 100)
743sketch003 = startSketchOn(extrude001, face = seg02)
744sketch002 = startSketchOn(extrude001, face = seg01)
745"#;
746
747 let other_file = (
748 std::path::PathBuf::from("toBeImported.kcl"),
749 r#"sketch001 = startSketchOn(XZ)
750profile001 = startProfile(sketch001, at = [281.54, 305.81])
751 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
752 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
753 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
754 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
755 |> close()
756extrude(profile001, length = 100)"#
757 .to_string(),
758 );
759
760 let tmp_dir = std::env::temp_dir();
761 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
762
763 let tmp_file = tmp_dir.join(other_file.0);
765 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
766 std::fs::write(tmp_file, other_file.1).unwrap();
767
768 let ExecTestResults { program, exec_ctxt, .. } =
769 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
770 .await
771 .unwrap();
772
773 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
774 new_program.compute_digest();
775
776 let result = get_changed_program(
777 CacheInformation {
778 ast: &program.ast,
779 settings: &exec_ctxt.settings,
780 },
781 CacheInformation {
782 ast: &new_program.ast,
783 settings: &exec_ctxt.settings,
784 },
785 )
786 .await;
787
788 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
789 panic!("Expected CheckImportsOnly, got {result:?}");
790 };
791
792 assert_eq!(reapply_settings, false);
793 exec_ctxt.close().await;
794 }
795
796 #[tokio::test(flavor = "multi_thread")]
797 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
798 let code = r#"import "toBeImported.kcl" as importedCube
799
800importedCube
801
802sketch001 = startSketchOn(XZ)
803profile001 = startProfile(sketch001, at = [-134.53, -56.17])
804 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
805 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
806 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
807 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
808 |> close()
809extrude001 = extrude(profile001, length = 100)
810sketch003 = startSketchOn(extrude001, face = seg02)
811sketch002 = startSketchOn(extrude001, face = seg01)
812"#;
813
814 let other_file = (
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 .to_string(),
825 );
826
827 let other_file2 = (
828 std::path::PathBuf::from("toBeImported.kcl"),
829 r#"sketch001 = startSketchOn(XZ)
830profile001 = startProfile(sketch001, at = [281.54, 305.81])
831 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
832 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
833 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
834 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
835 |> close()
836extrude(profile001, length = 100)
837|> translate(z=100)
838"#
839 .to_string(),
840 );
841
842 let tmp_dir = std::env::temp_dir();
843 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
844
845 let tmp_file = tmp_dir.join(other_file.0);
847 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
848 std::fs::write(&tmp_file, other_file.1).unwrap();
849
850 let ExecTestResults { program, exec_ctxt, .. } =
851 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
852 .await
853 .unwrap();
854
855 std::fs::write(tmp_file, other_file2.1).unwrap();
857
858 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
859 new_program.compute_digest();
860
861 let result = get_changed_program(
862 CacheInformation {
863 ast: &program.ast,
864 settings: &exec_ctxt.settings,
865 },
866 CacheInformation {
867 ast: &new_program.ast,
868 settings: &exec_ctxt.settings,
869 },
870 )
871 .await;
872
873 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
874 panic!("Expected CheckImportsOnly, got {result:?}");
875 };
876
877 assert_eq!(reapply_settings, false);
878 exec_ctxt.close().await;
879 }
880
881 #[tokio::test(flavor = "multi_thread")]
882 async fn test_get_changed_program_added_outer_attribute() {
883 let old_code = r#"import "tests/inputs/cube.step"
884"#;
885 let new_code = r#"@(coords = opengl)
886import "tests/inputs/cube.step"
887"#;
888
889 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
890
891 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
892 new_program.compute_digest();
893
894 let result = get_changed_program(
895 CacheInformation {
896 ast: &program.ast,
897 settings: &exec_ctxt.settings,
898 },
899 CacheInformation {
900 ast: &new_program.ast,
901 settings: &exec_ctxt.settings,
902 },
903 )
904 .await;
905
906 assert_eq!(
907 result,
908 CacheResult::ReExecute {
909 clear_scene: true,
910 reapply_settings: false,
911 program: new_program.ast,
912 }
913 );
914 exec_ctxt.close().await;
915 }
916
917 #[tokio::test(flavor = "multi_thread")]
918 async fn test_get_changed_program_different_outer_attribute() {
919 let old_code = r#"@(coords = vulkan)
920import "tests/inputs/cube.step"
921"#;
922 let new_code = r#"@(coords = opengl)
923import "tests/inputs/cube.step"
924"#;
925
926 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
927
928 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
929 new_program.compute_digest();
930
931 let result = get_changed_program(
932 CacheInformation {
933 ast: &program.ast,
934 settings: &exec_ctxt.settings,
935 },
936 CacheInformation {
937 ast: &new_program.ast,
938 settings: &exec_ctxt.settings,
939 },
940 )
941 .await;
942
943 assert_eq!(
944 result,
945 CacheResult::ReExecute {
946 clear_scene: true,
947 reapply_settings: false,
948 program: new_program.ast,
949 }
950 );
951 exec_ctxt.close().await;
952 }
953}