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