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