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