1use std::sync::Arc;
4
5use indexmap::IndexMap;
6use itertools::EitherOrBoth;
7use itertools::Itertools;
8use tokio::sync::RwLock;
9
10use crate::ExecOutcome;
11use crate::ExecutorContext;
12use crate::execution::EnvironmentRef;
13use crate::execution::ExecutorSettings;
14use crate::execution::annotations;
15use crate::execution::memory::Stack;
16use crate::execution::state::ModuleInfoMap;
17use crate::execution::state::{self as exec_state};
18use crate::front::Object;
19use crate::modules::ModuleId;
20use crate::modules::ModulePath;
21use crate::modules::ModuleSource;
22use crate::parsing::ast::types::Annotation;
23use crate::parsing::ast::types::Node;
24use crate::parsing::ast::types::Program;
25use crate::walk::Node as WalkNode;
26
27lazy_static::lazy_static! {
28 static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
30 static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
32}
33
34pub(super) async fn read_old_ast() -> Option<GlobalState> {
36 let old_ast = OLD_AST.read().await;
37 old_ast.clone()
38}
39
40pub(super) async fn write_old_ast(old_state: GlobalState) {
41 let mut old_ast = OLD_AST.write().await;
42 *old_ast = Some(old_state);
43}
44
45pub(crate) async fn read_old_memory() -> Option<SketchModeState> {
46 let old_mem = PREV_MEMORY.read().await;
47 old_mem.clone()
48}
49
50pub(crate) async fn write_old_memory(mem: SketchModeState) {
51 let mut old_mem = PREV_MEMORY.write().await;
52 *old_mem = Some(mem);
53}
54
55pub async fn bust_cache() {
56 let mut old_ast = OLD_AST.write().await;
57 *old_ast = None;
58}
59
60pub async fn clear_mem_cache() {
61 let mut old_mem = PREV_MEMORY.write().await;
62 *old_mem = None;
63}
64
65#[derive(Debug, Clone)]
67pub struct CacheInformation<'a> {
68 pub ast: &'a Node<Program>,
69 pub settings: &'a ExecutorSettings,
70}
71
72#[derive(Debug, Clone)]
74pub(super) struct GlobalState {
75 pub(super) main: ModuleState,
76 pub(super) exec_state: exec_state::GlobalState,
78 pub(super) settings: ExecutorSettings,
80}
81
82impl GlobalState {
83 pub fn new(
84 state: exec_state::ExecState,
85 settings: ExecutorSettings,
86 ast: Node<Program>,
87 result_env: EnvironmentRef,
88 ) -> Self {
89 Self {
90 main: ModuleState {
91 ast,
92 exec_state: state.mod_local,
93 result_env,
94 },
95 exec_state: state.global,
96 settings,
97 }
98 }
99
100 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
101 self.settings = settings;
102 self
103 }
104
105 pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
106 exec_state::ExecState {
107 global: self.exec_state.clone(),
108 mod_local: self.main.exec_state.clone(),
109 }
110 }
111
112 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
113 ExecOutcome {
116 variables: self.main.exec_state.variables(self.main.result_env),
117 filenames: self.exec_state.filenames(),
118 #[cfg(feature = "artifact-graph")]
119 operations: self.exec_state.root_module_artifacts.operations,
120 #[cfg(feature = "artifact-graph")]
121 artifact_graph: self.exec_state.artifacts.graph,
122 #[cfg(feature = "artifact-graph")]
123 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
124 #[cfg(feature = "artifact-graph")]
125 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
126 #[cfg(feature = "artifact-graph")]
127 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
128 issues: self.exec_state.issues,
129 default_planes: ctx.engine.get_default_planes().read().await.clone(),
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
136pub(super) struct ModuleState {
137 pub(super) ast: Node<Program>,
139 pub(super) exec_state: exec_state::ModuleState,
141 pub(super) result_env: EnvironmentRef,
143}
144
145#[derive(Debug, Clone)]
147pub(crate) struct SketchModeState {
148 pub stack: Stack,
150 pub module_infos: ModuleInfoMap,
152 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
154 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
156 #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
158 pub scene_objects: Vec<Object>,
159}
160
161#[cfg(test)]
162impl SketchModeState {
163 pub(crate) fn new_for_tests() -> Self {
164 Self {
165 stack: Stack::new_for_tests(),
166 module_infos: ModuleInfoMap::default(),
167 path_to_source_id: Default::default(),
168 id_to_source: Default::default(),
169 scene_objects: Vec::new(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq)]
176#[allow(clippy::large_enum_variant)]
177pub(super) enum CacheResult {
178 ReExecute {
179 clear_scene: bool,
181 reapply_settings: bool,
183 program: Node<Program>,
185 },
186 CheckImportsOnly {
190 reapply_settings: bool,
192 ast: Node<Program>,
194 },
195 NoAction(bool),
197}
198
199pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
207 let mut reapply_settings = false;
208
209 if old.settings != new.settings {
212 reapply_settings = true;
215 }
216
217 if old.ast == new.ast {
220 if !old.ast.has_import_statements() {
224 return CacheResult::NoAction(reapply_settings);
225 }
226
227 return CacheResult::CheckImportsOnly {
229 reapply_settings,
230 ast: old.ast.clone(),
231 };
232 }
233
234 let mut old_ast = old.ast.clone();
236 let mut new_ast = new.ast.clone();
237
238 old_ast.compute_digest();
241 new_ast.compute_digest();
242
243 if old_ast.digest == new_ast.digest {
245 if !old.ast.has_import_statements() {
249 return CacheResult::NoAction(reapply_settings);
250 }
251
252 return CacheResult::CheckImportsOnly {
254 reapply_settings,
255 ast: old.ast.clone(),
256 };
257 }
258
259 if !old_ast
261 .inner_attrs
262 .iter()
263 .filter(annotations::is_significant)
264 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
265 .all(|pair| {
266 match pair {
267 EitherOrBoth::Both(old, new) => {
268 let Annotation { name, properties, .. } = &old.inner;
271 let Annotation {
272 name: new_name,
273 properties: new_properties,
274 ..
275 } = &new.inner;
276
277 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
278 && properties
279 .as_ref()
280 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
281 == new_properties
282 .as_ref()
283 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
284 }
285 _ => false,
286 }
287 })
288 {
289 return CacheResult::ReExecute {
293 clear_scene: true,
294 reapply_settings: true,
295 program: new.ast.clone(),
296 };
297 }
298
299 generate_changed_program(old_ast, new_ast, reapply_settings)
301}
302
303fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
315 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
316 let old_node: WalkNode = old.into();
317 let new_node: WalkNode = new.into();
318 old_node.digest() == new_node.digest()
319 }) {
320 return CacheResult::ReExecute {
326 clear_scene: true,
327 reapply_settings,
328 program: new_ast,
329 };
330 }
331
332 match new_ast.body.len().cmp(&old_ast.body.len()) {
336 std::cmp::Ordering::Less => {
337 CacheResult::ReExecute {
346 clear_scene: true,
347 reapply_settings,
348 program: new_ast,
349 }
350 }
351 std::cmp::Ordering::Greater => {
352 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
360
361 CacheResult::ReExecute {
362 clear_scene: false,
363 reapply_settings,
364 program: new_ast,
365 }
366 }
367 std::cmp::Ordering::Equal => {
368 CacheResult::NoAction(reapply_settings)
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use pretty_assertions::assert_eq;
385
386 use super::*;
387 use crate::execution::ExecTestResults;
388 use crate::execution::parse_execute;
389 use crate::execution::parse_execute_with_project_dir;
390
391 #[tokio::test(flavor = "multi_thread")]
392 async fn test_get_changed_program_same_code() {
393 let new = r#"// Remove the end face for the extrusion.
394firstSketch = startSketchOn(XY)
395 |> startProfile(at = [-12, 12])
396 |> line(end = [24, 0])
397 |> line(end = [0, -24])
398 |> line(end = [-24, 0])
399 |> close()
400 |> extrude(length = 6)
401
402// Remove the end face for the extrusion.
403shell(firstSketch, faces = [END], thickness = 0.25)"#;
404
405 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
406
407 let result = get_changed_program(
408 CacheInformation {
409 ast: &program.ast,
410 settings: &exec_ctxt.settings,
411 },
412 CacheInformation {
413 ast: &program.ast,
414 settings: &exec_ctxt.settings,
415 },
416 )
417 .await;
418
419 assert_eq!(result, CacheResult::NoAction(false));
420 exec_ctxt.close().await;
421 }
422
423 #[tokio::test(flavor = "multi_thread")]
424 async fn test_get_changed_program_same_code_changed_whitespace() {
425 let old = r#" // Remove the end face for the extrusion.
426firstSketch = startSketchOn(XY)
427 |> startProfile(at = [-12, 12])
428 |> line(end = [24, 0])
429 |> line(end = [0, -24])
430 |> line(end = [-24, 0])
431 |> close()
432 |> extrude(length = 6)
433
434// Remove the end face for the extrusion.
435shell(firstSketch, faces = [END], thickness = 0.25) "#;
436
437 let new = r#"// Remove the end face for the extrusion.
438firstSketch = startSketchOn(XY)
439 |> startProfile(at = [-12, 12])
440 |> line(end = [24, 0])
441 |> line(end = [0, -24])
442 |> line(end = [-24, 0])
443 |> close()
444 |> extrude(length = 6)
445
446// Remove the end face for the extrusion.
447shell(firstSketch, faces = [END], thickness = 0.25)"#;
448
449 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
450
451 let program_new = crate::Program::parse_no_errs(new).unwrap();
452
453 let result = get_changed_program(
454 CacheInformation {
455 ast: &program.ast,
456 settings: &exec_ctxt.settings,
457 },
458 CacheInformation {
459 ast: &program_new.ast,
460 settings: &exec_ctxt.settings,
461 },
462 )
463 .await;
464
465 assert_eq!(result, CacheResult::NoAction(false));
466 exec_ctxt.close().await;
467 }
468
469 #[tokio::test(flavor = "multi_thread")]
470 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
471 let old = r#" // Removed the end face for the extrusion.
472firstSketch = startSketchOn(XY)
473 |> startProfile(at = [-12, 12])
474 |> line(end = [24, 0])
475 |> line(end = [0, -24])
476 |> line(end = [-24, 0])
477 |> close()
478 |> extrude(length = 6)
479
480// Remove the end face for the extrusion.
481shell(firstSketch, faces = [END], thickness = 0.25) "#;
482
483 let new = r#"// Remove the end face for the extrusion.
484firstSketch = startSketchOn(XY)
485 |> startProfile(at = [-12, 12])
486 |> line(end = [24, 0])
487 |> line(end = [0, -24])
488 |> line(end = [-24, 0])
489 |> close()
490 |> extrude(length = 6)
491
492// Remove the end face for the extrusion.
493shell(firstSketch, faces = [END], thickness = 0.25)"#;
494
495 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
496
497 let program_new = crate::Program::parse_no_errs(new).unwrap();
498
499 let result = get_changed_program(
500 CacheInformation {
501 ast: &program.ast,
502 settings: &exec_ctxt.settings,
503 },
504 CacheInformation {
505 ast: &program_new.ast,
506 settings: &exec_ctxt.settings,
507 },
508 )
509 .await;
510
511 assert_eq!(result, CacheResult::NoAction(false));
512 exec_ctxt.close().await;
513 }
514
515 #[tokio::test(flavor = "multi_thread")]
516 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
517 let old = r#"@foo(whatever = whatever)
518@bar
519// Removed the end face for the extrusion.
520firstSketch = startSketchOn(XY)
521 |> startProfile(at = [-12, 12])
522 |> line(end = [24, 0])
523 |> line(end = [0, -24])
524 |> line(end = [-24, 0]) // my thing
525 |> close()
526 |> extrude(length = 6)
527
528// Remove the end face for the extrusion.
529shell(firstSketch, faces = [END], thickness = 0.25) "#;
530
531 let new = r#"@foo(whatever = 42)
532@baz
533// Remove the end face for the extrusion.
534firstSketch = startSketchOn(XY)
535 |> startProfile(at = [-12, 12])
536 |> line(end = [24, 0])
537 |> line(end = [0, -24])
538 |> line(end = [-24, 0])
539 |> close()
540 |> extrude(length = 6)
541
542// Remove the end face for the extrusion.
543shell(firstSketch, faces = [END], thickness = 0.25)"#;
544
545 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
546
547 let program_new = crate::Program::parse_no_errs(new).unwrap();
548
549 let result = get_changed_program(
550 CacheInformation {
551 ast: &program.ast,
552 settings: &exec_ctxt.settings,
553 },
554 CacheInformation {
555 ast: &program_new.ast,
556 settings: &exec_ctxt.settings,
557 },
558 )
559 .await;
560
561 assert_eq!(result, CacheResult::NoAction(false));
562 exec_ctxt.close().await;
563 }
564
565 #[tokio::test(flavor = "multi_thread")]
567 async fn test_get_changed_program_same_code_but_different_grid_setting() {
568 let new = r#"// Remove the end face for the extrusion.
569firstSketch = startSketchOn(XY)
570 |> startProfile(at = [-12, 12])
571 |> line(end = [24, 0])
572 |> line(end = [0, -24])
573 |> line(end = [-24, 0])
574 |> close()
575 |> extrude(length = 6)
576
577// Remove the end face for the extrusion.
578shell(firstSketch, faces = [END], thickness = 0.25)"#;
579
580 let ExecTestResults {
581 program, mut exec_ctxt, ..
582 } = parse_execute(new).await.unwrap();
583
584 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
586
587 let result = get_changed_program(
588 CacheInformation {
589 ast: &program.ast,
590 settings: &Default::default(),
591 },
592 CacheInformation {
593 ast: &program.ast,
594 settings: &exec_ctxt.settings,
595 },
596 )
597 .await;
598
599 assert_eq!(result, CacheResult::NoAction(true));
600 exec_ctxt.close().await;
601 }
602
603 #[tokio::test(flavor = "multi_thread")]
605 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
606 let new = r#"// Remove the end face for the extrusion.
607firstSketch = startSketchOn(XY)
608 |> startProfile(at = [-12, 12])
609 |> line(end = [24, 0])
610 |> line(end = [0, -24])
611 |> line(end = [-24, 0])
612 |> close()
613 |> extrude(length = 6)
614
615// Remove the end face for the extrusion.
616shell(firstSketch, faces = [END], thickness = 0.25)"#;
617
618 let ExecTestResults {
619 program, mut exec_ctxt, ..
620 } = parse_execute(new).await.unwrap();
621
622 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
624
625 let result = get_changed_program(
626 CacheInformation {
627 ast: &program.ast,
628 settings: &Default::default(),
629 },
630 CacheInformation {
631 ast: &program.ast,
632 settings: &exec_ctxt.settings,
633 },
634 )
635 .await;
636
637 assert_eq!(result, CacheResult::NoAction(true));
638
639 let old_settings = exec_ctxt.settings.clone();
641 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
642
643 let result = get_changed_program(
644 CacheInformation {
645 ast: &program.ast,
646 settings: &old_settings,
647 },
648 CacheInformation {
649 ast: &program.ast,
650 settings: &exec_ctxt.settings,
651 },
652 )
653 .await;
654
655 assert_eq!(result, CacheResult::NoAction(true));
656
657 let old_settings = exec_ctxt.settings.clone();
659 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
660
661 let result = get_changed_program(
662 CacheInformation {
663 ast: &program.ast,
664 settings: &old_settings,
665 },
666 CacheInformation {
667 ast: &program.ast,
668 settings: &exec_ctxt.settings,
669 },
670 )
671 .await;
672
673 assert_eq!(result, CacheResult::NoAction(true));
674 exec_ctxt.close().await;
675 }
676
677 #[tokio::test(flavor = "multi_thread")]
680 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
681 let old_code = r#"@settings(defaultLengthUnit = in)
682startSketchOn(XY)
683"#;
684 let new_code = r#"@settings(defaultLengthUnit = mm)
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")]
719 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
720 let old_code = r#"@settings(defaultLengthUnit = in)
721startSketchOn(XY)
722"#;
723 let new_code = r#"
724startSketchOn(XY)
725"#;
726
727 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
728
729 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
730 new_program.compute_digest();
731
732 let result = get_changed_program(
733 CacheInformation {
734 ast: &program.ast,
735 settings: &exec_ctxt.settings,
736 },
737 CacheInformation {
738 ast: &new_program.ast,
739 settings: &exec_ctxt.settings,
740 },
741 )
742 .await;
743
744 assert_eq!(
745 result,
746 CacheResult::ReExecute {
747 clear_scene: true,
748 reapply_settings: true,
749 program: new_program.ast,
750 }
751 );
752 exec_ctxt.close().await;
753 }
754
755 #[tokio::test(flavor = "multi_thread")]
756 async fn test_multi_file_no_changes_does_not_reexecute() {
757 let code = r#"import "toBeImported.kcl" as importedCube
758
759importedCube
760
761sketch001 = startSketchOn(XZ)
762profile001 = startProfile(sketch001, at = [-134.53, -56.17])
763 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
764 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
765 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
766 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
767 |> close()
768extrude001 = extrude(profile001, length = 100)
769sketch003 = startSketchOn(extrude001, face = seg02)
770sketch002 = startSketchOn(extrude001, face = seg01)
771"#;
772
773 let other_file = (
774 std::path::PathBuf::from("toBeImported.kcl"),
775 r#"sketch001 = startSketchOn(XZ)
776profile001 = startProfile(sketch001, at = [281.54, 305.81])
777 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
778 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
779 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
780 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
781 |> close()
782extrude(profile001, length = 100)"#
783 .to_string(),
784 );
785
786 let tmp_dir = std::env::temp_dir();
787 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
788
789 let tmp_file = tmp_dir.join(other_file.0);
791 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
792 std::fs::write(tmp_file, other_file.1).unwrap();
793
794 let ExecTestResults { program, exec_ctxt, .. } =
795 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
796 .await
797 .unwrap();
798
799 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
800 new_program.compute_digest();
801
802 let result = get_changed_program(
803 CacheInformation {
804 ast: &program.ast,
805 settings: &exec_ctxt.settings,
806 },
807 CacheInformation {
808 ast: &new_program.ast,
809 settings: &exec_ctxt.settings,
810 },
811 )
812 .await;
813
814 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
815 panic!("Expected CheckImportsOnly, got {result:?}");
816 };
817
818 assert_eq!(reapply_settings, false);
819 exec_ctxt.close().await;
820 }
821
822 #[tokio::test(flavor = "multi_thread")]
823 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
824 let code = r#"import "toBeImported.kcl" as importedCube
825
826importedCube
827
828sketch001 = startSketchOn(XZ)
829profile001 = startProfile(sketch001, at = [-134.53, -56.17])
830 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
831 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
832 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
833 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
834 |> close()
835extrude001 = extrude(profile001, length = 100)
836sketch003 = startSketchOn(extrude001, face = seg02)
837sketch002 = startSketchOn(extrude001, face = seg01)
838"#;
839
840 let other_file = (
841 std::path::PathBuf::from("toBeImported.kcl"),
842 r#"sketch001 = startSketchOn(XZ)
843profile001 = startProfile(sketch001, at = [281.54, 305.81])
844 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
845 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
846 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
847 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
848 |> close()
849extrude(profile001, length = 100)"#
850 .to_string(),
851 );
852
853 let other_file2 = (
854 std::path::PathBuf::from("toBeImported.kcl"),
855 r#"sketch001 = startSketchOn(XZ)
856profile001 = startProfile(sketch001, at = [281.54, 305.81])
857 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
858 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
859 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
860 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
861 |> close()
862extrude(profile001, length = 100)
863|> translate(z=100)
864"#
865 .to_string(),
866 );
867
868 let tmp_dir = std::env::temp_dir();
869 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
870
871 let tmp_file = tmp_dir.join(other_file.0);
873 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
874 std::fs::write(&tmp_file, other_file.1).unwrap();
875
876 let ExecTestResults { program, exec_ctxt, .. } =
877 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
878 .await
879 .unwrap();
880
881 std::fs::write(tmp_file, other_file2.1).unwrap();
883
884 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
885 new_program.compute_digest();
886
887 let result = get_changed_program(
888 CacheInformation {
889 ast: &program.ast,
890 settings: &exec_ctxt.settings,
891 },
892 CacheInformation {
893 ast: &new_program.ast,
894 settings: &exec_ctxt.settings,
895 },
896 )
897 .await;
898
899 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
900 panic!("Expected CheckImportsOnly, got {result:?}");
901 };
902
903 assert_eq!(reapply_settings, false);
904 exec_ctxt.close().await;
905 }
906
907 #[tokio::test(flavor = "multi_thread")]
908 async fn test_get_changed_program_added_outer_attribute() {
909 let old_code = r#"import "tests/inputs/cube.step"
910"#;
911 let new_code = r#"@(coords = opengl)
912import "tests/inputs/cube.step"
913"#;
914
915 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
916
917 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
918 new_program.compute_digest();
919
920 let result = get_changed_program(
921 CacheInformation {
922 ast: &program.ast,
923 settings: &exec_ctxt.settings,
924 },
925 CacheInformation {
926 ast: &new_program.ast,
927 settings: &exec_ctxt.settings,
928 },
929 )
930 .await;
931
932 assert_eq!(
933 result,
934 CacheResult::ReExecute {
935 clear_scene: true,
936 reapply_settings: false,
937 program: new_program.ast,
938 }
939 );
940 exec_ctxt.close().await;
941 }
942
943 #[tokio::test(flavor = "multi_thread")]
944 async fn test_get_changed_program_different_outer_attribute() {
945 let old_code = r#"@(coords = vulkan)
946import "tests/inputs/cube.step"
947"#;
948 let new_code = r#"@(coords = opengl)
949import "tests/inputs/cube.step"
950"#;
951
952 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
953
954 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
955 new_program.compute_digest();
956
957 let result = get_changed_program(
958 CacheInformation {
959 ast: &program.ast,
960 settings: &exec_ctxt.settings,
961 },
962 CacheInformation {
963 ast: &new_program.ast,
964 settings: &exec_ctxt.settings,
965 },
966 )
967 .await;
968
969 assert_eq!(
970 result,
971 CacheResult::ReExecute {
972 clear_scene: true,
973 reapply_settings: false,
974 program: new_program.ast,
975 }
976 );
977 exec_ctxt.close().await;
978 }
979}