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