1use std::sync::Arc;
4
5use indexmap::IndexMap;
6use itertools::EitherOrBoth;
7use itertools::Itertools;
8use kcl_api::Object;
9use kcl_api::ObjectId;
10use kittycad_modeling_cmds::KclVersion;
11use tokio::sync::RwLock;
12
13use crate::ExecOutcome;
14use crate::ExecutorContext;
15use crate::SourceRange;
16use crate::errors::KclError;
17use crate::execution::ConstraintKey;
18use crate::execution::ConstraintState;
19use crate::execution::EnvironmentRef;
20use crate::execution::ExecutorSettings;
21use crate::execution::KclValue;
22use crate::execution::KclValueView;
23use crate::execution::annotations;
24#[cfg(test)]
25use crate::execution::computed_kcl_version;
26use crate::execution::memory::Stack;
27use crate::execution::state::ModuleInfoMap;
28use crate::execution::state::NotYetAdded;
29use crate::execution::state::{self as exec_state};
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
38#[derive(Debug, Default)]
39struct CacheInner {
40 pub old_ast: Option<GlobalState>,
42 pub prev_memory: Option<SketchModeState>,
44 pub engine_scene_generation: usize,
49}
50
51#[derive(Debug, Clone)]
57pub struct Cache(Arc<dyn CacheStorage>);
58
59trait CacheStorage: std::fmt::Debug + Send + Sync {
61 fn inner(&self) -> &RwLock<CacheInner>;
62}
63
64impl CacheStorage for RwLock<CacheInner> {
65 fn inner(&self) -> &RwLock<CacheInner> {
66 self
67 }
68}
69
70impl Default for Cache {
71 fn default() -> Self {
72 Self(Arc::new(RwLock::new(CacheInner::default())))
73 }
74}
75
76impl Cache {
77 pub(super) async fn read_old_ast(&self) -> Option<GlobalState> {
79 self.0.inner().read().await.old_ast.clone()
80 }
81
82 pub(super) async fn write_old_ast(&self, old_state: GlobalState) {
83 let mut inner = self.0.inner().write().await;
84 inner.old_ast = Some(old_state);
85 }
86
87 pub(crate) async fn read_old_memory(&self) -> Option<SketchModeState> {
88 self.0.inner().read().await.prev_memory.clone()
89 }
90
91 pub(crate) async fn write_old_memory(&self, mem: SketchModeState) {
92 let mut inner = self.0.inner().write().await;
93 inner.prev_memory = Some(mem);
94 }
95
96 pub(crate) async fn engine_scene_generation(&self) -> usize {
98 self.0.inner().read().await.engine_scene_generation
99 }
100
101 pub(super) async fn bump_engine_scene_generation(&self) {
102 let mut inner = self.0.inner().write().await;
103 inner.engine_scene_generation = inner.engine_scene_generation.wrapping_add(1);
104 }
105
106 pub async fn bust_cache(&self) {
108 let mut inner = self.0.inner().write().await;
109 inner.old_ast = None;
110 }
111
112 pub async fn clear_mem_cache(&self) {
114 let mut inner = self.0.inner().write().await;
115 inner.prev_memory = None;
116 }
117
118 pub async fn read_old_memory_var(&self, name: &str) -> Option<KclValue> {
120 self.read_old_memory()
121 .await
122 .and_then(|memory| memory.stack.get(name, SourceRange::default()).ok())
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct CacheInformation<'a> {
129 pub ast: &'a Node<Program>,
130 pub settings: &'a ExecutorSettings,
131 pub kcl_version: KclVersion,
133}
134
135impl<'a> CacheInformation<'a> {
136 #[cfg(test)]
137 fn new(ast: &'a Node<Program>, settings: &'a ExecutorSettings) -> Self {
138 let kcl_version = computed_kcl_version(ast);
140 Self {
141 ast,
142 settings,
143 kcl_version,
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
150pub(super) struct GlobalState {
151 pub(super) main: ModuleState,
152 pub(super) exec_state: exec_state::GlobalState,
154 pub(super) settings: ExecutorSettings,
156 pub kcl_version: KclVersion,
158}
159
160impl GlobalState {
161 pub fn new(
162 state: exec_state::ExecState,
163 settings: ExecutorSettings,
164 kcl_version: KclVersion,
165 ast: Node<Program>,
166 result_env: EnvironmentRef,
167 ) -> Self {
168 Self {
169 main: ModuleState {
170 ast,
171 exec_state: state.mod_local,
172 result_env,
173 },
174 exec_state: state.global,
175 settings,
176 kcl_version,
177 }
178 }
179
180 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
181 self.settings = settings;
182 self
183 }
184
185 pub fn reconstitute_exec_state(&self, ctx: &ExecutorContext) -> exec_state::ExecState {
186 exec_state::ExecState {
187 execution_callbacks: ctx.execution_callbacks.clone(),
188 global: self.exec_state.clone(),
189 mod_local: self.main.exec_state.clone(),
190 }
191 }
192
193 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> Result<ExecOutcome, KclError> {
194 let variables = self.main.exec_state.variables(self.main.result_env)?;
197 #[cfg(test)]
198 let test_program_memory = variables.clone();
199 let variables = variables
200 .into_iter()
201 .map(|(key, value)| (key, KclValueView::from(value)))
202 .collect();
203 Ok(ExecOutcome {
204 variables,
205 filenames: self.exec_state.filenames(),
206 engine_scene_generation: ctx.cache.engine_scene_generation().await,
207 operations: self.exec_state.operations_by_module(),
208 artifact_graph: self.exec_state.artifacts.graph,
209 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
210 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
211 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
212 refactor_metadata: self.exec_state.root_module_artifacts.refactor_metadata.clone(),
213 issues: self.exec_state.issues,
214 source_files: self.exec_state.id_to_source,
215 default_planes: ctx.engine.get_default_planes().read().await.clone(),
216 #[cfg(test)]
217 test_program_memory,
218 })
219 }
220
221 pub fn mock_memory_state(&self) -> Result<SketchModeState, KclError> {
222 let mut stack = self.main.exec_state.stack.deep_clone()?;
223 stack.restore_env(self.main.result_env)?;
224
225 Ok(SketchModeState {
226 stack,
227 module_infos: self.exec_state.module_infos.clone(),
228 path_to_source_id: self.exec_state.path_to_source_id.clone(),
229 id_to_source: self.exec_state.id_to_source.clone(),
230 never_type_ranges: self.exec_state.never_type_ranges.clone(),
231 constraint_state: self.main.exec_state.constraint_state.clone(),
232 scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
233 std_not_yet_added: self.exec_state.std_not_yet_added.clone(),
234 kcl_version: self.exec_state.entry_point_kcl_version.unwrap_or_default(),
235 })
236 }
237}
238
239#[derive(Debug, Clone)]
241pub(super) struct ModuleState {
242 pub(super) ast: Node<Program>,
244 pub(super) exec_state: exec_state::ModuleState,
246 pub(super) result_env: EnvironmentRef,
248}
249
250#[derive(Debug, Clone)]
252pub(crate) struct SketchModeState {
253 pub stack: Stack,
255 pub module_infos: ModuleInfoMap,
257 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
259 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
261 pub never_type_ranges: IndexMap<ModuleId, Vec<SourceRange>>,
263 pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
265 pub scene_objects: Vec<Object>,
267 pub std_not_yet_added: IndexMap<String, NotYetAdded>,
270 pub kcl_version: KclVersion,
273}
274
275impl SketchModeState {
276 pub(crate) fn reusable_for(&self, kcl_version: KclVersion) -> bool {
283 self.kcl_version == kcl_version
284 }
285}
286
287#[cfg(test)]
288impl SketchModeState {
289 pub(crate) fn new_for_tests() -> Self {
290 Self {
291 stack: Stack::new_for_tests(),
292 module_infos: ModuleInfoMap::default(),
293 path_to_source_id: Default::default(),
294 id_to_source: Default::default(),
295 never_type_ranges: Default::default(),
296 constraint_state: Default::default(),
297 scene_objects: Vec::new(),
298 std_not_yet_added: Default::default(),
299 kcl_version: KclVersion::default(),
300 }
301 }
302}
303
304#[derive(Debug, Clone, PartialEq)]
306#[allow(clippy::large_enum_variant)]
307pub(super) enum CacheResult {
308 ReExecute {
309 clear_scene: bool,
311 reapply_settings: bool,
313 program: Node<Program>,
315 },
316 CheckImportsOnly {
320 reapply_settings: bool,
322 ast: Node<Program>,
324 },
325 NoAction(bool),
327}
328
329pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
334 let mut reapply_settings = false;
335
336 if old.kcl_version != new.kcl_version {
337 return CacheResult::ReExecute {
340 clear_scene: true,
341 reapply_settings: true,
342 program: new.ast.clone(),
343 };
344 }
345
346 if old.settings != new.settings {
349 reapply_settings = true;
352 }
353
354 if old.ast == new.ast {
357 if !old.ast.has_import_statements() {
361 return CacheResult::NoAction(reapply_settings);
362 }
363
364 return CacheResult::CheckImportsOnly {
366 reapply_settings,
367 ast: old.ast.clone(),
368 };
369 }
370
371 let mut old_ast = old.ast.clone();
373 let mut new_ast = new.ast.clone();
374
375 old_ast.compute_digest();
378 new_ast.compute_digest();
379
380 if old_ast.digest == new_ast.digest {
382 if !old.ast.has_import_statements() {
386 return CacheResult::NoAction(reapply_settings);
387 }
388
389 return CacheResult::CheckImportsOnly {
391 reapply_settings,
392 ast: old.ast.clone(),
393 };
394 }
395
396 if !old_ast
398 .inner_attrs
399 .iter()
400 .filter(annotations::is_significant)
401 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
402 .all(|pair| {
403 match pair {
404 EitherOrBoth::Both(old, new) => {
405 let Annotation { name, properties, .. } = &old.inner;
408 let Annotation {
409 name: new_name,
410 properties: new_properties,
411 ..
412 } = &new.inner;
413
414 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
415 && properties
416 .as_ref()
417 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
418 == new_properties
419 .as_ref()
420 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
421 }
422 _ => false,
423 }
424 })
425 {
426 return CacheResult::ReExecute {
430 clear_scene: true,
431 reapply_settings: true,
432 program: new.ast.clone(),
433 };
434 }
435
436 generate_changed_program(old_ast, new_ast, reapply_settings)
438}
439
440fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
452 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
453 let old_node: WalkNode = old.into();
454 let new_node: WalkNode = new.into();
455 old_node.digest() == new_node.digest()
456 }) {
457 return CacheResult::ReExecute {
463 clear_scene: true,
464 reapply_settings,
465 program: new_ast,
466 };
467 }
468
469 match new_ast.body.len().cmp(&old_ast.body.len()) {
473 std::cmp::Ordering::Less => {
474 CacheResult::ReExecute {
483 clear_scene: true,
484 reapply_settings,
485 program: new_ast,
486 }
487 }
488 std::cmp::Ordering::Greater => {
489 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
497
498 CacheResult::ReExecute {
499 clear_scene: false,
500 reapply_settings,
501 program: new_ast,
502 }
503 }
504 std::cmp::Ordering::Equal => {
505 CacheResult::NoAction(reapply_settings)
515 }
516 }
517}
518
519#[cfg(test)]
520mod tests {
521 use pretty_assertions::assert_eq;
522
523 use super::*;
524 use crate::execution::ExecTestResults;
525 use crate::execution::parse_execute;
526 use crate::execution::parse_execute_with_project_dir;
527
528 #[tokio::test(flavor = "multi_thread")]
529 async fn test_get_changed_program_same_code() {
530 let new = r#"// Remove the end face for the extrusion.
531firstSketch = startSketchOn(XY)
532 |> startProfile(at = [-12, 12])
533 |> line(end = [24, 0])
534 |> line(end = [0, -24])
535 |> line(end = [-24, 0])
536 |> close()
537 |> extrude(length = 6)
538
539// Remove the end face for the extrusion.
540shell(firstSketch, faces = [END], thickness = 0.25)"#;
541
542 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
543
544 let result = get_changed_program(
545 CacheInformation::new(&program.ast, &exec_ctxt.settings),
546 CacheInformation::new(&program.ast, &exec_ctxt.settings),
547 )
548 .await;
549
550 assert_eq!(result, CacheResult::NoAction(false));
551 exec_ctxt.close().await;
552 }
553
554 #[tokio::test(flavor = "multi_thread")]
555 async fn test_get_changed_program_same_code_changed_whitespace() {
556 let old = r#" // Remove the end face for the extrusion.
557firstSketch = startSketchOn(XY)
558 |> startProfile(at = [-12, 12])
559 |> line(end = [24, 0])
560 |> line(end = [0, -24])
561 |> line(end = [-24, 0])
562 |> close()
563 |> extrude(length = 6)
564
565// Remove the end face for the extrusion.
566shell(firstSketch, faces = [END], thickness = 0.25) "#;
567
568 let new = r#"// 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::new(&program.ast, &exec_ctxt.settings),
586 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
587 )
588 .await;
589
590 assert_eq!(result, CacheResult::NoAction(false));
591 exec_ctxt.close().await;
592 }
593
594 #[tokio::test(flavor = "multi_thread")]
595 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
596 let old = r#" // Removed the end face for the extrusion.
597firstSketch = startSketchOn(XY)
598 |> startProfile(at = [-12, 12])
599 |> line(end = [24, 0])
600 |> line(end = [0, -24])
601 |> line(end = [-24, 0])
602 |> close()
603 |> extrude(length = 6)
604
605// Remove the end face for the extrusion.
606shell(firstSketch, faces = [END], thickness = 0.25) "#;
607
608 let new = r#"// 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")]
635 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
636 let old = r#"@foo(whatever = whatever)
637@bar
638// Removed the end face for the extrusion.
639firstSketch = startSketchOn(XY)
640 |> startProfile(at = [-12, 12])
641 |> line(end = [24, 0])
642 |> line(end = [0, -24])
643 |> line(end = [-24, 0]) // my thing
644 |> close()
645 |> extrude(length = 6)
646
647// Remove the end face for the extrusion.
648shell(firstSketch, faces = [END], thickness = 0.25) "#;
649
650 let new = r#"@foo(whatever = 42)
651@baz
652// Remove the end face for the extrusion.
653firstSketch = startSketchOn(XY)
654 |> startProfile(at = [-12, 12])
655 |> line(end = [24, 0])
656 |> line(end = [0, -24])
657 |> line(end = [-24, 0])
658 |> close()
659 |> extrude(length = 6)
660
661// Remove the end face for the extrusion.
662shell(firstSketch, faces = [END], thickness = 0.25)"#;
663
664 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
665
666 let program_new = crate::Program::parse_no_errs(new).unwrap();
667
668 let result = get_changed_program(
669 CacheInformation::new(&program.ast, &exec_ctxt.settings),
670 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
671 )
672 .await;
673
674 assert_eq!(result, CacheResult::NoAction(false));
675 exec_ctxt.close().await;
676 }
677
678 #[tokio::test(flavor = "multi_thread")]
680 async fn test_get_changed_program_same_code_but_different_grid_setting() {
681 let new = r#"// Remove the end face for the extrusion.
682firstSketch = startSketchOn(XY)
683 |> startProfile(at = [-12, 12])
684 |> line(end = [24, 0])
685 |> line(end = [0, -24])
686 |> line(end = [-24, 0])
687 |> close()
688 |> extrude(length = 6)
689
690// Remove the end face for the extrusion.
691shell(firstSketch, faces = [END], thickness = 0.25)"#;
692
693 let ExecTestResults {
694 program, mut exec_ctxt, ..
695 } = parse_execute(new).await.unwrap();
696
697 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
699
700 let result = get_changed_program(
701 CacheInformation::new(&program.ast, &Default::default()),
702 CacheInformation::new(&program.ast, &exec_ctxt.settings),
703 )
704 .await;
705
706 assert_eq!(result, CacheResult::NoAction(true));
707 exec_ctxt.close().await;
708 }
709
710 #[tokio::test(flavor = "multi_thread")]
712 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
713 let new = r#"// Remove the end face for the extrusion.
714firstSketch = startSketchOn(XY)
715 |> startProfile(at = [-12, 12])
716 |> line(end = [24, 0])
717 |> line(end = [0, -24])
718 |> line(end = [-24, 0])
719 |> close()
720 |> extrude(length = 6)
721
722// Remove the end face for the extrusion.
723shell(firstSketch, faces = [END], thickness = 0.25)"#;
724
725 let ExecTestResults {
726 program, mut exec_ctxt, ..
727 } = parse_execute(new).await.unwrap();
728
729 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
731
732 let result = get_changed_program(
733 CacheInformation::new(&program.ast, &Default::default()),
734 CacheInformation::new(&program.ast, &exec_ctxt.settings),
735 )
736 .await;
737
738 assert_eq!(result, CacheResult::NoAction(true));
739
740 let old_settings = exec_ctxt.settings.clone();
742 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
743
744 let result = get_changed_program(
745 CacheInformation::new(&program.ast, &old_settings),
746 CacheInformation::new(&program.ast, &exec_ctxt.settings),
747 )
748 .await;
749
750 assert_eq!(result, CacheResult::NoAction(true));
751
752 let old_settings = exec_ctxt.settings.clone();
754 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
755
756 let result = get_changed_program(
757 CacheInformation::new(&program.ast, &old_settings),
758 CacheInformation::new(&program.ast, &exec_ctxt.settings),
759 )
760 .await;
761
762 assert_eq!(result, CacheResult::NoAction(true));
763 exec_ctxt.close().await;
764 }
765
766 #[tokio::test(flavor = "multi_thread")]
768 async fn test_get_changed_program_same_code_but_different_kcl_version_using_annotation() {
769 for (old_version, new_version) in [
770 ("2.0", "\"3.0-preview\""),
771 ("2.0", "3.0"),
772 ("3.0", "2.0"),
773 ("\"3.0-preview\"", "3.0"),
774 ("3.0", "\"3.0-preview\""),
775 ] {
776 let old_code = format!("@settings(kclVersion = {old_version})\nx = 1\ny = x + 1\n");
777 let new_code = format!("@settings(kclVersion = {new_version})\nx = 1\ny = x + 1\n");
778 let mut program = crate::Program::parse_no_errs(&old_code).unwrap();
779 program.compute_digest();
780 let mut new_program = crate::Program::parse_no_errs(&new_code).unwrap();
781 new_program.compute_digest();
782 let settings = ExecutorSettings::default();
783
784 let result = get_changed_program(
785 CacheInformation::new(&program.ast, &settings),
786 CacheInformation::new(&new_program.ast, &settings),
787 )
788 .await;
789
790 assert_eq!(
791 result,
792 CacheResult::ReExecute {
793 clear_scene: true,
794 reapply_settings: true,
795 program: new_program.ast,
796 },
797 "{old_version} -> {new_version}"
798 );
799 }
800 }
801
802 #[tokio::test(flavor = "multi_thread")]
805 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
806 let old_code = r#"@settings(defaultLengthUnit = in)
807startSketchOn(XY)
808"#;
809 let new_code = r#"@settings(defaultLengthUnit = mm)
810startSketchOn(XY)
811"#;
812
813 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
814
815 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
816 new_program.compute_digest();
817
818 let result = get_changed_program(
819 CacheInformation::new(&program.ast, &exec_ctxt.settings),
820 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
821 )
822 .await;
823
824 assert_eq!(
825 result,
826 CacheResult::ReExecute {
827 clear_scene: true,
828 reapply_settings: true,
829 program: new_program.ast,
830 }
831 );
832 exec_ctxt.close().await;
833 }
834
835 #[tokio::test(flavor = "multi_thread")]
838 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
839 let old_code = r#"@settings(defaultLengthUnit = in)
840startSketchOn(XY)
841"#;
842 let new_code = r#"
843startSketchOn(XY)
844"#;
845
846 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
847
848 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
849 new_program.compute_digest();
850
851 let result = get_changed_program(
852 CacheInformation::new(&program.ast, &exec_ctxt.settings),
853 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
854 )
855 .await;
856
857 assert_eq!(
858 result,
859 CacheResult::ReExecute {
860 clear_scene: true,
861 reapply_settings: true,
862 program: new_program.ast,
863 }
864 );
865 exec_ctxt.close().await;
866 }
867
868 #[tokio::test(flavor = "multi_thread")]
869 async fn test_multi_file_no_changes_does_not_reexecute() {
870 let code = r#"import "toBeImported.kcl" as importedCube
871
872importedCube
873
874sketch001 = startSketchOn(XZ)
875profile001 = startProfile(sketch001, at = [-134.53, -56.17])
876 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
877 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
878 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
879 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
880 |> close()
881extrude001 = extrude(profile001, length = 100)
882sketch003 = startSketchOn(extrude001, face = seg02)
883sketch002 = startSketchOn(extrude001, face = seg01)
884"#;
885
886 let other_file = (
887 std::path::PathBuf::from("toBeImported.kcl"),
888 r#"sketch001 = startSketchOn(XZ)
889profile001 = startProfile(sketch001, at = [281.54, 305.81])
890 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
891 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
892 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
893 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
894 |> close()
895extrude(profile001, length = 100)"#
896 .to_string(),
897 );
898
899 let tmp_dir = std::env::temp_dir();
900 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
901
902 let tmp_file = tmp_dir.join(other_file.0);
904 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
905 std::fs::write(tmp_file, other_file.1).unwrap();
906
907 let ExecTestResults { program, exec_ctxt, .. } =
908 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
909 .await
910 .unwrap();
911
912 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
913 new_program.compute_digest();
914
915 let result = get_changed_program(
916 CacheInformation::new(&program.ast, &exec_ctxt.settings),
917 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
918 )
919 .await;
920
921 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
922 panic!("Expected CheckImportsOnly, got {result:?}");
923 };
924
925 assert_eq!(reapply_settings, false);
926 exec_ctxt.close().await;
927 }
928
929 #[tokio::test(flavor = "multi_thread")]
930 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
931 let code = r#"import "toBeImported.kcl" as importedCube
932
933importedCube
934
935sketch001 = startSketchOn(XZ)
936profile001 = startProfile(sketch001, at = [-134.53, -56.17])
937 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
938 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
939 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
940 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
941 |> close()
942extrude001 = extrude(profile001, length = 100)
943sketch003 = startSketchOn(extrude001, face = seg02)
944sketch002 = startSketchOn(extrude001, face = seg01)
945"#;
946
947 let other_file = (
948 std::path::PathBuf::from("toBeImported.kcl"),
949 r#"sketch001 = startSketchOn(XZ)
950profile001 = startProfile(sketch001, at = [281.54, 305.81])
951 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
952 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
953 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
954 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
955 |> close()
956extrude(profile001, length = 100)"#
957 .to_string(),
958 );
959
960 let other_file2 = (
961 std::path::PathBuf::from("toBeImported.kcl"),
962 r#"sketch001 = startSketchOn(XZ)
963profile001 = startProfile(sketch001, at = [281.54, 305.81])
964 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
965 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
966 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
967 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
968 |> close()
969extrude(profile001, length = 100)
970|> translate(z=100)
971"#
972 .to_string(),
973 );
974
975 let tmp_dir = std::env::temp_dir();
976 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
977
978 let tmp_file = tmp_dir.join(other_file.0);
980 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
981 std::fs::write(&tmp_file, other_file.1).unwrap();
982
983 let ExecTestResults { program, exec_ctxt, .. } =
984 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
985 .await
986 .unwrap();
987
988 std::fs::write(tmp_file, other_file2.1).unwrap();
990
991 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
992 new_program.compute_digest();
993
994 let result = get_changed_program(
995 CacheInformation::new(&program.ast, &exec_ctxt.settings),
996 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
997 )
998 .await;
999
1000 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
1001 panic!("Expected CheckImportsOnly, got {result:?}");
1002 };
1003
1004 assert_eq!(reapply_settings, false);
1005 exec_ctxt.close().await;
1006 }
1007
1008 #[tokio::test(flavor = "multi_thread")]
1009 async fn test_get_changed_program_added_outer_attribute() {
1010 let old_code = r#"import "tests/inputs/cube.step"
1011"#;
1012 let new_code = r#"@(coords = opengl)
1013import "tests/inputs/cube.step"
1014"#;
1015
1016 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1017
1018 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1019 new_program.compute_digest();
1020
1021 let result = get_changed_program(
1022 CacheInformation::new(&program.ast, &exec_ctxt.settings),
1023 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
1024 )
1025 .await;
1026
1027 assert_eq!(
1028 result,
1029 CacheResult::ReExecute {
1030 clear_scene: true,
1031 reapply_settings: false,
1032 program: new_program.ast,
1033 }
1034 );
1035 exec_ctxt.close().await;
1036 }
1037
1038 #[tokio::test(flavor = "multi_thread")]
1039 async fn test_get_changed_program_different_outer_attribute() {
1040 let old_code = r#"@(coords = vulkan)
1041import "tests/inputs/cube.step"
1042"#;
1043 let new_code = r#"@(coords = opengl)
1044import "tests/inputs/cube.step"
1045"#;
1046
1047 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1048
1049 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1050 new_program.compute_digest();
1051
1052 let result = get_changed_program(
1053 CacheInformation::new(&program.ast, &exec_ctxt.settings),
1054 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
1055 )
1056 .await;
1057
1058 assert_eq!(
1059 result,
1060 CacheResult::ReExecute {
1061 clear_scene: true,
1062 reapply_settings: false,
1063 program: new_program.ast,
1064 }
1065 );
1066 exec_ctxt.close().await;
1067 }
1068}