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