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