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 pub fn mock_memory_state(&self) -> SketchModeState {
134 let mut stack = self.main.exec_state.stack.deep_clone();
135 stack.restore_env(self.main.result_env);
136
137 SketchModeState {
138 stack,
139 module_infos: self.exec_state.module_infos.clone(),
140 path_to_source_id: self.exec_state.path_to_source_id.clone(),
141 id_to_source: self.exec_state.id_to_source.clone(),
142 #[cfg(feature = "artifact-graph")]
143 scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
144 #[cfg(not(feature = "artifact-graph"))]
145 scene_objects: Default::default(),
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
152pub(super) struct ModuleState {
153 pub(super) ast: Node<Program>,
155 pub(super) exec_state: exec_state::ModuleState,
157 pub(super) result_env: EnvironmentRef,
159}
160
161#[derive(Debug, Clone)]
163pub(crate) struct SketchModeState {
164 pub stack: Stack,
166 pub module_infos: ModuleInfoMap,
168 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
170 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
172 #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
174 pub scene_objects: Vec<Object>,
175}
176
177#[cfg(test)]
178impl SketchModeState {
179 pub(crate) fn new_for_tests() -> Self {
180 Self {
181 stack: Stack::new_for_tests(),
182 module_infos: ModuleInfoMap::default(),
183 path_to_source_id: Default::default(),
184 id_to_source: Default::default(),
185 scene_objects: Vec::new(),
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq)]
192#[allow(clippy::large_enum_variant)]
193pub(super) enum CacheResult {
194 ReExecute {
195 clear_scene: bool,
197 reapply_settings: bool,
199 program: Node<Program>,
201 },
202 CheckImportsOnly {
206 reapply_settings: bool,
208 ast: Node<Program>,
210 },
211 NoAction(bool),
213}
214
215pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
223 let mut reapply_settings = false;
224
225 if old.settings != new.settings {
228 reapply_settings = true;
231 }
232
233 if old.ast == new.ast {
236 if !old.ast.has_import_statements() {
240 return CacheResult::NoAction(reapply_settings);
241 }
242
243 return CacheResult::CheckImportsOnly {
245 reapply_settings,
246 ast: old.ast.clone(),
247 };
248 }
249
250 let mut old_ast = old.ast.clone();
252 let mut new_ast = new.ast.clone();
253
254 old_ast.compute_digest();
257 new_ast.compute_digest();
258
259 if old_ast.digest == new_ast.digest {
261 if !old.ast.has_import_statements() {
265 return CacheResult::NoAction(reapply_settings);
266 }
267
268 return CacheResult::CheckImportsOnly {
270 reapply_settings,
271 ast: old.ast.clone(),
272 };
273 }
274
275 if !old_ast
277 .inner_attrs
278 .iter()
279 .filter(annotations::is_significant)
280 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
281 .all(|pair| {
282 match pair {
283 EitherOrBoth::Both(old, new) => {
284 let Annotation { name, properties, .. } = &old.inner;
287 let Annotation {
288 name: new_name,
289 properties: new_properties,
290 ..
291 } = &new.inner;
292
293 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
294 && properties
295 .as_ref()
296 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
297 == new_properties
298 .as_ref()
299 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
300 }
301 _ => false,
302 }
303 })
304 {
305 return CacheResult::ReExecute {
309 clear_scene: true,
310 reapply_settings: true,
311 program: new.ast.clone(),
312 };
313 }
314
315 generate_changed_program(old_ast, new_ast, reapply_settings)
317}
318
319fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
331 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
332 let old_node: WalkNode = old.into();
333 let new_node: WalkNode = new.into();
334 old_node.digest() == new_node.digest()
335 }) {
336 return CacheResult::ReExecute {
342 clear_scene: true,
343 reapply_settings,
344 program: new_ast,
345 };
346 }
347
348 match new_ast.body.len().cmp(&old_ast.body.len()) {
352 std::cmp::Ordering::Less => {
353 CacheResult::ReExecute {
362 clear_scene: true,
363 reapply_settings,
364 program: new_ast,
365 }
366 }
367 std::cmp::Ordering::Greater => {
368 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
376
377 CacheResult::ReExecute {
378 clear_scene: false,
379 reapply_settings,
380 program: new_ast,
381 }
382 }
383 std::cmp::Ordering::Equal => {
384 CacheResult::NoAction(reapply_settings)
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use pretty_assertions::assert_eq;
401
402 use super::*;
403 use crate::execution::ExecTestResults;
404 use crate::execution::parse_execute;
405 use crate::execution::parse_execute_with_project_dir;
406
407 #[tokio::test(flavor = "multi_thread")]
408 async fn test_get_changed_program_same_code() {
409 let new = r#"// Remove the end face for the extrusion.
410firstSketch = startSketchOn(XY)
411 |> startProfile(at = [-12, 12])
412 |> line(end = [24, 0])
413 |> line(end = [0, -24])
414 |> line(end = [-24, 0])
415 |> close()
416 |> extrude(length = 6)
417
418// Remove the end face for the extrusion.
419shell(firstSketch, faces = [END], thickness = 0.25)"#;
420
421 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
422
423 let result = get_changed_program(
424 CacheInformation {
425 ast: &program.ast,
426 settings: &exec_ctxt.settings,
427 },
428 CacheInformation {
429 ast: &program.ast,
430 settings: &exec_ctxt.settings,
431 },
432 )
433 .await;
434
435 assert_eq!(result, CacheResult::NoAction(false));
436 exec_ctxt.close().await;
437 }
438
439 #[tokio::test(flavor = "multi_thread")]
440 async fn test_get_changed_program_same_code_changed_whitespace() {
441 let old = r#" // Remove the end face for the extrusion.
442firstSketch = startSketchOn(XY)
443 |> startProfile(at = [-12, 12])
444 |> line(end = [24, 0])
445 |> line(end = [0, -24])
446 |> line(end = [-24, 0])
447 |> close()
448 |> extrude(length = 6)
449
450// Remove the end face for the extrusion.
451shell(firstSketch, faces = [END], thickness = 0.25) "#;
452
453 let new = r#"// Remove the end face for the extrusion.
454firstSketch = startSketchOn(XY)
455 |> startProfile(at = [-12, 12])
456 |> line(end = [24, 0])
457 |> line(end = [0, -24])
458 |> line(end = [-24, 0])
459 |> close()
460 |> extrude(length = 6)
461
462// Remove the end face for the extrusion.
463shell(firstSketch, faces = [END], thickness = 0.25)"#;
464
465 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
466
467 let program_new = crate::Program::parse_no_errs(new).unwrap();
468
469 let result = get_changed_program(
470 CacheInformation {
471 ast: &program.ast,
472 settings: &exec_ctxt.settings,
473 },
474 CacheInformation {
475 ast: &program_new.ast,
476 settings: &exec_ctxt.settings,
477 },
478 )
479 .await;
480
481 assert_eq!(result, CacheResult::NoAction(false));
482 exec_ctxt.close().await;
483 }
484
485 #[tokio::test(flavor = "multi_thread")]
486 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
487 let old = r#" // Removed the end face for the extrusion.
488firstSketch = startSketchOn(XY)
489 |> startProfile(at = [-12, 12])
490 |> line(end = [24, 0])
491 |> line(end = [0, -24])
492 |> line(end = [-24, 0])
493 |> close()
494 |> extrude(length = 6)
495
496// Remove the end face for the extrusion.
497shell(firstSketch, faces = [END], thickness = 0.25) "#;
498
499 let new = r#"// Remove the end face for the extrusion.
500firstSketch = startSketchOn(XY)
501 |> startProfile(at = [-12, 12])
502 |> line(end = [24, 0])
503 |> line(end = [0, -24])
504 |> line(end = [-24, 0])
505 |> close()
506 |> extrude(length = 6)
507
508// Remove the end face for the extrusion.
509shell(firstSketch, faces = [END], thickness = 0.25)"#;
510
511 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
512
513 let program_new = crate::Program::parse_no_errs(new).unwrap();
514
515 let result = get_changed_program(
516 CacheInformation {
517 ast: &program.ast,
518 settings: &exec_ctxt.settings,
519 },
520 CacheInformation {
521 ast: &program_new.ast,
522 settings: &exec_ctxt.settings,
523 },
524 )
525 .await;
526
527 assert_eq!(result, CacheResult::NoAction(false));
528 exec_ctxt.close().await;
529 }
530
531 #[tokio::test(flavor = "multi_thread")]
532 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
533 let old = r#"@foo(whatever = whatever)
534@bar
535// Removed the end face for the extrusion.
536firstSketch = startSketchOn(XY)
537 |> startProfile(at = [-12, 12])
538 |> line(end = [24, 0])
539 |> line(end = [0, -24])
540 |> line(end = [-24, 0]) // my thing
541 |> close()
542 |> extrude(length = 6)
543
544// Remove the end face for the extrusion.
545shell(firstSketch, faces = [END], thickness = 0.25) "#;
546
547 let new = r#"@foo(whatever = 42)
548@baz
549// Remove the end face for the extrusion.
550firstSketch = startSketchOn(XY)
551 |> startProfile(at = [-12, 12])
552 |> line(end = [24, 0])
553 |> line(end = [0, -24])
554 |> line(end = [-24, 0])
555 |> close()
556 |> extrude(length = 6)
557
558// Remove the end face for the extrusion.
559shell(firstSketch, faces = [END], thickness = 0.25)"#;
560
561 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
562
563 let program_new = crate::Program::parse_no_errs(new).unwrap();
564
565 let result = get_changed_program(
566 CacheInformation {
567 ast: &program.ast,
568 settings: &exec_ctxt.settings,
569 },
570 CacheInformation {
571 ast: &program_new.ast,
572 settings: &exec_ctxt.settings,
573 },
574 )
575 .await;
576
577 assert_eq!(result, CacheResult::NoAction(false));
578 exec_ctxt.close().await;
579 }
580
581 #[tokio::test(flavor = "multi_thread")]
583 async fn test_get_changed_program_same_code_but_different_grid_setting() {
584 let new = r#"// Remove the end face for the extrusion.
585firstSketch = startSketchOn(XY)
586 |> startProfile(at = [-12, 12])
587 |> line(end = [24, 0])
588 |> line(end = [0, -24])
589 |> line(end = [-24, 0])
590 |> close()
591 |> extrude(length = 6)
592
593// Remove the end face for the extrusion.
594shell(firstSketch, faces = [END], thickness = 0.25)"#;
595
596 let ExecTestResults {
597 program, mut exec_ctxt, ..
598 } = parse_execute(new).await.unwrap();
599
600 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
602
603 let result = get_changed_program(
604 CacheInformation {
605 ast: &program.ast,
606 settings: &Default::default(),
607 },
608 CacheInformation {
609 ast: &program.ast,
610 settings: &exec_ctxt.settings,
611 },
612 )
613 .await;
614
615 assert_eq!(result, CacheResult::NoAction(true));
616 exec_ctxt.close().await;
617 }
618
619 #[tokio::test(flavor = "multi_thread")]
621 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
622 let new = r#"// Remove the end face for the extrusion.
623firstSketch = startSketchOn(XY)
624 |> startProfile(at = [-12, 12])
625 |> line(end = [24, 0])
626 |> line(end = [0, -24])
627 |> line(end = [-24, 0])
628 |> close()
629 |> extrude(length = 6)
630
631// Remove the end face for the extrusion.
632shell(firstSketch, faces = [END], thickness = 0.25)"#;
633
634 let ExecTestResults {
635 program, mut exec_ctxt, ..
636 } = parse_execute(new).await.unwrap();
637
638 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
640
641 let result = get_changed_program(
642 CacheInformation {
643 ast: &program.ast,
644 settings: &Default::default(),
645 },
646 CacheInformation {
647 ast: &program.ast,
648 settings: &exec_ctxt.settings,
649 },
650 )
651 .await;
652
653 assert_eq!(result, CacheResult::NoAction(true));
654
655 let old_settings = exec_ctxt.settings.clone();
657 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
658
659 let result = get_changed_program(
660 CacheInformation {
661 ast: &program.ast,
662 settings: &old_settings,
663 },
664 CacheInformation {
665 ast: &program.ast,
666 settings: &exec_ctxt.settings,
667 },
668 )
669 .await;
670
671 assert_eq!(result, CacheResult::NoAction(true));
672
673 let old_settings = exec_ctxt.settings.clone();
675 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
676
677 let result = get_changed_program(
678 CacheInformation {
679 ast: &program.ast,
680 settings: &old_settings,
681 },
682 CacheInformation {
683 ast: &program.ast,
684 settings: &exec_ctxt.settings,
685 },
686 )
687 .await;
688
689 assert_eq!(result, CacheResult::NoAction(true));
690 exec_ctxt.close().await;
691 }
692
693 #[tokio::test(flavor = "multi_thread")]
696 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
697 let old_code = r#"@settings(defaultLengthUnit = in)
698startSketchOn(XY)
699"#;
700 let new_code = r#"@settings(defaultLengthUnit = mm)
701startSketchOn(XY)
702"#;
703
704 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
705
706 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
707 new_program.compute_digest();
708
709 let result = get_changed_program(
710 CacheInformation {
711 ast: &program.ast,
712 settings: &exec_ctxt.settings,
713 },
714 CacheInformation {
715 ast: &new_program.ast,
716 settings: &exec_ctxt.settings,
717 },
718 )
719 .await;
720
721 assert_eq!(
722 result,
723 CacheResult::ReExecute {
724 clear_scene: true,
725 reapply_settings: true,
726 program: new_program.ast,
727 }
728 );
729 exec_ctxt.close().await;
730 }
731
732 #[tokio::test(flavor = "multi_thread")]
735 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
736 let old_code = r#"@settings(defaultLengthUnit = in)
737startSketchOn(XY)
738"#;
739 let new_code = r#"
740startSketchOn(XY)
741"#;
742
743 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
744
745 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
746 new_program.compute_digest();
747
748 let result = get_changed_program(
749 CacheInformation {
750 ast: &program.ast,
751 settings: &exec_ctxt.settings,
752 },
753 CacheInformation {
754 ast: &new_program.ast,
755 settings: &exec_ctxt.settings,
756 },
757 )
758 .await;
759
760 assert_eq!(
761 result,
762 CacheResult::ReExecute {
763 clear_scene: true,
764 reapply_settings: true,
765 program: new_program.ast,
766 }
767 );
768 exec_ctxt.close().await;
769 }
770
771 #[tokio::test(flavor = "multi_thread")]
772 async fn test_multi_file_no_changes_does_not_reexecute() {
773 let code = r#"import "toBeImported.kcl" as importedCube
774
775importedCube
776
777sketch001 = startSketchOn(XZ)
778profile001 = startProfile(sketch001, at = [-134.53, -56.17])
779 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
780 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
781 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
782 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
783 |> close()
784extrude001 = extrude(profile001, length = 100)
785sketch003 = startSketchOn(extrude001, face = seg02)
786sketch002 = startSketchOn(extrude001, face = seg01)
787"#;
788
789 let other_file = (
790 std::path::PathBuf::from("toBeImported.kcl"),
791 r#"sketch001 = startSketchOn(XZ)
792profile001 = startProfile(sketch001, at = [281.54, 305.81])
793 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
794 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
795 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
796 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
797 |> close()
798extrude(profile001, length = 100)"#
799 .to_string(),
800 );
801
802 let tmp_dir = std::env::temp_dir();
803 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
804
805 let tmp_file = tmp_dir.join(other_file.0);
807 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
808 std::fs::write(tmp_file, other_file.1).unwrap();
809
810 let ExecTestResults { program, exec_ctxt, .. } =
811 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
812 .await
813 .unwrap();
814
815 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
816 new_program.compute_digest();
817
818 let result = get_changed_program(
819 CacheInformation {
820 ast: &program.ast,
821 settings: &exec_ctxt.settings,
822 },
823 CacheInformation {
824 ast: &new_program.ast,
825 settings: &exec_ctxt.settings,
826 },
827 )
828 .await;
829
830 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
831 panic!("Expected CheckImportsOnly, got {result:?}");
832 };
833
834 assert_eq!(reapply_settings, false);
835 exec_ctxt.close().await;
836 }
837
838 #[tokio::test(flavor = "multi_thread")]
839 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
840 let code = r#"import "toBeImported.kcl" as importedCube
841
842importedCube
843
844sketch001 = startSketchOn(XZ)
845profile001 = startProfile(sketch001, at = [-134.53, -56.17])
846 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
847 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
848 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
849 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
850 |> close()
851extrude001 = extrude(profile001, length = 100)
852sketch003 = startSketchOn(extrude001, face = seg02)
853sketch002 = startSketchOn(extrude001, face = seg01)
854"#;
855
856 let other_file = (
857 std::path::PathBuf::from("toBeImported.kcl"),
858 r#"sketch001 = startSketchOn(XZ)
859profile001 = startProfile(sketch001, at = [281.54, 305.81])
860 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
861 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
862 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
863 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
864 |> close()
865extrude(profile001, length = 100)"#
866 .to_string(),
867 );
868
869 let other_file2 = (
870 std::path::PathBuf::from("toBeImported.kcl"),
871 r#"sketch001 = startSketchOn(XZ)
872profile001 = startProfile(sketch001, at = [281.54, 305.81])
873 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
874 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
875 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
876 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
877 |> close()
878extrude(profile001, length = 100)
879|> translate(z=100)
880"#
881 .to_string(),
882 );
883
884 let tmp_dir = std::env::temp_dir();
885 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
886
887 let tmp_file = tmp_dir.join(other_file.0);
889 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
890 std::fs::write(&tmp_file, other_file.1).unwrap();
891
892 let ExecTestResults { program, exec_ctxt, .. } =
893 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
894 .await
895 .unwrap();
896
897 std::fs::write(tmp_file, other_file2.1).unwrap();
899
900 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
901 new_program.compute_digest();
902
903 let result = get_changed_program(
904 CacheInformation {
905 ast: &program.ast,
906 settings: &exec_ctxt.settings,
907 },
908 CacheInformation {
909 ast: &new_program.ast,
910 settings: &exec_ctxt.settings,
911 },
912 )
913 .await;
914
915 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
916 panic!("Expected CheckImportsOnly, got {result:?}");
917 };
918
919 assert_eq!(reapply_settings, false);
920 exec_ctxt.close().await;
921 }
922
923 #[tokio::test(flavor = "multi_thread")]
924 async fn test_get_changed_program_added_outer_attribute() {
925 let old_code = r#"import "tests/inputs/cube.step"
926"#;
927 let new_code = r#"@(coords = opengl)
928import "tests/inputs/cube.step"
929"#;
930
931 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
932
933 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
934 new_program.compute_digest();
935
936 let result = get_changed_program(
937 CacheInformation {
938 ast: &program.ast,
939 settings: &exec_ctxt.settings,
940 },
941 CacheInformation {
942 ast: &new_program.ast,
943 settings: &exec_ctxt.settings,
944 },
945 )
946 .await;
947
948 assert_eq!(
949 result,
950 CacheResult::ReExecute {
951 clear_scene: true,
952 reapply_settings: false,
953 program: new_program.ast,
954 }
955 );
956 exec_ctxt.close().await;
957 }
958
959 #[tokio::test(flavor = "multi_thread")]
960 async fn test_get_changed_program_different_outer_attribute() {
961 let old_code = r#"@(coords = vulkan)
962import "tests/inputs/cube.step"
963"#;
964 let new_code = r#"@(coords = opengl)
965import "tests/inputs/cube.step"
966"#;
967
968 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
969
970 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
971 new_program.compute_digest();
972
973 let result = get_changed_program(
974 CacheInformation {
975 ast: &program.ast,
976 settings: &exec_ctxt.settings,
977 },
978 CacheInformation {
979 ast: &new_program.ast,
980 settings: &exec_ctxt.settings,
981 },
982 )
983 .await;
984
985 assert_eq!(
986 result,
987 CacheResult::ReExecute {
988 clear_scene: true,
989 reapply_settings: false,
990 program: new_program.ast,
991 }
992 );
993 exec_ctxt.close().await;
994 }
995}