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;
22#[cfg(test)]
23use crate::execution::computed_kcl_version;
24use crate::execution::memory::Stack;
25use crate::execution::state::ModuleInfoMap;
26use crate::execution::state::NotYetAdded;
27use crate::execution::state::{self as exec_state};
28use crate::front::Object;
29use crate::front::ObjectId;
30use crate::modules::ModuleId;
31use crate::modules::ModulePath;
32use crate::modules::ModuleSource;
33use crate::parsing::ast::types::Annotation;
34use crate::parsing::ast::types::Node;
35use crate::parsing::ast::types::Program;
36use crate::walk::Node as WalkNode;
37
38lazy_static::lazy_static! {
39 static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
41 static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
43}
44
45pub(super) async fn read_old_ast() -> Option<GlobalState> {
47 let old_ast = OLD_AST.read().await;
48 old_ast.clone()
49}
50
51pub(super) async fn write_old_ast(old_state: GlobalState) {
52 let mut old_ast = OLD_AST.write().await;
53 *old_ast = Some(old_state);
54}
55
56pub(crate) async fn read_old_memory() -> Option<SketchModeState> {
57 let old_mem = PREV_MEMORY.read().await;
58 old_mem.clone()
59}
60
61pub(crate) async fn write_old_memory(mem: SketchModeState) {
62 let mut old_mem = PREV_MEMORY.write().await;
63 *old_mem = Some(mem);
64}
65
66pub async fn bust_cache() {
67 let mut old_ast = OLD_AST.write().await;
68 *old_ast = None;
69}
70
71pub async fn clear_mem_cache() {
72 let mut old_mem = PREV_MEMORY.write().await;
73 *old_mem = None;
74}
75
76#[derive(Debug, Clone)]
78pub struct CacheInformation<'a> {
79 pub ast: &'a Node<Program>,
80 pub settings: &'a ExecutorSettings,
81 pub kcl_version: KclVersion,
83}
84
85impl<'a> CacheInformation<'a> {
86 #[cfg(test)]
87 fn new(ast: &'a Node<Program>, settings: &'a ExecutorSettings) -> Self {
88 let kcl_version = computed_kcl_version(ast);
90 Self {
91 ast,
92 settings,
93 kcl_version,
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub(super) struct GlobalState {
101 pub(super) main: ModuleState,
102 pub(super) exec_state: exec_state::GlobalState,
104 pub(super) settings: ExecutorSettings,
106 pub kcl_version: KclVersion,
108}
109
110impl GlobalState {
111 pub fn new(
112 state: exec_state::ExecState,
113 settings: ExecutorSettings,
114 kcl_version: KclVersion,
115 ast: Node<Program>,
116 result_env: EnvironmentRef,
117 ) -> Self {
118 Self {
119 main: ModuleState {
120 ast,
121 exec_state: state.mod_local,
122 result_env,
123 },
124 exec_state: state.global,
125 settings,
126 kcl_version,
127 }
128 }
129
130 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
131 self.settings = settings;
132 self
133 }
134
135 pub fn reconstitute_exec_state(&self, ctx: &ExecutorContext) -> exec_state::ExecState {
136 exec_state::ExecState {
137 execution_callbacks: ctx.execution_callbacks.clone(),
138 global: self.exec_state.clone(),
139 mod_local: self.main.exec_state.clone(),
140 }
141 }
142
143 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> Result<ExecOutcome, KclError> {
144 let variables = self.main.exec_state.variables(self.main.result_env)?;
147 #[cfg(test)]
148 let test_program_memory = variables.clone();
149 let variables = variables
150 .into_iter()
151 .map(|(key, value)| (key, KclValueView::from(value)))
152 .collect();
153 Ok(ExecOutcome {
154 variables,
155 filenames: self.exec_state.filenames(),
156 operations: self.exec_state.operations_by_module(),
157 artifact_graph: self.exec_state.artifacts.graph,
158 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
159 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
160 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
161 refactor_metadata: self.exec_state.root_module_artifacts.refactor_metadata.clone(),
162 issues: self.exec_state.issues,
163 source_files: self.exec_state.id_to_source,
164 default_planes: ctx.engine.get_default_planes().read().await.clone(),
165 #[cfg(test)]
166 test_program_memory,
167 })
168 }
169
170 pub fn mock_memory_state(&self) -> Result<SketchModeState, KclError> {
171 let mut stack = self.main.exec_state.stack.deep_clone()?;
172 stack.restore_env(self.main.result_env)?;
173
174 Ok(SketchModeState {
175 stack,
176 module_infos: self.exec_state.module_infos.clone(),
177 path_to_source_id: self.exec_state.path_to_source_id.clone(),
178 id_to_source: self.exec_state.id_to_source.clone(),
179 never_type_ranges: self.exec_state.never_type_ranges.clone(),
180 constraint_state: self.main.exec_state.constraint_state.clone(),
181 scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
182 std_not_yet_added: self.exec_state.std_not_yet_added.clone(),
183 kcl_version: self.exec_state.entry_point_kcl_version.unwrap_or_default(),
184 })
185 }
186}
187
188#[derive(Debug, Clone)]
190pub(super) struct ModuleState {
191 pub(super) ast: Node<Program>,
193 pub(super) exec_state: exec_state::ModuleState,
195 pub(super) result_env: EnvironmentRef,
197}
198
199#[derive(Debug, Clone)]
201pub(crate) struct SketchModeState {
202 pub stack: Stack,
204 pub module_infos: ModuleInfoMap,
206 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
208 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
210 pub never_type_ranges: IndexMap<ModuleId, Vec<SourceRange>>,
212 pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
214 pub scene_objects: Vec<Object>,
216 pub std_not_yet_added: IndexMap<String, NotYetAdded>,
219 pub kcl_version: KclVersion,
222}
223
224impl SketchModeState {
225 pub(crate) fn reusable_for(&self, kcl_version: KclVersion) -> bool {
232 self.kcl_version == kcl_version
233 }
234}
235
236#[doc(hidden)]
238pub async fn read_old_memory_var(name: &str) -> Option<KclValue> {
239 let memory = read_old_memory().await?;
240 memory.stack.get(name, SourceRange::default()).ok()
241}
242
243#[cfg(test)]
244impl SketchModeState {
245 pub(crate) fn new_for_tests() -> Self {
246 Self {
247 stack: Stack::new_for_tests(),
248 module_infos: ModuleInfoMap::default(),
249 path_to_source_id: Default::default(),
250 id_to_source: Default::default(),
251 never_type_ranges: Default::default(),
252 constraint_state: Default::default(),
253 scene_objects: Vec::new(),
254 std_not_yet_added: Default::default(),
255 kcl_version: KclVersion::default(),
256 }
257 }
258}
259
260#[derive(Debug, Clone, PartialEq)]
262#[allow(clippy::large_enum_variant)]
263pub(super) enum CacheResult {
264 ReExecute {
265 clear_scene: bool,
267 reapply_settings: bool,
269 program: Node<Program>,
271 },
272 CheckImportsOnly {
276 reapply_settings: bool,
278 ast: Node<Program>,
280 },
281 NoAction(bool),
283}
284
285pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
290 let mut reapply_settings = false;
291
292 if old.kcl_version != new.kcl_version {
293 return CacheResult::ReExecute {
296 clear_scene: true,
297 reapply_settings: true,
298 program: new.ast.clone(),
299 };
300 }
301
302 if old.settings != new.settings {
305 reapply_settings = true;
308 }
309
310 if old.ast == new.ast {
313 if !old.ast.has_import_statements() {
317 return CacheResult::NoAction(reapply_settings);
318 }
319
320 return CacheResult::CheckImportsOnly {
322 reapply_settings,
323 ast: old.ast.clone(),
324 };
325 }
326
327 let mut old_ast = old.ast.clone();
329 let mut new_ast = new.ast.clone();
330
331 old_ast.compute_digest();
334 new_ast.compute_digest();
335
336 if old_ast.digest == new_ast.digest {
338 if !old.ast.has_import_statements() {
342 return CacheResult::NoAction(reapply_settings);
343 }
344
345 return CacheResult::CheckImportsOnly {
347 reapply_settings,
348 ast: old.ast.clone(),
349 };
350 }
351
352 if !old_ast
354 .inner_attrs
355 .iter()
356 .filter(annotations::is_significant)
357 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
358 .all(|pair| {
359 match pair {
360 EitherOrBoth::Both(old, new) => {
361 let Annotation { name, properties, .. } = &old.inner;
364 let Annotation {
365 name: new_name,
366 properties: new_properties,
367 ..
368 } = &new.inner;
369
370 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
371 && properties
372 .as_ref()
373 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
374 == new_properties
375 .as_ref()
376 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
377 }
378 _ => false,
379 }
380 })
381 {
382 return CacheResult::ReExecute {
386 clear_scene: true,
387 reapply_settings: true,
388 program: new.ast.clone(),
389 };
390 }
391
392 generate_changed_program(old_ast, new_ast, reapply_settings)
394}
395
396fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
408 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
409 let old_node: WalkNode = old.into();
410 let new_node: WalkNode = new.into();
411 old_node.digest() == new_node.digest()
412 }) {
413 return CacheResult::ReExecute {
419 clear_scene: true,
420 reapply_settings,
421 program: new_ast,
422 };
423 }
424
425 match new_ast.body.len().cmp(&old_ast.body.len()) {
429 std::cmp::Ordering::Less => {
430 CacheResult::ReExecute {
439 clear_scene: true,
440 reapply_settings,
441 program: new_ast,
442 }
443 }
444 std::cmp::Ordering::Greater => {
445 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
453
454 CacheResult::ReExecute {
455 clear_scene: false,
456 reapply_settings,
457 program: new_ast,
458 }
459 }
460 std::cmp::Ordering::Equal => {
461 CacheResult::NoAction(reapply_settings)
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use pretty_assertions::assert_eq;
478
479 use super::*;
480 use crate::execution::ExecTestResults;
481 use crate::execution::parse_execute;
482 use crate::execution::parse_execute_with_project_dir;
483
484 #[tokio::test(flavor = "multi_thread")]
485 async fn test_get_changed_program_same_code() {
486 let new = r#"// Remove the end face for the extrusion.
487firstSketch = startSketchOn(XY)
488 |> startProfile(at = [-12, 12])
489 |> line(end = [24, 0])
490 |> line(end = [0, -24])
491 |> line(end = [-24, 0])
492 |> close()
493 |> extrude(length = 6)
494
495// Remove the end face for the extrusion.
496shell(firstSketch, faces = [END], thickness = 0.25)"#;
497
498 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
499
500 let result = get_changed_program(
501 CacheInformation::new(&program.ast, &exec_ctxt.settings),
502 CacheInformation::new(&program.ast, &exec_ctxt.settings),
503 )
504 .await;
505
506 assert_eq!(result, CacheResult::NoAction(false));
507 exec_ctxt.close().await;
508 }
509
510 #[tokio::test(flavor = "multi_thread")]
511 async fn test_get_changed_program_same_code_changed_whitespace() {
512 let old = r#" // Remove the end face for the extrusion.
513firstSketch = startSketchOn(XY)
514 |> startProfile(at = [-12, 12])
515 |> line(end = [24, 0])
516 |> line(end = [0, -24])
517 |> line(end = [-24, 0])
518 |> close()
519 |> extrude(length = 6)
520
521// Remove the end face for the extrusion.
522shell(firstSketch, faces = [END], thickness = 0.25) "#;
523
524 let new = r#"// Remove the end face for the extrusion.
525firstSketch = startSketchOn(XY)
526 |> startProfile(at = [-12, 12])
527 |> line(end = [24, 0])
528 |> line(end = [0, -24])
529 |> line(end = [-24, 0])
530 |> close()
531 |> extrude(length = 6)
532
533// Remove the end face for the extrusion.
534shell(firstSketch, faces = [END], thickness = 0.25)"#;
535
536 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
537
538 let program_new = crate::Program::parse_no_errs(new).unwrap();
539
540 let result = get_changed_program(
541 CacheInformation::new(&program.ast, &exec_ctxt.settings),
542 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
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_comment_start_of_program() {
552 let old = r#" // Removed the end face for the extrusion.
553firstSketch = startSketchOn(XY)
554 |> startProfile(at = [-12, 12])
555 |> line(end = [24, 0])
556 |> line(end = [0, -24])
557 |> line(end = [-24, 0])
558 |> close()
559 |> extrude(length = 6)
560
561// Remove the end face for the extrusion.
562shell(firstSketch, faces = [END], thickness = 0.25) "#;
563
564 let new = r#"// Remove the end face for the extrusion.
565firstSketch = startSketchOn(XY)
566 |> startProfile(at = [-12, 12])
567 |> line(end = [24, 0])
568 |> line(end = [0, -24])
569 |> line(end = [-24, 0])
570 |> close()
571 |> extrude(length = 6)
572
573// Remove the end face for the extrusion.
574shell(firstSketch, faces = [END], thickness = 0.25)"#;
575
576 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
577
578 let program_new = crate::Program::parse_no_errs(new).unwrap();
579
580 let result = get_changed_program(
581 CacheInformation::new(&program.ast, &exec_ctxt.settings),
582 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
583 )
584 .await;
585
586 assert_eq!(result, CacheResult::NoAction(false));
587 exec_ctxt.close().await;
588 }
589
590 #[tokio::test(flavor = "multi_thread")]
591 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
592 let old = r#"@foo(whatever = whatever)
593@bar
594// Removed 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]) // my thing
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 new = r#"@foo(whatever = 42)
607@baz
608// Remove the end face for the extrusion.
609firstSketch = startSketchOn(XY)
610 |> startProfile(at = [-12, 12])
611 |> line(end = [24, 0])
612 |> line(end = [0, -24])
613 |> line(end = [-24, 0])
614 |> close()
615 |> extrude(length = 6)
616
617// Remove the end face for the extrusion.
618shell(firstSketch, faces = [END], thickness = 0.25)"#;
619
620 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
621
622 let program_new = crate::Program::parse_no_errs(new).unwrap();
623
624 let result = get_changed_program(
625 CacheInformation::new(&program.ast, &exec_ctxt.settings),
626 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
627 )
628 .await;
629
630 assert_eq!(result, CacheResult::NoAction(false));
631 exec_ctxt.close().await;
632 }
633
634 #[tokio::test(flavor = "multi_thread")]
636 async fn test_get_changed_program_same_code_but_different_grid_setting() {
637 let new = r#"// Remove the end face for the extrusion.
638firstSketch = startSketchOn(XY)
639 |> startProfile(at = [-12, 12])
640 |> line(end = [24, 0])
641 |> line(end = [0, -24])
642 |> line(end = [-24, 0])
643 |> close()
644 |> extrude(length = 6)
645
646// Remove the end face for the extrusion.
647shell(firstSketch, faces = [END], thickness = 0.25)"#;
648
649 let ExecTestResults {
650 program, mut exec_ctxt, ..
651 } = parse_execute(new).await.unwrap();
652
653 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
655
656 let result = get_changed_program(
657 CacheInformation::new(&program.ast, &Default::default()),
658 CacheInformation::new(&program.ast, &exec_ctxt.settings),
659 )
660 .await;
661
662 assert_eq!(result, CacheResult::NoAction(true));
663 exec_ctxt.close().await;
664 }
665
666 #[tokio::test(flavor = "multi_thread")]
668 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
669 let new = r#"// Remove the end face for the extrusion.
670firstSketch = startSketchOn(XY)
671 |> startProfile(at = [-12, 12])
672 |> line(end = [24, 0])
673 |> line(end = [0, -24])
674 |> line(end = [-24, 0])
675 |> close()
676 |> extrude(length = 6)
677
678// Remove the end face for the extrusion.
679shell(firstSketch, faces = [END], thickness = 0.25)"#;
680
681 let ExecTestResults {
682 program, mut exec_ctxt, ..
683 } = parse_execute(new).await.unwrap();
684
685 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
687
688 let result = get_changed_program(
689 CacheInformation::new(&program.ast, &Default::default()),
690 CacheInformation::new(&program.ast, &exec_ctxt.settings),
691 )
692 .await;
693
694 assert_eq!(result, CacheResult::NoAction(true));
695
696 let old_settings = exec_ctxt.settings.clone();
698 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
699
700 let result = get_changed_program(
701 CacheInformation::new(&program.ast, &old_settings),
702 CacheInformation::new(&program.ast, &exec_ctxt.settings),
703 )
704 .await;
705
706 assert_eq!(result, CacheResult::NoAction(true));
707
708 let old_settings = exec_ctxt.settings.clone();
710 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
711
712 let result = get_changed_program(
713 CacheInformation::new(&program.ast, &old_settings),
714 CacheInformation::new(&program.ast, &exec_ctxt.settings),
715 )
716 .await;
717
718 assert_eq!(result, CacheResult::NoAction(true));
719 exec_ctxt.close().await;
720 }
721
722 #[tokio::test(flavor = "multi_thread")]
724 async fn test_get_changed_program_same_code_but_different_kcl_version_using_annotation() {
725 for (old_version, new_version) in [
726 ("2.0", "\"3.0-preview\""),
727 ("2.0", "3.0"),
728 ("3.0", "2.0"),
729 ("\"3.0-preview\"", "3.0"),
730 ("3.0", "\"3.0-preview\""),
731 ] {
732 let old_code = format!("@settings(kclVersion = {old_version})\nx = 1\ny = x + 1\n");
733 let new_code = format!("@settings(kclVersion = {new_version})\nx = 1\ny = x + 1\n");
734 let mut program = crate::Program::parse_no_errs(&old_code).unwrap();
735 program.compute_digest();
736 let mut new_program = crate::Program::parse_no_errs(&new_code).unwrap();
737 new_program.compute_digest();
738 let settings = ExecutorSettings::default();
739
740 let result = get_changed_program(
741 CacheInformation::new(&program.ast, &settings),
742 CacheInformation::new(&new_program.ast, &settings),
743 )
744 .await;
745
746 assert_eq!(
747 result,
748 CacheResult::ReExecute {
749 clear_scene: true,
750 reapply_settings: true,
751 program: new_program.ast,
752 },
753 "{old_version} -> {new_version}"
754 );
755 }
756 }
757
758 #[tokio::test(flavor = "multi_thread")]
761 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
762 let old_code = r#"@settings(defaultLengthUnit = in)
763startSketchOn(XY)
764"#;
765 let new_code = r#"@settings(defaultLengthUnit = mm)
766startSketchOn(XY)
767"#;
768
769 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
770
771 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
772 new_program.compute_digest();
773
774 let result = get_changed_program(
775 CacheInformation::new(&program.ast, &exec_ctxt.settings),
776 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
777 )
778 .await;
779
780 assert_eq!(
781 result,
782 CacheResult::ReExecute {
783 clear_scene: true,
784 reapply_settings: true,
785 program: new_program.ast,
786 }
787 );
788 exec_ctxt.close().await;
789 }
790
791 #[tokio::test(flavor = "multi_thread")]
794 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
795 let old_code = r#"@settings(defaultLengthUnit = in)
796startSketchOn(XY)
797"#;
798 let new_code = r#"
799startSketchOn(XY)
800"#;
801
802 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
803
804 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
805 new_program.compute_digest();
806
807 let result = get_changed_program(
808 CacheInformation::new(&program.ast, &exec_ctxt.settings),
809 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
810 )
811 .await;
812
813 assert_eq!(
814 result,
815 CacheResult::ReExecute {
816 clear_scene: true,
817 reapply_settings: true,
818 program: new_program.ast,
819 }
820 );
821 exec_ctxt.close().await;
822 }
823
824 #[tokio::test(flavor = "multi_thread")]
825 async fn test_multi_file_no_changes_does_not_reexecute() {
826 let code = r#"import "toBeImported.kcl" as importedCube
827
828importedCube
829
830sketch001 = startSketchOn(XZ)
831profile001 = startProfile(sketch001, at = [-134.53, -56.17])
832 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
833 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
834 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
835 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
836 |> close()
837extrude001 = extrude(profile001, length = 100)
838sketch003 = startSketchOn(extrude001, face = seg02)
839sketch002 = startSketchOn(extrude001, face = seg01)
840"#;
841
842 let other_file = (
843 std::path::PathBuf::from("toBeImported.kcl"),
844 r#"sketch001 = startSketchOn(XZ)
845profile001 = startProfile(sketch001, at = [281.54, 305.81])
846 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
847 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
848 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
849 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
850 |> close()
851extrude(profile001, length = 100)"#
852 .to_string(),
853 );
854
855 let tmp_dir = std::env::temp_dir();
856 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
857
858 let tmp_file = tmp_dir.join(other_file.0);
860 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
861 std::fs::write(tmp_file, other_file.1).unwrap();
862
863 let ExecTestResults { program, exec_ctxt, .. } =
864 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
865 .await
866 .unwrap();
867
868 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
869 new_program.compute_digest();
870
871 let result = get_changed_program(
872 CacheInformation::new(&program.ast, &exec_ctxt.settings),
873 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
874 )
875 .await;
876
877 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
878 panic!("Expected CheckImportsOnly, got {result:?}");
879 };
880
881 assert_eq!(reapply_settings, false);
882 exec_ctxt.close().await;
883 }
884
885 #[tokio::test(flavor = "multi_thread")]
886 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
887 let code = r#"import "toBeImported.kcl" as importedCube
888
889importedCube
890
891sketch001 = startSketchOn(XZ)
892profile001 = startProfile(sketch001, at = [-134.53, -56.17])
893 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
894 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
895 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
896 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
897 |> close()
898extrude001 = extrude(profile001, length = 100)
899sketch003 = startSketchOn(extrude001, face = seg02)
900sketch002 = startSketchOn(extrude001, face = seg01)
901"#;
902
903 let other_file = (
904 std::path::PathBuf::from("toBeImported.kcl"),
905 r#"sketch001 = startSketchOn(XZ)
906profile001 = startProfile(sketch001, at = [281.54, 305.81])
907 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
908 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
909 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
910 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
911 |> close()
912extrude(profile001, length = 100)"#
913 .to_string(),
914 );
915
916 let other_file2 = (
917 std::path::PathBuf::from("toBeImported.kcl"),
918 r#"sketch001 = startSketchOn(XZ)
919profile001 = startProfile(sketch001, at = [281.54, 305.81])
920 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
921 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
922 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
923 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
924 |> close()
925extrude(profile001, length = 100)
926|> translate(z=100)
927"#
928 .to_string(),
929 );
930
931 let tmp_dir = std::env::temp_dir();
932 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
933
934 let tmp_file = tmp_dir.join(other_file.0);
936 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
937 std::fs::write(&tmp_file, other_file.1).unwrap();
938
939 let ExecTestResults { program, exec_ctxt, .. } =
940 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
941 .await
942 .unwrap();
943
944 std::fs::write(tmp_file, other_file2.1).unwrap();
946
947 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
948 new_program.compute_digest();
949
950 let result = get_changed_program(
951 CacheInformation::new(&program.ast, &exec_ctxt.settings),
952 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
953 )
954 .await;
955
956 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
957 panic!("Expected CheckImportsOnly, got {result:?}");
958 };
959
960 assert_eq!(reapply_settings, false);
961 exec_ctxt.close().await;
962 }
963
964 #[tokio::test(flavor = "multi_thread")]
965 async fn test_get_changed_program_added_outer_attribute() {
966 let old_code = r#"import "tests/inputs/cube.step"
967"#;
968 let new_code = r#"@(coords = opengl)
969import "tests/inputs/cube.step"
970"#;
971
972 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
973
974 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
975 new_program.compute_digest();
976
977 let result = get_changed_program(
978 CacheInformation::new(&program.ast, &exec_ctxt.settings),
979 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
980 )
981 .await;
982
983 assert_eq!(
984 result,
985 CacheResult::ReExecute {
986 clear_scene: true,
987 reapply_settings: false,
988 program: new_program.ast,
989 }
990 );
991 exec_ctxt.close().await;
992 }
993
994 #[tokio::test(flavor = "multi_thread")]
995 async fn test_get_changed_program_different_outer_attribute() {
996 let old_code = r#"@(coords = vulkan)
997import "tests/inputs/cube.step"
998"#;
999 let new_code = r#"@(coords = opengl)
1000import "tests/inputs/cube.step"
1001"#;
1002
1003 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1004
1005 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1006 new_program.compute_digest();
1007
1008 let result = get_changed_program(
1009 CacheInformation::new(&program.ast, &exec_ctxt.settings),
1010 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
1011 )
1012 .await;
1013
1014 assert_eq!(
1015 result,
1016 CacheResult::ReExecute {
1017 clear_scene: true,
1018 reapply_settings: false,
1019 program: new_program.ast,
1020 }
1021 );
1022 exec_ctxt.close().await;
1023 }
1024}