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