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