kcl_lib/execution/
cache.rs

1//! Functions for helping with caching an ast and finding the parts the changed.
2
3use std::sync::Arc;
4
5use itertools::{EitherOrBoth, Itertools};
6use tokio::sync::RwLock;
7
8use crate::{
9    ExecOutcome, ExecutorContext,
10    execution::{
11        EnvironmentRef, ExecutorSettings, annotations,
12        memory::Stack,
13        state::{self as exec_state, ModuleInfoMap},
14    },
15    parsing::ast::types::{Annotation, Node, Program},
16    walk::Node as WalkNode,
17};
18
19lazy_static::lazy_static! {
20    /// A static mutable lock for updating the last successful execution state for the cache.
21    static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
22    // The last successful run's memory. Not cleared after an unsuccessful run.
23    static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
24}
25
26/// Read the old ast memory from the lock.
27pub(super) async fn read_old_ast() -> Option<GlobalState> {
28    let old_ast = OLD_AST.read().await;
29    old_ast.clone()
30}
31
32pub(super) async fn write_old_ast(old_state: GlobalState) {
33    let mut old_ast = OLD_AST.write().await;
34    *old_ast = Some(old_state);
35}
36
37pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
38    let old_mem = PREV_MEMORY.read().await;
39    old_mem.clone()
40}
41
42pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
43    let mut old_mem = PREV_MEMORY.write().await;
44    *old_mem = Some(mem);
45}
46
47pub async fn bust_cache() {
48    let mut old_ast = OLD_AST.write().await;
49    *old_ast = None;
50}
51
52pub async fn clear_mem_cache() {
53    let mut old_mem = PREV_MEMORY.write().await;
54    *old_mem = None;
55}
56
57/// Information for the caching an AST and smartly re-executing it if we can.
58#[derive(Debug, Clone)]
59pub struct CacheInformation<'a> {
60    pub ast: &'a Node<Program>,
61    pub settings: &'a ExecutorSettings,
62}
63
64/// The cached state of the whole program.
65#[derive(Debug, Clone)]
66pub(super) struct GlobalState {
67    pub(super) main: ModuleState,
68    /// The exec state.
69    pub(super) exec_state: exec_state::GlobalState,
70    /// The last settings used for execution.
71    pub(super) settings: ExecutorSettings,
72}
73
74impl GlobalState {
75    pub fn new(
76        state: exec_state::ExecState,
77        settings: ExecutorSettings,
78        ast: Node<Program>,
79        result_env: EnvironmentRef,
80    ) -> Self {
81        Self {
82            main: ModuleState {
83                ast,
84                exec_state: state.mod_local,
85                result_env,
86            },
87            exec_state: state.global,
88            settings,
89        }
90    }
91
92    pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
93        self.settings = settings;
94        self
95    }
96
97    pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
98        exec_state::ExecState {
99            global: self.exec_state.clone(),
100            mod_local: self.main.exec_state.clone(),
101        }
102    }
103
104    pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
105        // Fields are opt-in so that we don't accidentally leak private internal
106        // state when we add more to ExecState.
107        ExecOutcome {
108            variables: self.main.exec_state.variables(self.main.result_env),
109            filenames: self.exec_state.filenames(),
110            #[cfg(feature = "artifact-graph")]
111            operations: self.exec_state.root_module_artifacts.operations,
112            #[cfg(feature = "artifact-graph")]
113            artifact_graph: self.exec_state.artifacts.graph,
114            errors: self.exec_state.errors,
115            default_planes: ctx.engine.get_default_planes().read().await.clone(),
116        }
117    }
118}
119
120/// Per-module cached state
121#[derive(Debug, Clone)]
122pub(super) struct ModuleState {
123    /// The AST of the module.
124    pub(super) ast: Node<Program>,
125    /// The ExecState of the module.
126    pub(super) exec_state: exec_state::ModuleState,
127    /// The memory env for the module.
128    pub(super) result_env: EnvironmentRef,
129}
130
131/// The result of a cache check.
132#[derive(Debug, Clone, PartialEq)]
133#[allow(clippy::large_enum_variant)]
134pub(super) enum CacheResult {
135    ReExecute {
136        /// Should we clear the scene and start over?
137        clear_scene: bool,
138        /// Do we need to reapply settings?
139        reapply_settings: bool,
140        /// The program that needs to be executed.
141        program: Node<Program>,
142    },
143    /// Check only the imports, and not the main program.
144    /// Before sending this we already checked the main program and it is the same.
145    /// And we made sure the import statements > 0.
146    CheckImportsOnly {
147        /// Argument is whether we need to reapply settings.
148        reapply_settings: bool,
149        /// The ast of the main file, which did not change.
150        ast: Node<Program>,
151    },
152    /// Argument is whether we need to reapply settings.
153    NoAction(bool),
154}
155
156/// Given an old ast, old program memory and new ast, find the parts of the code that need to be
157/// re-executed.
158/// This function should never error, because in the case of any internal error, we should just pop
159/// the cache.
160///
161/// Returns `None` when there are no changes to the program, i.e. it is
162/// fully cached.
163pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
164    let mut reapply_settings = false;
165
166    // If the settings are different we might need to bust the cache.
167    // We specifically do this before checking if they are the exact same.
168    if old.settings != new.settings {
169        // If anything else is different we may not need to re-execute, but rather just
170        // run the settings again.
171        reapply_settings = true;
172    }
173
174    // If the ASTs are the EXACT same we return None.
175    // We don't even need to waste time computing the digests.
176    if old.ast == new.ast {
177        // First we need to make sure an imported file didn't change it's ast.
178        // We know they have the same imports because the ast is the same.
179        // If we have no imports, we can skip this.
180        if !old.ast.has_import_statements() {
181            return CacheResult::NoAction(reapply_settings);
182        }
183
184        // Tell the CacheResult we need to check all the imports, but the main ast is the same.
185        return CacheResult::CheckImportsOnly {
186            reapply_settings,
187            ast: old.ast.clone(),
188        };
189    }
190
191    // We have to clone just because the digests are stored inline :-(
192    let mut old_ast = old.ast.clone();
193    let mut new_ast = new.ast.clone();
194
195    // The digests should already be computed, but just in case we don't
196    // want to compare against none.
197    old_ast.compute_digest();
198    new_ast.compute_digest();
199
200    // Check if the digest is the same.
201    if old_ast.digest == new_ast.digest {
202        // First we need to make sure an imported file didn't change it's ast.
203        // We know they have the same imports because the ast is the same.
204        // If we have no imports, we can skip this.
205        if !old.ast.has_import_statements() {
206            return CacheResult::NoAction(reapply_settings);
207        }
208
209        // Tell the CacheResult we need to check all the imports, but the main ast is the same.
210        return CacheResult::CheckImportsOnly {
211            reapply_settings,
212            ast: old.ast.clone(),
213        };
214    }
215
216    // Check if the annotations are different.
217    if !old_ast
218        .inner_attrs
219        .iter()
220        .filter(annotations::is_significant)
221        .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
222        .all(|pair| {
223            match pair {
224                EitherOrBoth::Both(old, new) => {
225                    // Compare annotations, ignoring source ranges.  Digests must
226                    // have been computed before this.
227                    let Annotation { name, properties, .. } = &old.inner;
228                    let Annotation {
229                        name: new_name,
230                        properties: new_properties,
231                        ..
232                    } = &new.inner;
233
234                    name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
235                        && properties
236                            .as_ref()
237                            .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
238                            == new_properties
239                                .as_ref()
240                                .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
241                }
242                _ => false,
243            }
244        })
245    {
246        // If any of the annotations are different at the beginning of the
247        // program, it's likely the settings, and we have to bust the cache and
248        // re-execute the whole thing.
249        return CacheResult::ReExecute {
250            clear_scene: true,
251            reapply_settings: true,
252            program: new.ast.clone(),
253        };
254    }
255
256    // Check if the changes were only to Non-code areas, like comments or whitespace.
257    generate_changed_program(old_ast, new_ast, reapply_settings)
258}
259
260/// Force-generate a new CacheResult, even if one shouldn't be made. The
261/// way in which this gets invoked should always be through
262/// [get_changed_program]. This is purely to contain the logic on
263/// how we construct a new [CacheResult].
264///
265/// A CacheResult's program may be a *diff* of only the parts that need
266/// to be executed (only in the case of "pure additions" at time of writing.).
267/// This diff-based AST should not be persisted or used anywhere beyond the execution flow,
268/// as it will be incomplete.
269///
270/// Digests *must* be computed before calling this.
271fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
272    if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
273        let old_node: WalkNode = old.into();
274        let new_node: WalkNode = new.into();
275        old_node.digest() == new_node.digest()
276    }) {
277        // If any of the nodes are different in the stretch of body that
278        // overlaps, we have to bust cache and rebuild the scene. This
279        // means a single insertion or deletion will result in a cache
280        // bust.
281
282        return CacheResult::ReExecute {
283            clear_scene: true,
284            reapply_settings,
285            program: new_ast,
286        };
287    }
288
289    // otherwise the overlapping section of the ast bodies matches.
290    // Let's see what the rest of the slice looks like.
291
292    match new_ast.body.len().cmp(&old_ast.body.len()) {
293        std::cmp::Ordering::Less => {
294            // the new AST is shorter than the old AST -- statements
295            // were removed from the "current" code in the "new" code.
296            //
297            // Statements up until now match which means this is a
298            // "pure delete" of the remaining slice, when we get to
299            // supporting that.
300
301            // Cache bust time.
302            CacheResult::ReExecute {
303                clear_scene: true,
304                reapply_settings,
305                program: new_ast,
306            }
307        }
308        std::cmp::Ordering::Greater => {
309            // the new AST is longer than the old AST, which means
310            // statements were added to the new code we haven't previously
311            // seen.
312            //
313            // Statements up until now are the same, which means this
314            // is a "pure addition" of the remaining slice.
315
316            new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
317
318            CacheResult::ReExecute {
319                clear_scene: false,
320                reapply_settings,
321                program: new_ast,
322            }
323        }
324        std::cmp::Ordering::Equal => {
325            // currently unreachable, but let's pretend like the code
326            // above can do something meaningful here for when we get
327            // to diffing and yanking chunks of the program apart.
328
329            // We don't actually want to do anything here; so we're going
330            // to not clear and do nothing. Is this wrong? I don't think
331            // so but i think many things. This def needs to change
332            // when the code above changes.
333
334            CacheResult::NoAction(reapply_settings)
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use pretty_assertions::assert_eq;
342
343    use super::*;
344    use crate::execution::{ExecTestResults, parse_execute, parse_execute_with_project_dir};
345
346    #[tokio::test(flavor = "multi_thread")]
347    async fn test_get_changed_program_same_code() {
348        let new = r#"// Remove the end face for the extrusion.
349firstSketch = startSketchOn(XY)
350  |> startProfile(at = [-12, 12])
351  |> line(end = [24, 0])
352  |> line(end = [0, -24])
353  |> line(end = [-24, 0])
354  |> close()
355  |> extrude(length = 6)
356
357// Remove the end face for the extrusion.
358shell(firstSketch, faces = [END], thickness = 0.25)"#;
359
360        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
361
362        let result = get_changed_program(
363            CacheInformation {
364                ast: &program.ast,
365                settings: &exec_ctxt.settings,
366            },
367            CacheInformation {
368                ast: &program.ast,
369                settings: &exec_ctxt.settings,
370            },
371        )
372        .await;
373
374        assert_eq!(result, CacheResult::NoAction(false));
375    }
376
377    #[tokio::test(flavor = "multi_thread")]
378    async fn test_get_changed_program_same_code_changed_whitespace() {
379        let old = r#" // Remove the end face for the extrusion.
380firstSketch = startSketchOn(XY)
381  |> startProfile(at = [-12, 12])
382  |> line(end = [24, 0])
383  |> line(end = [0, -24])
384  |> line(end = [-24, 0])
385  |> close()
386  |> extrude(length = 6)
387
388// Remove the end face for the extrusion.
389shell(firstSketch, faces = [END], thickness = 0.25) "#;
390
391        let new = r#"// Remove the end face for the extrusion.
392firstSketch = startSketchOn(XY)
393  |> startProfile(at = [-12, 12])
394  |> line(end = [24, 0])
395  |> line(end = [0, -24])
396  |> line(end = [-24, 0])
397  |> close()
398  |> extrude(length = 6)
399
400// Remove the end face for the extrusion.
401shell(firstSketch, faces = [END], thickness = 0.25)"#;
402
403        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
404
405        let program_new = crate::Program::parse_no_errs(new).unwrap();
406
407        let result = get_changed_program(
408            CacheInformation {
409                ast: &program.ast,
410                settings: &exec_ctxt.settings,
411            },
412            CacheInformation {
413                ast: &program_new.ast,
414                settings: &exec_ctxt.settings,
415            },
416        )
417        .await;
418
419        assert_eq!(result, CacheResult::NoAction(false));
420    }
421
422    #[tokio::test(flavor = "multi_thread")]
423    async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
424        let old = r#" // Removed the end face for the extrusion.
425firstSketch = startSketchOn(XY)
426  |> startProfile(at = [-12, 12])
427  |> line(end = [24, 0])
428  |> line(end = [0, -24])
429  |> line(end = [-24, 0])
430  |> close()
431  |> extrude(length = 6)
432
433// Remove the end face for the extrusion.
434shell(firstSketch, faces = [END], thickness = 0.25) "#;
435
436        let new = r#"// Remove the end face for the extrusion.
437firstSketch = startSketchOn(XY)
438  |> startProfile(at = [-12, 12])
439  |> line(end = [24, 0])
440  |> line(end = [0, -24])
441  |> line(end = [-24, 0])
442  |> close()
443  |> extrude(length = 6)
444
445// Remove the end face for the extrusion.
446shell(firstSketch, faces = [END], thickness = 0.25)"#;
447
448        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
449
450        let program_new = crate::Program::parse_no_errs(new).unwrap();
451
452        let result = get_changed_program(
453            CacheInformation {
454                ast: &program.ast,
455                settings: &exec_ctxt.settings,
456            },
457            CacheInformation {
458                ast: &program_new.ast,
459                settings: &exec_ctxt.settings,
460            },
461        )
462        .await;
463
464        assert_eq!(result, CacheResult::NoAction(false));
465    }
466
467    #[tokio::test(flavor = "multi_thread")]
468    async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
469        let old = r#"@foo(whatever = whatever)
470@bar
471// Removed the end face for the extrusion.
472firstSketch = startSketchOn(XY)
473  |> startProfile(at = [-12, 12])
474  |> line(end = [24, 0])
475  |> line(end = [0, -24])
476  |> line(end = [-24, 0]) // my thing
477  |> close()
478  |> extrude(length = 6)
479
480// Remove the end face for the extrusion.
481shell(firstSketch, faces = [END], thickness = 0.25) "#;
482
483        let new = r#"@foo(whatever = 42)
484@baz
485// Remove the end face for the extrusion.
486firstSketch = startSketchOn(XY)
487  |> startProfile(at = [-12, 12])
488  |> line(end = [24, 0])
489  |> line(end = [0, -24])
490  |> line(end = [-24, 0])
491  |> close()
492  |> extrude(length = 6)
493
494// Remove the end face for the extrusion.
495shell(firstSketch, faces = [END], thickness = 0.25)"#;
496
497        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
498
499        let program_new = crate::Program::parse_no_errs(new).unwrap();
500
501        let result = get_changed_program(
502            CacheInformation {
503                ast: &program.ast,
504                settings: &exec_ctxt.settings,
505            },
506            CacheInformation {
507                ast: &program_new.ast,
508                settings: &exec_ctxt.settings,
509            },
510        )
511        .await;
512
513        assert_eq!(result, CacheResult::NoAction(false));
514    }
515
516    // Changing the grid settings with the exact same file should NOT bust the cache.
517    #[tokio::test(flavor = "multi_thread")]
518    async fn test_get_changed_program_same_code_but_different_grid_setting() {
519        let new = r#"// Remove the end face for the extrusion.
520firstSketch = startSketchOn(XY)
521  |> startProfile(at = [-12, 12])
522  |> line(end = [24, 0])
523  |> line(end = [0, -24])
524  |> line(end = [-24, 0])
525  |> close()
526  |> extrude(length = 6)
527
528// Remove the end face for the extrusion.
529shell(firstSketch, faces = [END], thickness = 0.25)"#;
530
531        let ExecTestResults {
532            program, mut exec_ctxt, ..
533        } = parse_execute(new).await.unwrap();
534
535        // Change the settings.
536        exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
537
538        let result = get_changed_program(
539            CacheInformation {
540                ast: &program.ast,
541                settings: &Default::default(),
542            },
543            CacheInformation {
544                ast: &program.ast,
545                settings: &exec_ctxt.settings,
546            },
547        )
548        .await;
549
550        assert_eq!(result, CacheResult::NoAction(true));
551    }
552
553    // Changing the edge visibility settings with the exact same file should NOT bust the cache.
554    #[tokio::test(flavor = "multi_thread")]
555    async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
556        let new = 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 ExecTestResults {
569            program, mut exec_ctxt, ..
570        } = parse_execute(new).await.unwrap();
571
572        // Change the settings.
573        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
574
575        let result = get_changed_program(
576            CacheInformation {
577                ast: &program.ast,
578                settings: &Default::default(),
579            },
580            CacheInformation {
581                ast: &program.ast,
582                settings: &exec_ctxt.settings,
583            },
584        )
585        .await;
586
587        assert_eq!(result, CacheResult::NoAction(true));
588
589        // Change the settings back.
590        let old_settings = exec_ctxt.settings.clone();
591        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
592
593        let result = get_changed_program(
594            CacheInformation {
595                ast: &program.ast,
596                settings: &old_settings,
597            },
598            CacheInformation {
599                ast: &program.ast,
600                settings: &exec_ctxt.settings,
601            },
602        )
603        .await;
604
605        assert_eq!(result, CacheResult::NoAction(true));
606
607        // Change the settings back.
608        let old_settings = exec_ctxt.settings.clone();
609        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
610
611        let result = get_changed_program(
612            CacheInformation {
613                ast: &program.ast,
614                settings: &old_settings,
615            },
616            CacheInformation {
617                ast: &program.ast,
618                settings: &exec_ctxt.settings,
619            },
620        )
621        .await;
622
623        assert_eq!(result, CacheResult::NoAction(true));
624    }
625
626    // Changing the units settings using an annotation with the exact same file
627    // should bust the cache.
628    #[tokio::test(flavor = "multi_thread")]
629    async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
630        let old_code = r#"@settings(defaultLengthUnit = in)
631startSketchOn(XY)
632"#;
633        let new_code = r#"@settings(defaultLengthUnit = mm)
634startSketchOn(XY)
635"#;
636
637        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
638
639        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
640        new_program.compute_digest();
641
642        let result = get_changed_program(
643            CacheInformation {
644                ast: &program.ast,
645                settings: &exec_ctxt.settings,
646            },
647            CacheInformation {
648                ast: &new_program.ast,
649                settings: &exec_ctxt.settings,
650            },
651        )
652        .await;
653
654        assert_eq!(
655            result,
656            CacheResult::ReExecute {
657                clear_scene: true,
658                reapply_settings: true,
659                program: new_program.ast,
660            }
661        );
662    }
663
664    // Removing the units settings using an annotation, when it was non-default
665    // units, with the exact same file should bust the cache.
666    #[tokio::test(flavor = "multi_thread")]
667    async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
668        let old_code = r#"@settings(defaultLengthUnit = in)
669startSketchOn(XY)
670"#;
671        let new_code = r#"
672startSketchOn(XY)
673"#;
674
675        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
676
677        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
678        new_program.compute_digest();
679
680        let result = get_changed_program(
681            CacheInformation {
682                ast: &program.ast,
683                settings: &exec_ctxt.settings,
684            },
685            CacheInformation {
686                ast: &new_program.ast,
687                settings: &exec_ctxt.settings,
688            },
689        )
690        .await;
691
692        assert_eq!(
693            result,
694            CacheResult::ReExecute {
695                clear_scene: true,
696                reapply_settings: true,
697                program: new_program.ast,
698            }
699        );
700    }
701
702    #[tokio::test(flavor = "multi_thread")]
703    async fn test_multi_file_no_changes_does_not_reexecute() {
704        let code = r#"import "toBeImported.kcl" as importedCube
705
706importedCube
707
708sketch001 = startSketchOn(XZ)
709profile001 = startProfile(sketch001, at = [-134.53, -56.17])
710  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
711  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
712  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
713  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
714  |> close()
715extrude001 = extrude(profile001, length = 100)
716sketch003 = startSketchOn(extrude001, face = seg02)
717sketch002 = startSketchOn(extrude001, face = seg01)
718"#;
719
720        let other_file = (
721            std::path::PathBuf::from("toBeImported.kcl"),
722            r#"sketch001 = startSketchOn(XZ)
723profile001 = startProfile(sketch001, at = [281.54, 305.81])
724  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
725  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
726  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
727  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
728  |> close()
729extrude(profile001, length = 100)"#
730                .to_string(),
731        );
732
733        let tmp_dir = std::env::temp_dir();
734        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
735
736        // Create a temporary file for each of the other files.
737        let tmp_file = tmp_dir.join(other_file.0);
738        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
739        std::fs::write(tmp_file, other_file.1).unwrap();
740
741        let ExecTestResults { program, exec_ctxt, .. } =
742            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
743                .await
744                .unwrap();
745
746        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
747        new_program.compute_digest();
748
749        let result = get_changed_program(
750            CacheInformation {
751                ast: &program.ast,
752                settings: &exec_ctxt.settings,
753            },
754            CacheInformation {
755                ast: &new_program.ast,
756                settings: &exec_ctxt.settings,
757            },
758        )
759        .await;
760
761        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
762            panic!("Expected CheckImportsOnly, got {result:?}");
763        };
764
765        assert_eq!(reapply_settings, false);
766    }
767
768    #[tokio::test(flavor = "multi_thread")]
769    async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
770        let code = r#"import "toBeImported.kcl" as importedCube
771
772importedCube
773
774sketch001 = startSketchOn(XZ)
775profile001 = startProfile(sketch001, at = [-134.53, -56.17])
776  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
777  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
778  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
779  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
780  |> close()
781extrude001 = extrude(profile001, length = 100)
782sketch003 = startSketchOn(extrude001, face = seg02)
783sketch002 = startSketchOn(extrude001, face = seg01)
784"#;
785
786        let other_file = (
787            std::path::PathBuf::from("toBeImported.kcl"),
788            r#"sketch001 = startSketchOn(XZ)
789profile001 = startProfile(sketch001, at = [281.54, 305.81])
790  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
791  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
792  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
793  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
794  |> close()
795extrude(profile001, length = 100)"#
796                .to_string(),
797        );
798
799        let other_file2 = (
800            std::path::PathBuf::from("toBeImported.kcl"),
801            r#"sketch001 = startSketchOn(XZ)
802profile001 = startProfile(sketch001, at = [281.54, 305.81])
803  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
804  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
805  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
806  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
807  |> close()
808extrude(profile001, length = 100)
809|> translate(z=100) 
810"#
811            .to_string(),
812        );
813
814        let tmp_dir = std::env::temp_dir();
815        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
816
817        // Create a temporary file for each of the other files.
818        let tmp_file = tmp_dir.join(other_file.0);
819        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
820        std::fs::write(&tmp_file, other_file.1).unwrap();
821
822        let ExecTestResults { program, exec_ctxt, .. } =
823            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
824                .await
825                .unwrap();
826
827        // Change the other file.
828        std::fs::write(tmp_file, other_file2.1).unwrap();
829
830        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
831        new_program.compute_digest();
832
833        let result = get_changed_program(
834            CacheInformation {
835                ast: &program.ast,
836                settings: &exec_ctxt.settings,
837            },
838            CacheInformation {
839                ast: &new_program.ast,
840                settings: &exec_ctxt.settings,
841            },
842        )
843        .await;
844
845        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
846            panic!("Expected CheckImportsOnly, got {result:?}");
847        };
848
849        assert_eq!(reapply_settings, false);
850    }
851}