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::ConstraintKey;
13use crate::execution::ConstraintState;
14use crate::execution::EnvironmentRef;
15use crate::execution::ExecutorSettings;
16use crate::execution::annotations;
17use crate::execution::memory::Stack;
18use crate::execution::state::ModuleInfoMap;
19use crate::execution::state::{self as exec_state};
20use crate::front::Object;
21use crate::front::ObjectId;
22use crate::modules::ModuleId;
23use crate::modules::ModulePath;
24use crate::modules::ModuleSource;
25use crate::parsing::ast::types::Annotation;
26use crate::parsing::ast::types::Node;
27use crate::parsing::ast::types::Program;
28use crate::walk::Node as WalkNode;
29
30lazy_static::lazy_static! {
31 static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
33 static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
35}
36
37pub(super) async fn read_old_ast() -> Option<GlobalState> {
39 let old_ast = OLD_AST.read().await;
40 old_ast.clone()
41}
42
43pub(super) async fn write_old_ast(old_state: GlobalState) {
44 let mut old_ast = OLD_AST.write().await;
45 *old_ast = Some(old_state);
46}
47
48pub(crate) async fn read_old_memory() -> Option<SketchModeState> {
49 let old_mem = PREV_MEMORY.read().await;
50 old_mem.clone()
51}
52
53pub(crate) async fn write_old_memory(mem: SketchModeState) {
54 let mut old_mem = PREV_MEMORY.write().await;
55 *old_mem = Some(mem);
56}
57
58pub async fn bust_cache() {
59 let mut old_ast = OLD_AST.write().await;
60 *old_ast = None;
61}
62
63pub async fn clear_mem_cache() {
64 let mut old_mem = PREV_MEMORY.write().await;
65 *old_mem = None;
66}
67
68#[derive(Debug, Clone)]
70pub struct CacheInformation<'a> {
71 pub ast: &'a Node<Program>,
72 pub settings: &'a ExecutorSettings,
73}
74
75#[derive(Debug, Clone)]
77pub(super) struct GlobalState {
78 pub(super) main: ModuleState,
79 pub(super) exec_state: exec_state::GlobalState,
81 pub(super) settings: ExecutorSettings,
83}
84
85impl GlobalState {
86 pub fn new(
87 state: exec_state::ExecState,
88 settings: ExecutorSettings,
89 ast: Node<Program>,
90 result_env: EnvironmentRef,
91 ) -> Self {
92 Self {
93 main: ModuleState {
94 ast,
95 exec_state: state.mod_local,
96 result_env,
97 },
98 exec_state: state.global,
99 settings,
100 }
101 }
102
103 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
104 self.settings = settings;
105 self
106 }
107
108 pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
109 exec_state::ExecState {
110 global: self.exec_state.clone(),
111 mod_local: self.main.exec_state.clone(),
112 }
113 }
114
115 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
116 ExecOutcome {
119 variables: self.main.exec_state.variables(self.main.result_env),
120 filenames: self.exec_state.filenames(),
121 #[cfg(feature = "artifact-graph")]
122 operations: self.exec_state.root_module_artifacts.operations,
123 #[cfg(feature = "artifact-graph")]
124 artifact_graph: self.exec_state.artifacts.graph,
125 #[cfg(feature = "artifact-graph")]
126 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
127 #[cfg(feature = "artifact-graph")]
128 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
129 #[cfg(feature = "artifact-graph")]
130 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
131 issues: self.exec_state.issues,
132 default_planes: ctx.engine.get_default_planes().read().await.clone(),
133 }
134 }
135
136 pub fn mock_memory_state(&self) -> SketchModeState {
137 let mut stack = self.main.exec_state.stack.deep_clone();
138 stack.restore_env(self.main.result_env);
139
140 SketchModeState {
141 stack,
142 module_infos: self.exec_state.module_infos.clone(),
143 path_to_source_id: self.exec_state.path_to_source_id.clone(),
144 id_to_source: self.exec_state.id_to_source.clone(),
145 constraint_state: self.main.exec_state.constraint_state.clone(),
146 #[cfg(feature = "artifact-graph")]
147 scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
148 #[cfg(not(feature = "artifact-graph"))]
149 scene_objects: Default::default(),
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
156pub(super) struct ModuleState {
157 pub(super) ast: Node<Program>,
159 pub(super) exec_state: exec_state::ModuleState,
161 pub(super) result_env: EnvironmentRef,
163}
164
165#[derive(Debug, Clone)]
167pub(crate) struct SketchModeState {
168 pub stack: Stack,
170 pub module_infos: ModuleInfoMap,
172 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
174 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
176 pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
178 #[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
180 pub scene_objects: Vec<Object>,
181}
182
183#[cfg(test)]
184impl SketchModeState {
185 pub(crate) fn new_for_tests() -> Self {
186 Self {
187 stack: Stack::new_for_tests(),
188 module_infos: ModuleInfoMap::default(),
189 path_to_source_id: Default::default(),
190 id_to_source: Default::default(),
191 constraint_state: Default::default(),
192 scene_objects: Vec::new(),
193 }
194 }
195}
196
197#[derive(Debug, Clone, PartialEq)]
199#[allow(clippy::large_enum_variant)]
200pub(super) enum CacheResult {
201 ReExecute {
202 clear_scene: bool,
204 reapply_settings: bool,
206 program: Node<Program>,
208 },
209 CheckImportsOnly {
213 reapply_settings: bool,
215 ast: Node<Program>,
217 },
218 NoAction(bool),
220}
221
222pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
230 let mut reapply_settings = false;
231
232 if old.settings != new.settings {
235 reapply_settings = true;
238 }
239
240 if old.ast == new.ast {
243 if !old.ast.has_import_statements() {
247 return CacheResult::NoAction(reapply_settings);
248 }
249
250 return CacheResult::CheckImportsOnly {
252 reapply_settings,
253 ast: old.ast.clone(),
254 };
255 }
256
257 let mut old_ast = old.ast.clone();
259 let mut new_ast = new.ast.clone();
260
261 old_ast.compute_digest();
264 new_ast.compute_digest();
265
266 if old_ast.digest == new_ast.digest {
268 if !old.ast.has_import_statements() {
272 return CacheResult::NoAction(reapply_settings);
273 }
274
275 return CacheResult::CheckImportsOnly {
277 reapply_settings,
278 ast: old.ast.clone(),
279 };
280 }
281
282 if !old_ast
284 .inner_attrs
285 .iter()
286 .filter(annotations::is_significant)
287 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
288 .all(|pair| {
289 match pair {
290 EitherOrBoth::Both(old, new) => {
291 let Annotation { name, properties, .. } = &old.inner;
294 let Annotation {
295 name: new_name,
296 properties: new_properties,
297 ..
298 } = &new.inner;
299
300 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
301 && properties
302 .as_ref()
303 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
304 == new_properties
305 .as_ref()
306 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
307 }
308 _ => false,
309 }
310 })
311 {
312 return CacheResult::ReExecute {
316 clear_scene: true,
317 reapply_settings: true,
318 program: new.ast.clone(),
319 };
320 }
321
322 generate_changed_program(old_ast, new_ast, reapply_settings)
324}
325
326fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
338 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
339 let old_node: WalkNode = old.into();
340 let new_node: WalkNode = new.into();
341 old_node.digest() == new_node.digest()
342 }) {
343 return CacheResult::ReExecute {
349 clear_scene: true,
350 reapply_settings,
351 program: new_ast,
352 };
353 }
354
355 match new_ast.body.len().cmp(&old_ast.body.len()) {
359 std::cmp::Ordering::Less => {
360 CacheResult::ReExecute {
369 clear_scene: true,
370 reapply_settings,
371 program: new_ast,
372 }
373 }
374 std::cmp::Ordering::Greater => {
375 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
383
384 CacheResult::ReExecute {
385 clear_scene: false,
386 reapply_settings,
387 program: new_ast,
388 }
389 }
390 std::cmp::Ordering::Equal => {
391 CacheResult::NoAction(reapply_settings)
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use pretty_assertions::assert_eq;
408
409 use super::*;
410 use crate::execution::ExecTestResults;
411 use crate::execution::parse_execute;
412 use crate::execution::parse_execute_with_project_dir;
413
414 #[tokio::test(flavor = "multi_thread")]
415 async fn test_get_changed_program_same_code() {
416 let new = r#"// Remove the end face for the extrusion.
417firstSketch = startSketchOn(XY)
418 |> startProfile(at = [-12, 12])
419 |> line(end = [24, 0])
420 |> line(end = [0, -24])
421 |> line(end = [-24, 0])
422 |> close()
423 |> extrude(length = 6)
424
425// Remove the end face for the extrusion.
426shell(firstSketch, faces = [END], thickness = 0.25)"#;
427
428 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
429
430 let result = get_changed_program(
431 CacheInformation {
432 ast: &program.ast,
433 settings: &exec_ctxt.settings,
434 },
435 CacheInformation {
436 ast: &program.ast,
437 settings: &exec_ctxt.settings,
438 },
439 )
440 .await;
441
442 assert_eq!(result, CacheResult::NoAction(false));
443 exec_ctxt.close().await;
444 }
445
446 #[tokio::test(flavor = "multi_thread")]
447 async fn test_get_changed_program_same_code_changed_whitespace() {
448 let old = r#" // Remove the end face for the extrusion.
449firstSketch = startSketchOn(XY)
450 |> startProfile(at = [-12, 12])
451 |> line(end = [24, 0])
452 |> line(end = [0, -24])
453 |> line(end = [-24, 0])
454 |> close()
455 |> extrude(length = 6)
456
457// Remove the end face for the extrusion.
458shell(firstSketch, faces = [END], thickness = 0.25) "#;
459
460 let new = r#"// Remove the end face for the extrusion.
461firstSketch = startSketchOn(XY)
462 |> startProfile(at = [-12, 12])
463 |> line(end = [24, 0])
464 |> line(end = [0, -24])
465 |> line(end = [-24, 0])
466 |> close()
467 |> extrude(length = 6)
468
469// Remove the end face for the extrusion.
470shell(firstSketch, faces = [END], thickness = 0.25)"#;
471
472 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
473
474 let program_new = crate::Program::parse_no_errs(new).unwrap();
475
476 let result = get_changed_program(
477 CacheInformation {
478 ast: &program.ast,
479 settings: &exec_ctxt.settings,
480 },
481 CacheInformation {
482 ast: &program_new.ast,
483 settings: &exec_ctxt.settings,
484 },
485 )
486 .await;
487
488 assert_eq!(result, CacheResult::NoAction(false));
489 exec_ctxt.close().await;
490 }
491
492 #[tokio::test(flavor = "multi_thread")]
493 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
494 let old = r#" // Removed 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 new = r#"// Remove the end face for the extrusion.
507firstSketch = startSketchOn(XY)
508 |> startProfile(at = [-12, 12])
509 |> line(end = [24, 0])
510 |> line(end = [0, -24])
511 |> line(end = [-24, 0])
512 |> close()
513 |> extrude(length = 6)
514
515// Remove the end face for the extrusion.
516shell(firstSketch, faces = [END], thickness = 0.25)"#;
517
518 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
519
520 let program_new = crate::Program::parse_no_errs(new).unwrap();
521
522 let result = get_changed_program(
523 CacheInformation {
524 ast: &program.ast,
525 settings: &exec_ctxt.settings,
526 },
527 CacheInformation {
528 ast: &program_new.ast,
529 settings: &exec_ctxt.settings,
530 },
531 )
532 .await;
533
534 assert_eq!(result, CacheResult::NoAction(false));
535 exec_ctxt.close().await;
536 }
537
538 #[tokio::test(flavor = "multi_thread")]
539 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
540 let old = r#"@foo(whatever = whatever)
541@bar
542// Removed 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]) // my thing
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 new = r#"@foo(whatever = 42)
555@baz
556// Remove the end face for the extrusion.
557firstSketch = startSketchOn(XY)
558 |> startProfile(at = [-12, 12])
559 |> line(end = [24, 0])
560 |> line(end = [0, -24])
561 |> line(end = [-24, 0])
562 |> close()
563 |> extrude(length = 6)
564
565// Remove the end face for the extrusion.
566shell(firstSketch, faces = [END], thickness = 0.25)"#;
567
568 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
569
570 let program_new = crate::Program::parse_no_errs(new).unwrap();
571
572 let result = get_changed_program(
573 CacheInformation {
574 ast: &program.ast,
575 settings: &exec_ctxt.settings,
576 },
577 CacheInformation {
578 ast: &program_new.ast,
579 settings: &exec_ctxt.settings,
580 },
581 )
582 .await;
583
584 assert_eq!(result, CacheResult::NoAction(false));
585 exec_ctxt.close().await;
586 }
587
588 #[tokio::test(flavor = "multi_thread")]
590 async fn test_get_changed_program_same_code_but_different_grid_setting() {
591 let new = r#"// Remove the end face for the extrusion.
592firstSketch = startSketchOn(XY)
593 |> startProfile(at = [-12, 12])
594 |> line(end = [24, 0])
595 |> line(end = [0, -24])
596 |> line(end = [-24, 0])
597 |> close()
598 |> extrude(length = 6)
599
600// Remove the end face for the extrusion.
601shell(firstSketch, faces = [END], thickness = 0.25)"#;
602
603 let ExecTestResults {
604 program, mut exec_ctxt, ..
605 } = parse_execute(new).await.unwrap();
606
607 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
609
610 let result = get_changed_program(
611 CacheInformation {
612 ast: &program.ast,
613 settings: &Default::default(),
614 },
615 CacheInformation {
616 ast: &program.ast,
617 settings: &exec_ctxt.settings,
618 },
619 )
620 .await;
621
622 assert_eq!(result, CacheResult::NoAction(true));
623 exec_ctxt.close().await;
624 }
625
626 #[tokio::test(flavor = "multi_thread")]
628 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
629 let new = r#"// Remove the end face for the extrusion.
630firstSketch = startSketchOn(XY)
631 |> startProfile(at = [-12, 12])
632 |> line(end = [24, 0])
633 |> line(end = [0, -24])
634 |> line(end = [-24, 0])
635 |> close()
636 |> extrude(length = 6)
637
638// Remove the end face for the extrusion.
639shell(firstSketch, faces = [END], thickness = 0.25)"#;
640
641 let ExecTestResults {
642 program, mut exec_ctxt, ..
643 } = parse_execute(new).await.unwrap();
644
645 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
647
648 let result = get_changed_program(
649 CacheInformation {
650 ast: &program.ast,
651 settings: &Default::default(),
652 },
653 CacheInformation {
654 ast: &program.ast,
655 settings: &exec_ctxt.settings,
656 },
657 )
658 .await;
659
660 assert_eq!(result, CacheResult::NoAction(true));
661
662 let old_settings = exec_ctxt.settings.clone();
664 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
665
666 let result = get_changed_program(
667 CacheInformation {
668 ast: &program.ast,
669 settings: &old_settings,
670 },
671 CacheInformation {
672 ast: &program.ast,
673 settings: &exec_ctxt.settings,
674 },
675 )
676 .await;
677
678 assert_eq!(result, CacheResult::NoAction(true));
679
680 let old_settings = exec_ctxt.settings.clone();
682 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
683
684 let result = get_changed_program(
685 CacheInformation {
686 ast: &program.ast,
687 settings: &old_settings,
688 },
689 CacheInformation {
690 ast: &program.ast,
691 settings: &exec_ctxt.settings,
692 },
693 )
694 .await;
695
696 assert_eq!(result, CacheResult::NoAction(true));
697 exec_ctxt.close().await;
698 }
699
700 #[tokio::test(flavor = "multi_thread")]
703 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
704 let old_code = r#"@settings(defaultLengthUnit = in)
705startSketchOn(XY)
706"#;
707 let new_code = r#"@settings(defaultLengthUnit = mm)
708startSketchOn(XY)
709"#;
710
711 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
712
713 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
714 new_program.compute_digest();
715
716 let result = get_changed_program(
717 CacheInformation {
718 ast: &program.ast,
719 settings: &exec_ctxt.settings,
720 },
721 CacheInformation {
722 ast: &new_program.ast,
723 settings: &exec_ctxt.settings,
724 },
725 )
726 .await;
727
728 assert_eq!(
729 result,
730 CacheResult::ReExecute {
731 clear_scene: true,
732 reapply_settings: true,
733 program: new_program.ast,
734 }
735 );
736 exec_ctxt.close().await;
737 }
738
739 #[tokio::test(flavor = "multi_thread")]
742 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
743 let old_code = r#"@settings(defaultLengthUnit = in)
744startSketchOn(XY)
745"#;
746 let new_code = r#"
747startSketchOn(XY)
748"#;
749
750 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
751
752 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
753 new_program.compute_digest();
754
755 let result = get_changed_program(
756 CacheInformation {
757 ast: &program.ast,
758 settings: &exec_ctxt.settings,
759 },
760 CacheInformation {
761 ast: &new_program.ast,
762 settings: &exec_ctxt.settings,
763 },
764 )
765 .await;
766
767 assert_eq!(
768 result,
769 CacheResult::ReExecute {
770 clear_scene: true,
771 reapply_settings: true,
772 program: new_program.ast,
773 }
774 );
775 exec_ctxt.close().await;
776 }
777
778 #[tokio::test(flavor = "multi_thread")]
779 async fn test_multi_file_no_changes_does_not_reexecute() {
780 let code = r#"import "toBeImported.kcl" as importedCube
781
782importedCube
783
784sketch001 = startSketchOn(XZ)
785profile001 = startProfile(sketch001, at = [-134.53, -56.17])
786 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
787 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
788 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
789 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
790 |> close()
791extrude001 = extrude(profile001, length = 100)
792sketch003 = startSketchOn(extrude001, face = seg02)
793sketch002 = startSketchOn(extrude001, face = seg01)
794"#;
795
796 let other_file = (
797 std::path::PathBuf::from("toBeImported.kcl"),
798 r#"sketch001 = startSketchOn(XZ)
799profile001 = startProfile(sketch001, at = [281.54, 305.81])
800 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
801 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
802 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
803 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
804 |> close()
805extrude(profile001, length = 100)"#
806 .to_string(),
807 );
808
809 let tmp_dir = std::env::temp_dir();
810 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
811
812 let tmp_file = tmp_dir.join(other_file.0);
814 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
815 std::fs::write(tmp_file, other_file.1).unwrap();
816
817 let ExecTestResults { program, exec_ctxt, .. } =
818 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
819 .await
820 .unwrap();
821
822 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
823 new_program.compute_digest();
824
825 let result = get_changed_program(
826 CacheInformation {
827 ast: &program.ast,
828 settings: &exec_ctxt.settings,
829 },
830 CacheInformation {
831 ast: &new_program.ast,
832 settings: &exec_ctxt.settings,
833 },
834 )
835 .await;
836
837 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
838 panic!("Expected CheckImportsOnly, got {result:?}");
839 };
840
841 assert_eq!(reapply_settings, false);
842 exec_ctxt.close().await;
843 }
844
845 #[tokio::test(flavor = "multi_thread")]
846 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
847 let code = r#"import "toBeImported.kcl" as importedCube
848
849importedCube
850
851sketch001 = startSketchOn(XZ)
852profile001 = startProfile(sketch001, at = [-134.53, -56.17])
853 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
854 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
855 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
856 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
857 |> close()
858extrude001 = extrude(profile001, length = 100)
859sketch003 = startSketchOn(extrude001, face = seg02)
860sketch002 = startSketchOn(extrude001, face = seg01)
861"#;
862
863 let other_file = (
864 std::path::PathBuf::from("toBeImported.kcl"),
865 r#"sketch001 = startSketchOn(XZ)
866profile001 = startProfile(sketch001, at = [281.54, 305.81])
867 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
868 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
869 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
870 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
871 |> close()
872extrude(profile001, length = 100)"#
873 .to_string(),
874 );
875
876 let other_file2 = (
877 std::path::PathBuf::from("toBeImported.kcl"),
878 r#"sketch001 = startSketchOn(XZ)
879profile001 = startProfile(sketch001, at = [281.54, 305.81])
880 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
881 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
882 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
883 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
884 |> close()
885extrude(profile001, length = 100)
886|> translate(z=100)
887"#
888 .to_string(),
889 );
890
891 let tmp_dir = std::env::temp_dir();
892 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
893
894 let tmp_file = tmp_dir.join(other_file.0);
896 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
897 std::fs::write(&tmp_file, other_file.1).unwrap();
898
899 let ExecTestResults { program, exec_ctxt, .. } =
900 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
901 .await
902 .unwrap();
903
904 std::fs::write(tmp_file, other_file2.1).unwrap();
906
907 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
908 new_program.compute_digest();
909
910 let result = get_changed_program(
911 CacheInformation {
912 ast: &program.ast,
913 settings: &exec_ctxt.settings,
914 },
915 CacheInformation {
916 ast: &new_program.ast,
917 settings: &exec_ctxt.settings,
918 },
919 )
920 .await;
921
922 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
923 panic!("Expected CheckImportsOnly, got {result:?}");
924 };
925
926 assert_eq!(reapply_settings, false);
927 exec_ctxt.close().await;
928 }
929
930 #[tokio::test(flavor = "multi_thread")]
931 async fn test_get_changed_program_added_outer_attribute() {
932 let old_code = r#"import "tests/inputs/cube.step"
933"#;
934 let new_code = r#"@(coords = opengl)
935import "tests/inputs/cube.step"
936"#;
937
938 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
939
940 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
941 new_program.compute_digest();
942
943 let result = get_changed_program(
944 CacheInformation {
945 ast: &program.ast,
946 settings: &exec_ctxt.settings,
947 },
948 CacheInformation {
949 ast: &new_program.ast,
950 settings: &exec_ctxt.settings,
951 },
952 )
953 .await;
954
955 assert_eq!(
956 result,
957 CacheResult::ReExecute {
958 clear_scene: true,
959 reapply_settings: false,
960 program: new_program.ast,
961 }
962 );
963 exec_ctxt.close().await;
964 }
965
966 #[tokio::test(flavor = "multi_thread")]
967 async fn test_get_changed_program_different_outer_attribute() {
968 let old_code = r#"@(coords = vulkan)
969import "tests/inputs/cube.step"
970"#;
971 let new_code = r#"@(coords = opengl)
972import "tests/inputs/cube.step"
973"#;
974
975 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
976
977 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
978 new_program.compute_digest();
979
980 let result = get_changed_program(
981 CacheInformation {
982 ast: &program.ast,
983 settings: &exec_ctxt.settings,
984 },
985 CacheInformation {
986 ast: &new_program.ast,
987 settings: &exec_ctxt.settings,
988 },
989 )
990 .await;
991
992 assert_eq!(
993 result,
994 CacheResult::ReExecute {
995 clear_scene: true,
996 reapply_settings: false,
997 program: new_program.ast,
998 }
999 );
1000 exec_ctxt.close().await;
1001 }
1002}