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}
45
46#[derive(Debug, Clone)]
52pub struct Cache(Arc<dyn CacheStorage>);
53
54trait CacheStorage: std::fmt::Debug + Send + Sync {
56 fn inner(&self) -> &RwLock<CacheInner>;
57}
58
59impl CacheStorage for RwLock<CacheInner> {
60 fn inner(&self) -> &RwLock<CacheInner> {
61 self
62 }
63}
64
65impl Default for Cache {
66 fn default() -> Self {
67 Self(Arc::new(RwLock::new(CacheInner::default())))
68 }
69}
70
71impl Cache {
72 pub(super) async fn read_old_ast(&self) -> Option<GlobalState> {
74 self.0.inner().read().await.old_ast.clone()
75 }
76
77 pub(super) async fn write_old_ast(&self, old_state: GlobalState) {
78 let mut inner = self.0.inner().write().await;
79 inner.old_ast = Some(old_state);
80 }
81
82 pub(crate) async fn read_old_memory(&self) -> Option<SketchModeState> {
83 self.0.inner().read().await.prev_memory.clone()
84 }
85
86 pub(crate) async fn write_old_memory(&self, mem: SketchModeState) {
87 let mut inner = self.0.inner().write().await;
88 inner.prev_memory = Some(mem);
89 }
90
91 pub async fn bust_cache(&self) {
93 let mut inner = self.0.inner().write().await;
94 inner.old_ast = None;
95 }
96
97 pub async fn clear_mem_cache(&self) {
99 let mut inner = self.0.inner().write().await;
100 inner.prev_memory = None;
101 }
102
103 pub async fn read_old_memory_var(&self, name: &str) -> Option<KclValue> {
105 self.read_old_memory()
106 .await
107 .and_then(|memory| memory.stack.get(name, SourceRange::default()).ok())
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct CacheInformation<'a> {
114 pub ast: &'a Node<Program>,
115 pub settings: &'a ExecutorSettings,
116 pub kcl_version: KclVersion,
118}
119
120impl<'a> CacheInformation<'a> {
121 #[cfg(test)]
122 fn new(ast: &'a Node<Program>, settings: &'a ExecutorSettings) -> Self {
123 let kcl_version = computed_kcl_version(ast);
125 Self {
126 ast,
127 settings,
128 kcl_version,
129 }
130 }
131}
132
133#[derive(Debug, Clone)]
135pub(super) struct GlobalState {
136 pub(super) main: ModuleState,
137 pub(super) exec_state: exec_state::GlobalState,
139 pub(super) settings: ExecutorSettings,
141 pub kcl_version: KclVersion,
143}
144
145impl GlobalState {
146 pub fn new(
147 state: exec_state::ExecState,
148 settings: ExecutorSettings,
149 kcl_version: KclVersion,
150 ast: Node<Program>,
151 result_env: EnvironmentRef,
152 ) -> Self {
153 Self {
154 main: ModuleState {
155 ast,
156 exec_state: state.mod_local,
157 result_env,
158 },
159 exec_state: state.global,
160 settings,
161 kcl_version,
162 }
163 }
164
165 pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
166 self.settings = settings;
167 self
168 }
169
170 pub fn reconstitute_exec_state(&self, ctx: &ExecutorContext) -> exec_state::ExecState {
171 exec_state::ExecState {
172 execution_callbacks: ctx.execution_callbacks.clone(),
173 global: self.exec_state.clone(),
174 mod_local: self.main.exec_state.clone(),
175 }
176 }
177
178 pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> Result<ExecOutcome, KclError> {
179 let variables = self.main.exec_state.variables(self.main.result_env)?;
182 #[cfg(test)]
183 let test_program_memory = variables.clone();
184 let variables = variables
185 .into_iter()
186 .map(|(key, value)| (key, KclValueView::from(value)))
187 .collect();
188 Ok(ExecOutcome {
189 variables,
190 filenames: self.exec_state.filenames(),
191 from_cache: false,
192 operations: self.exec_state.operations_by_module(),
193 artifact_graph: self.exec_state.artifacts.graph,
194 scene_objects: self.exec_state.root_module_artifacts.scene_objects,
195 source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
196 var_solutions: self.exec_state.root_module_artifacts.var_solutions,
197 refactor_metadata: self.exec_state.root_module_artifacts.refactor_metadata.clone(),
198 issues: self.exec_state.issues,
199 source_files: self.exec_state.id_to_source,
200 default_planes: ctx.engine.get_default_planes().read().await.clone(),
201 #[cfg(test)]
202 test_program_memory,
203 })
204 }
205
206 pub fn mock_memory_state(&self) -> Result<SketchModeState, KclError> {
207 let mut stack = self.main.exec_state.stack.deep_clone()?;
208 stack.restore_env(self.main.result_env)?;
209
210 Ok(SketchModeState {
211 stack,
212 module_infos: self.exec_state.module_infos.clone(),
213 path_to_source_id: self.exec_state.path_to_source_id.clone(),
214 id_to_source: self.exec_state.id_to_source.clone(),
215 never_type_ranges: self.exec_state.never_type_ranges.clone(),
216 constraint_state: self.main.exec_state.constraint_state.clone(),
217 scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
218 std_not_yet_added: self.exec_state.std_not_yet_added.clone(),
219 kcl_version: self.exec_state.entry_point_kcl_version.unwrap_or_default(),
220 })
221 }
222}
223
224#[derive(Debug, Clone)]
226pub(super) struct ModuleState {
227 pub(super) ast: Node<Program>,
229 pub(super) exec_state: exec_state::ModuleState,
231 pub(super) result_env: EnvironmentRef,
233}
234
235#[derive(Debug, Clone)]
237pub(crate) struct SketchModeState {
238 pub stack: Stack,
240 pub module_infos: ModuleInfoMap,
242 pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
244 pub id_to_source: IndexMap<ModuleId, ModuleSource>,
246 pub never_type_ranges: IndexMap<ModuleId, Vec<SourceRange>>,
248 pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
250 pub scene_objects: Vec<Object>,
252 pub std_not_yet_added: IndexMap<String, NotYetAdded>,
255 pub kcl_version: KclVersion,
258}
259
260impl SketchModeState {
261 pub(crate) fn reusable_for(&self, kcl_version: KclVersion) -> bool {
268 self.kcl_version == kcl_version
269 }
270}
271
272#[cfg(test)]
273impl SketchModeState {
274 pub(crate) fn new_for_tests() -> Self {
275 Self {
276 stack: Stack::new_for_tests(),
277 module_infos: ModuleInfoMap::default(),
278 path_to_source_id: Default::default(),
279 id_to_source: Default::default(),
280 never_type_ranges: Default::default(),
281 constraint_state: Default::default(),
282 scene_objects: Vec::new(),
283 std_not_yet_added: Default::default(),
284 kcl_version: KclVersion::default(),
285 }
286 }
287}
288
289#[derive(Debug, Clone, PartialEq)]
291#[allow(clippy::large_enum_variant)]
292pub(super) enum CacheResult {
293 ReExecute {
294 clear_scene: bool,
296 reapply_settings: bool,
298 program: Node<Program>,
300 },
301 CheckImportsOnly {
305 reapply_settings: bool,
307 ast: Node<Program>,
309 },
310 NoAction(bool),
312}
313
314pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
319 let mut reapply_settings = false;
320
321 if old.kcl_version != new.kcl_version {
322 return CacheResult::ReExecute {
325 clear_scene: true,
326 reapply_settings: true,
327 program: new.ast.clone(),
328 };
329 }
330
331 if old.settings != new.settings {
334 reapply_settings = true;
337 }
338
339 if old.ast == new.ast {
342 if !old.ast.has_import_statements() {
346 return CacheResult::NoAction(reapply_settings);
347 }
348
349 return CacheResult::CheckImportsOnly {
351 reapply_settings,
352 ast: old.ast.clone(),
353 };
354 }
355
356 let mut old_ast = old.ast.clone();
358 let mut new_ast = new.ast.clone();
359
360 old_ast.compute_digest();
363 new_ast.compute_digest();
364
365 if old_ast.digest == new_ast.digest {
367 if !old.ast.has_import_statements() {
371 return CacheResult::NoAction(reapply_settings);
372 }
373
374 return CacheResult::CheckImportsOnly {
376 reapply_settings,
377 ast: old.ast.clone(),
378 };
379 }
380
381 if !old_ast
383 .inner_attrs
384 .iter()
385 .filter(annotations::is_significant)
386 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
387 .all(|pair| {
388 match pair {
389 EitherOrBoth::Both(old, new) => {
390 let Annotation { name, properties, .. } = &old.inner;
393 let Annotation {
394 name: new_name,
395 properties: new_properties,
396 ..
397 } = &new.inner;
398
399 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
400 && properties
401 .as_ref()
402 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
403 == new_properties
404 .as_ref()
405 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
406 }
407 _ => false,
408 }
409 })
410 {
411 return CacheResult::ReExecute {
415 clear_scene: true,
416 reapply_settings: true,
417 program: new.ast.clone(),
418 };
419 }
420
421 generate_changed_program(old_ast, new_ast, reapply_settings)
423}
424
425fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
437 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
438 let old_node: WalkNode = old.into();
439 let new_node: WalkNode = new.into();
440 old_node.digest() == new_node.digest()
441 }) {
442 return CacheResult::ReExecute {
448 clear_scene: true,
449 reapply_settings,
450 program: new_ast,
451 };
452 }
453
454 match new_ast.body.len().cmp(&old_ast.body.len()) {
458 std::cmp::Ordering::Less => {
459 CacheResult::ReExecute {
468 clear_scene: true,
469 reapply_settings,
470 program: new_ast,
471 }
472 }
473 std::cmp::Ordering::Greater => {
474 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
482
483 CacheResult::ReExecute {
484 clear_scene: false,
485 reapply_settings,
486 program: new_ast,
487 }
488 }
489 std::cmp::Ordering::Equal => {
490 CacheResult::NoAction(reapply_settings)
500 }
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use pretty_assertions::assert_eq;
507
508 use super::*;
509 use crate::execution::ExecTestResults;
510 use crate::execution::parse_execute;
511 use crate::execution::parse_execute_with_project_dir;
512
513 #[tokio::test(flavor = "multi_thread")]
514 async fn test_get_changed_program_same_code() {
515 let new = r#"// Remove the end face for the extrusion.
516firstSketch = startSketchOn(XY)
517 |> startProfile(at = [-12, 12])
518 |> line(end = [24, 0])
519 |> line(end = [0, -24])
520 |> line(end = [-24, 0])
521 |> close()
522 |> extrude(length = 6)
523
524// Remove the end face for the extrusion.
525shell(firstSketch, faces = [END], thickness = 0.25)"#;
526
527 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
528
529 let result = get_changed_program(
530 CacheInformation::new(&program.ast, &exec_ctxt.settings),
531 CacheInformation::new(&program.ast, &exec_ctxt.settings),
532 )
533 .await;
534
535 assert_eq!(result, CacheResult::NoAction(false));
536 exec_ctxt.close().await;
537 }
538
539 #[tokio::test(flavor = "multi_thread")]
540 async fn test_get_changed_program_same_code_changed_whitespace() {
541 let old = r#" // Remove the end face for the extrusion.
542firstSketch = startSketchOn(XY)
543 |> startProfile(at = [-12, 12])
544 |> line(end = [24, 0])
545 |> line(end = [0, -24])
546 |> line(end = [-24, 0])
547 |> close()
548 |> extrude(length = 6)
549
550// Remove the end face for the extrusion.
551shell(firstSketch, faces = [END], thickness = 0.25) "#;
552
553 let new = r#"// Remove the end face for the extrusion.
554firstSketch = startSketchOn(XY)
555 |> startProfile(at = [-12, 12])
556 |> line(end = [24, 0])
557 |> line(end = [0, -24])
558 |> line(end = [-24, 0])
559 |> close()
560 |> extrude(length = 6)
561
562// Remove the end face for the extrusion.
563shell(firstSketch, faces = [END], thickness = 0.25)"#;
564
565 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
566
567 let program_new = crate::Program::parse_no_errs(new).unwrap();
568
569 let result = get_changed_program(
570 CacheInformation::new(&program.ast, &exec_ctxt.settings),
571 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
572 )
573 .await;
574
575 assert_eq!(result, CacheResult::NoAction(false));
576 exec_ctxt.close().await;
577 }
578
579 #[tokio::test(flavor = "multi_thread")]
580 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
581 let old = r#" // Removed the end face for the extrusion.
582firstSketch = startSketchOn(XY)
583 |> startProfile(at = [-12, 12])
584 |> line(end = [24, 0])
585 |> line(end = [0, -24])
586 |> line(end = [-24, 0])
587 |> close()
588 |> extrude(length = 6)
589
590// Remove the end face for the extrusion.
591shell(firstSketch, faces = [END], thickness = 0.25) "#;
592
593 let new = r#"// Remove the end face for the extrusion.
594firstSketch = startSketchOn(XY)
595 |> startProfile(at = [-12, 12])
596 |> line(end = [24, 0])
597 |> line(end = [0, -24])
598 |> line(end = [-24, 0])
599 |> close()
600 |> extrude(length = 6)
601
602// Remove the end face for the extrusion.
603shell(firstSketch, faces = [END], thickness = 0.25)"#;
604
605 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
606
607 let program_new = crate::Program::parse_no_errs(new).unwrap();
608
609 let result = get_changed_program(
610 CacheInformation::new(&program.ast, &exec_ctxt.settings),
611 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
612 )
613 .await;
614
615 assert_eq!(result, CacheResult::NoAction(false));
616 exec_ctxt.close().await;
617 }
618
619 #[tokio::test(flavor = "multi_thread")]
620 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
621 let old = r#"@foo(whatever = whatever)
622@bar
623// Removed the end face for the extrusion.
624firstSketch = startSketchOn(XY)
625 |> startProfile(at = [-12, 12])
626 |> line(end = [24, 0])
627 |> line(end = [0, -24])
628 |> line(end = [-24, 0]) // my thing
629 |> close()
630 |> extrude(length = 6)
631
632// Remove the end face for the extrusion.
633shell(firstSketch, faces = [END], thickness = 0.25) "#;
634
635 let new = r#"@foo(whatever = 42)
636@baz
637// 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 { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
650
651 let program_new = crate::Program::parse_no_errs(new).unwrap();
652
653 let result = get_changed_program(
654 CacheInformation::new(&program.ast, &exec_ctxt.settings),
655 CacheInformation::new(&program_new.ast, &exec_ctxt.settings),
656 )
657 .await;
658
659 assert_eq!(result, CacheResult::NoAction(false));
660 exec_ctxt.close().await;
661 }
662
663 #[tokio::test(flavor = "multi_thread")]
665 async fn test_get_changed_program_same_code_but_different_grid_setting() {
666 let new = r#"// Remove the end face for the extrusion.
667firstSketch = startSketchOn(XY)
668 |> startProfile(at = [-12, 12])
669 |> line(end = [24, 0])
670 |> line(end = [0, -24])
671 |> line(end = [-24, 0])
672 |> close()
673 |> extrude(length = 6)
674
675// Remove the end face for the extrusion.
676shell(firstSketch, faces = [END], thickness = 0.25)"#;
677
678 let ExecTestResults {
679 program, mut exec_ctxt, ..
680 } = parse_execute(new).await.unwrap();
681
682 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
684
685 let result = get_changed_program(
686 CacheInformation::new(&program.ast, &Default::default()),
687 CacheInformation::new(&program.ast, &exec_ctxt.settings),
688 )
689 .await;
690
691 assert_eq!(result, CacheResult::NoAction(true));
692 exec_ctxt.close().await;
693 }
694
695 #[tokio::test(flavor = "multi_thread")]
697 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
698 let new = r#"// Remove the end face for the extrusion.
699firstSketch = startSketchOn(XY)
700 |> startProfile(at = [-12, 12])
701 |> line(end = [24, 0])
702 |> line(end = [0, -24])
703 |> line(end = [-24, 0])
704 |> close()
705 |> extrude(length = 6)
706
707// Remove the end face for the extrusion.
708shell(firstSketch, faces = [END], thickness = 0.25)"#;
709
710 let ExecTestResults {
711 program, mut exec_ctxt, ..
712 } = parse_execute(new).await.unwrap();
713
714 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
716
717 let result = get_changed_program(
718 CacheInformation::new(&program.ast, &Default::default()),
719 CacheInformation::new(&program.ast, &exec_ctxt.settings),
720 )
721 .await;
722
723 assert_eq!(result, CacheResult::NoAction(true));
724
725 let old_settings = exec_ctxt.settings.clone();
727 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
728
729 let result = get_changed_program(
730 CacheInformation::new(&program.ast, &old_settings),
731 CacheInformation::new(&program.ast, &exec_ctxt.settings),
732 )
733 .await;
734
735 assert_eq!(result, CacheResult::NoAction(true));
736
737 let old_settings = exec_ctxt.settings.clone();
739 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
740
741 let result = get_changed_program(
742 CacheInformation::new(&program.ast, &old_settings),
743 CacheInformation::new(&program.ast, &exec_ctxt.settings),
744 )
745 .await;
746
747 assert_eq!(result, CacheResult::NoAction(true));
748 exec_ctxt.close().await;
749 }
750
751 #[tokio::test(flavor = "multi_thread")]
753 async fn test_get_changed_program_same_code_but_different_kcl_version_using_annotation() {
754 for (old_version, new_version) in [
755 ("2.0", "\"3.0-preview\""),
756 ("2.0", "3.0"),
757 ("3.0", "2.0"),
758 ("\"3.0-preview\"", "3.0"),
759 ("3.0", "\"3.0-preview\""),
760 ] {
761 let old_code = format!("@settings(kclVersion = {old_version})\nx = 1\ny = x + 1\n");
762 let new_code = format!("@settings(kclVersion = {new_version})\nx = 1\ny = x + 1\n");
763 let mut program = crate::Program::parse_no_errs(&old_code).unwrap();
764 program.compute_digest();
765 let mut new_program = crate::Program::parse_no_errs(&new_code).unwrap();
766 new_program.compute_digest();
767 let settings = ExecutorSettings::default();
768
769 let result = get_changed_program(
770 CacheInformation::new(&program.ast, &settings),
771 CacheInformation::new(&new_program.ast, &settings),
772 )
773 .await;
774
775 assert_eq!(
776 result,
777 CacheResult::ReExecute {
778 clear_scene: true,
779 reapply_settings: true,
780 program: new_program.ast,
781 },
782 "{old_version} -> {new_version}"
783 );
784 }
785 }
786
787 #[tokio::test(flavor = "multi_thread")]
790 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
791 let old_code = r#"@settings(defaultLengthUnit = in)
792startSketchOn(XY)
793"#;
794 let new_code = r#"@settings(defaultLengthUnit = mm)
795startSketchOn(XY)
796"#;
797
798 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
799
800 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
801 new_program.compute_digest();
802
803 let result = get_changed_program(
804 CacheInformation::new(&program.ast, &exec_ctxt.settings),
805 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
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")]
823 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
824 let old_code = r#"@settings(defaultLengthUnit = in)
825startSketchOn(XY)
826"#;
827 let new_code = r#"
828startSketchOn(XY)
829"#;
830
831 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
832
833 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
834 new_program.compute_digest();
835
836 let result = get_changed_program(
837 CacheInformation::new(&program.ast, &exec_ctxt.settings),
838 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
839 )
840 .await;
841
842 assert_eq!(
843 result,
844 CacheResult::ReExecute {
845 clear_scene: true,
846 reapply_settings: true,
847 program: new_program.ast,
848 }
849 );
850 exec_ctxt.close().await;
851 }
852
853 #[tokio::test(flavor = "multi_thread")]
854 async fn test_multi_file_no_changes_does_not_reexecute() {
855 let code = r#"import "toBeImported.kcl" as importedCube
856
857importedCube
858
859sketch001 = startSketchOn(XZ)
860profile001 = startProfile(sketch001, at = [-134.53, -56.17])
861 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
862 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
863 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
864 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
865 |> close()
866extrude001 = extrude(profile001, length = 100)
867sketch003 = startSketchOn(extrude001, face = seg02)
868sketch002 = startSketchOn(extrude001, face = seg01)
869"#;
870
871 let other_file = (
872 std::path::PathBuf::from("toBeImported.kcl"),
873 r#"sketch001 = startSketchOn(XZ)
874profile001 = startProfile(sketch001, at = [281.54, 305.81])
875 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
876 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
877 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
878 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
879 |> close()
880extrude(profile001, length = 100)"#
881 .to_string(),
882 );
883
884 let tmp_dir = std::env::temp_dir();
885 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
886
887 let tmp_file = tmp_dir.join(other_file.0);
889 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
890 std::fs::write(tmp_file, other_file.1).unwrap();
891
892 let ExecTestResults { program, exec_ctxt, .. } =
893 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
894 .await
895 .unwrap();
896
897 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
898 new_program.compute_digest();
899
900 let result = get_changed_program(
901 CacheInformation::new(&program.ast, &exec_ctxt.settings),
902 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
903 )
904 .await;
905
906 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
907 panic!("Expected CheckImportsOnly, got {result:?}");
908 };
909
910 assert_eq!(reapply_settings, false);
911 exec_ctxt.close().await;
912 }
913
914 #[tokio::test(flavor = "multi_thread")]
915 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
916 let code = r#"import "toBeImported.kcl" as importedCube
917
918importedCube
919
920sketch001 = startSketchOn(XZ)
921profile001 = startProfile(sketch001, at = [-134.53, -56.17])
922 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
923 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
924 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
925 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
926 |> close()
927extrude001 = extrude(profile001, length = 100)
928sketch003 = startSketchOn(extrude001, face = seg02)
929sketch002 = startSketchOn(extrude001, face = seg01)
930"#;
931
932 let other_file = (
933 std::path::PathBuf::from("toBeImported.kcl"),
934 r#"sketch001 = startSketchOn(XZ)
935profile001 = startProfile(sketch001, at = [281.54, 305.81])
936 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
937 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
938 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
939 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
940 |> close()
941extrude(profile001, length = 100)"#
942 .to_string(),
943 );
944
945 let other_file2 = (
946 std::path::PathBuf::from("toBeImported.kcl"),
947 r#"sketch001 = startSketchOn(XZ)
948profile001 = startProfile(sketch001, at = [281.54, 305.81])
949 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
950 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
951 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
952 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
953 |> close()
954extrude(profile001, length = 100)
955|> translate(z=100)
956"#
957 .to_string(),
958 );
959
960 let tmp_dir = std::env::temp_dir();
961 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
962
963 let tmp_file = tmp_dir.join(other_file.0);
965 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
966 std::fs::write(&tmp_file, other_file.1).unwrap();
967
968 let ExecTestResults { program, exec_ctxt, .. } =
969 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
970 .await
971 .unwrap();
972
973 std::fs::write(tmp_file, other_file2.1).unwrap();
975
976 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
977 new_program.compute_digest();
978
979 let result = get_changed_program(
980 CacheInformation::new(&program.ast, &exec_ctxt.settings),
981 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
982 )
983 .await;
984
985 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
986 panic!("Expected CheckImportsOnly, got {result:?}");
987 };
988
989 assert_eq!(reapply_settings, false);
990 exec_ctxt.close().await;
991 }
992
993 #[tokio::test(flavor = "multi_thread")]
994 async fn test_get_changed_program_added_outer_attribute() {
995 let old_code = r#"import "tests/inputs/cube.step"
996"#;
997 let new_code = r#"@(coords = opengl)
998import "tests/inputs/cube.step"
999"#;
1000
1001 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1002
1003 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1004 new_program.compute_digest();
1005
1006 let result = get_changed_program(
1007 CacheInformation::new(&program.ast, &exec_ctxt.settings),
1008 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
1009 )
1010 .await;
1011
1012 assert_eq!(
1013 result,
1014 CacheResult::ReExecute {
1015 clear_scene: true,
1016 reapply_settings: false,
1017 program: new_program.ast,
1018 }
1019 );
1020 exec_ctxt.close().await;
1021 }
1022
1023 #[tokio::test(flavor = "multi_thread")]
1024 async fn test_get_changed_program_different_outer_attribute() {
1025 let old_code = r#"@(coords = vulkan)
1026import "tests/inputs/cube.step"
1027"#;
1028 let new_code = r#"@(coords = opengl)
1029import "tests/inputs/cube.step"
1030"#;
1031
1032 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1033
1034 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1035 new_program.compute_digest();
1036
1037 let result = get_changed_program(
1038 CacheInformation::new(&program.ast, &exec_ctxt.settings),
1039 CacheInformation::new(&new_program.ast, &exec_ctxt.settings),
1040 )
1041 .await;
1042
1043 assert_eq!(
1044 result,
1045 CacheResult::ReExecute {
1046 clear_scene: true,
1047 reapply_settings: false,
1048 program: new_program.ast,
1049 }
1050 );
1051 exec_ctxt.close().await;
1052 }
1053}