Files
modeling-app/rust/kcl-lib/src/execution/cache.rs
Jonathan Tran 9a549ff379 Track artifact commands and operations per-module (#7426)
* Change so that operations are stored per module

* Refactor so that all modeling commands go through ExecState

* Remove unneeded PartialOrd implementations

* Remove artifact_commands from KclError since it was only for debugging

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-06-11 01:30:48 +00:00

848 lines
28 KiB
Rust

//! Functions for helping with caching an ast and finding the parts the changed.
use std::sync::Arc;
use itertools::{EitherOrBoth, Itertools};
use tokio::sync::RwLock;
use crate::{
execution::{
annotations,
memory::Stack,
state::{self as exec_state, ModuleInfoMap},
EnvironmentRef, ExecutorSettings,
},
parsing::ast::types::{Annotation, Node, Program},
walk::Node as WalkNode,
ExecOutcome, ExecutorContext,
};
lazy_static::lazy_static! {
/// A static mutable lock for updating the last successful execution state for the cache.
static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
// The last successful run's memory. Not cleared after an unssuccessful run.
static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
}
/// Read the old ast memory from the lock.
pub(super) async fn read_old_ast() -> Option<GlobalState> {
let old_ast = OLD_AST.read().await;
old_ast.clone()
}
pub(super) async fn write_old_ast(old_state: GlobalState) {
let mut old_ast = OLD_AST.write().await;
*old_ast = Some(old_state);
}
pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
let old_mem = PREV_MEMORY.read().await;
old_mem.clone()
}
pub(crate) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
let mut old_mem = PREV_MEMORY.write().await;
*old_mem = Some(mem);
}
pub async fn bust_cache() {
let mut old_ast = OLD_AST.write().await;
*old_ast = None;
}
pub async fn clear_mem_cache() {
let mut old_mem = PREV_MEMORY.write().await;
*old_mem = None;
}
/// Information for the caching an AST and smartly re-executing it if we can.
#[derive(Debug, Clone)]
pub struct CacheInformation<'a> {
pub ast: &'a Node<Program>,
pub settings: &'a ExecutorSettings,
}
/// The cached state of the whole program.
#[derive(Debug, Clone)]
pub(super) struct GlobalState {
pub(super) main: ModuleState,
/// The exec state.
pub(super) exec_state: exec_state::GlobalState,
/// The last settings used for execution.
pub(super) settings: ExecutorSettings,
}
impl GlobalState {
pub fn new(
state: exec_state::ExecState,
settings: ExecutorSettings,
ast: Node<Program>,
result_env: EnvironmentRef,
) -> Self {
Self {
main: ModuleState {
ast,
exec_state: state.mod_local,
result_env,
},
exec_state: state.global,
settings,
}
}
pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
self.settings = settings;
self
}
pub fn reconstitute_exec_state(&self) -> exec_state::ExecState {
exec_state::ExecState {
global: self.exec_state.clone(),
mod_local: self.main.exec_state.clone(),
}
}
pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
}
/// Per-module cached state
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The AST of the module.
pub(super) ast: Node<Program>,
/// The ExecState of the module.
pub(super) exec_state: exec_state::ModuleState,
/// The memory env for the module.
pub(super) result_env: EnvironmentRef,
}
/// The result of a cache check.
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub(super) enum CacheResult {
ReExecute {
/// Should we clear the scene and start over?
clear_scene: bool,
/// Do we need to reapply settings?
reapply_settings: bool,
/// The program that needs to be executed.
program: Node<Program>,
},
/// Check only the imports, and not the main program.
/// Before sending this we already checked the main program and it is the same.
/// And we made sure the import statements > 0.
CheckImportsOnly {
/// Argument is whether we need to reapply settings.
reapply_settings: bool,
/// The ast of the main file, which did not change.
ast: Node<Program>,
},
/// Argument is whether we need to reapply settings.
NoAction(bool),
}
/// Given an old ast, old program memory and new ast, find the parts of the code that need to be
/// re-executed.
/// This function should never error, because in the case of any internal error, we should just pop
/// the cache.
///
/// Returns `None` when there are no changes to the program, i.e. it is
/// fully cached.
pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
let mut reapply_settings = false;
// If the settings are different we might need to bust the cache.
// We specifically do this before checking if they are the exact same.
if old.settings != new.settings {
// If anything else is different we may not need to re-execute, but rather just
// run the settings again.
reapply_settings = true;
}
// If the ASTs are the EXACT same we return None.
// We don't even need to waste time computing the digests.
if old.ast == new.ast {
// First we need to make sure an imported file didn't change it's ast.
// We know they have the same imports because the ast is the same.
// If we have no imports, we can skip this.
if !old.ast.has_import_statements() {
return CacheResult::NoAction(reapply_settings);
}
// Tell the CacheResult we need to check all the imports, but the main ast is the same.
return CacheResult::CheckImportsOnly {
reapply_settings,
ast: old.ast.clone(),
};
}
// We have to clone just because the digests are stored inline :-(
let mut old_ast = old.ast.clone();
let mut new_ast = new.ast.clone();
// The digests should already be computed, but just in case we don't
// want to compare against none.
old_ast.compute_digest();
new_ast.compute_digest();
// Check if the digest is the same.
if old_ast.digest == new_ast.digest {
// First we need to make sure an imported file didn't change it's ast.
// We know they have the same imports because the ast is the same.
// If we have no imports, we can skip this.
if !old.ast.has_import_statements() {
return CacheResult::NoAction(reapply_settings);
}
// Tell the CacheResult we need to check all the imports, but the main ast is the same.
return CacheResult::CheckImportsOnly {
reapply_settings,
ast: old.ast.clone(),
};
}
// Check if the annotations are different.
if !old_ast
.inner_attrs
.iter()
.filter(annotations::is_significant)
.zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
.all(|pair| {
match pair {
EitherOrBoth::Both(old, new) => {
// Compare annotations, ignoring source ranges. Digests must
// have been computed before this.
let Annotation { name, properties, .. } = &old.inner;
let Annotation {
name: new_name,
properties: new_properties,
..
} = &new.inner;
name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
&& properties
.as_ref()
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
== new_properties
.as_ref()
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
}
_ => false,
}
})
{
// If any of the annotations are different at the beginning of the
// program, it's likely the settings, and we have to bust the cache and
// re-execute the whole thing.
return CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
};
}
// Check if the changes were only to Non-code areas, like comments or whitespace.
generate_changed_program(old_ast, new_ast, reapply_settings)
}
/// Force-generate a new CacheResult, even if one shouldn't be made. The
/// way in which this gets invoked should always be through
/// [get_changed_program]. This is purely to contain the logic on
/// how we construct a new [CacheResult].
///
/// Digests *must* be computed before calling this.
fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
let old_node: WalkNode = old.into();
let new_node: WalkNode = new.into();
old_node.digest() == new_node.digest()
}) {
// If any of the nodes are different in the stretch of body that
// overlaps, we have to bust cache and rebuild the scene. This
// means a single insertion or deletion will result in a cache
// bust.
return CacheResult::ReExecute {
clear_scene: true,
reapply_settings,
program: new_ast,
};
}
// otherwise the overlapping section of the ast bodies matches.
// Let's see what the rest of the slice looks like.
match new_ast.body.len().cmp(&old_ast.body.len()) {
std::cmp::Ordering::Less => {
// the new AST is shorter than the old AST -- statements
// were removed from the "current" code in the "new" code.
//
// Statements up until now match which means this is a
// "pure delete" of the remaining slice, when we get to
// supporting that.
// Cache bust time.
CacheResult::ReExecute {
clear_scene: true,
reapply_settings,
program: new_ast,
}
}
std::cmp::Ordering::Greater => {
// the new AST is longer than the old AST, which means
// statements were added to the new code we haven't previously
// seen.
//
// Statements up until now are the same, which means this
// is a "pure addition" of the remaining slice.
new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
CacheResult::ReExecute {
clear_scene: false,
reapply_settings,
program: new_ast,
}
}
std::cmp::Ordering::Equal => {
// currently unreachable, but let's pretend like the code
// above can do something meaningful here for when we get
// to diffing and yanking chunks of the program apart.
// We don't actually want to do anything here; so we're going
// to not clear and do nothing. Is this wrong? I don't think
// so but i think many things. This def needs to change
// when the code above changes.
CacheResult::NoAction(reapply_settings)
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::execution::{parse_execute, parse_execute_with_project_dir, ExecTestResults};
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(false));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_whitespace() {
let old = r#" // Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &program_new.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(false));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
let old = r#" // Removed the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25) "#;
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &program_new.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(false));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
let old = r#"@foo(whatever = whatever)
@bar
// Removed the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0]) // my thing
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25) "#;
let new = r#"@foo(whatever = 42)
@baz
// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
let program_new = crate::Program::parse_no_errs(new).unwrap();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &program_new.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(false));
}
// Changing the grid settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_grid_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults {
program, mut exec_ctxt, ..
} = parse_execute(new).await.unwrap();
// Change the settings.
exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &Default::default(),
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(true));
}
// Changing the edge visibility settings with the exact same file should NOT bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
let new = r#"// Remove the end face for the extrusion.
firstSketch = startSketchOn(XY)
|> startProfile(at = [-12, 12])
|> line(end = [24, 0])
|> line(end = [0, -24])
|> line(end = [-24, 0])
|> close()
|> extrude(length = 6)
// Remove the end face for the extrusion.
shell(firstSketch, faces = [END], thickness = 0.25)"#;
let ExecTestResults {
program, mut exec_ctxt, ..
} = parse_execute(new).await.unwrap();
// Change the settings.
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &Default::default(),
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(true));
// Change the settings back.
let old_settings = exec_ctxt.settings.clone();
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &old_settings,
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(true));
// Change the settings back.
let old_settings = exec_ctxt.settings.clone();
exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &old_settings,
},
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(result, CacheResult::NoAction(true));
}
// Changing the units settings using an annotation with the exact same file
// should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
let old_code = r#"@settings(defaultLengthUnit = in)
startSketchOn(XY)
"#;
let new_code = r#"@settings(defaultLengthUnit = mm)
startSketchOn(XY)
"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
new_program.compute_digest();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &new_program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(
result,
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
}
);
}
// Removing the units settings using an annotation, when it was non-default
// units, with the exact same file should bust the cache.
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
let old_code = r#"@settings(defaultLengthUnit = in)
startSketchOn(XY)
"#;
let new_code = r#"
startSketchOn(XY)
"#;
let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
new_program.compute_digest();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &new_program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
assert_eq!(
result,
CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new_program.ast,
}
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multi_file_no_changes_does_not_reexecute() {
let code = r#"import "toBeImported.kcl" as importedCube
importedCube
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [-134.53, -56.17])
|> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(profile001, length = 100)
sketch003 = startSketchOn(extrude001, face = seg02)
sketch002 = startSketchOn(extrude001, face = seg01)
"#;
let other_file = (
std::path::PathBuf::from("toBeImported.kcl"),
r#"sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [281.54, 305.81])
|> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude(profile001, length = 100)"#
.to_string(),
);
let tmp_dir = std::env::temp_dir();
let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
// Create a temporary file for each of the other files.
let tmp_file = tmp_dir.join(other_file.0);
std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
std::fs::write(tmp_file, other_file.1).unwrap();
let ExecTestResults { program, exec_ctxt, .. } =
parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
.await
.unwrap();
let mut new_program = crate::Program::parse_no_errs(code).unwrap();
new_program.compute_digest();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &new_program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
panic!("Expected CheckImportsOnly, got {:?}", result);
};
assert_eq!(reapply_settings, false);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
let code = r#"import "toBeImported.kcl" as importedCube
importedCube
sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [-134.53, -56.17])
|> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
|> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
|> close()
extrude001 = extrude(profile001, length = 100)
sketch003 = startSketchOn(extrude001, face = seg02)
sketch002 = startSketchOn(extrude001, face = seg01)
"#;
let other_file = (
std::path::PathBuf::from("toBeImported.kcl"),
r#"sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [281.54, 305.81])
|> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude(profile001, length = 100)"#
.to_string(),
);
let other_file2 = (
std::path::PathBuf::from("toBeImported.kcl"),
r#"sketch001 = startSketchOn(XZ)
profile001 = startProfile(sketch001, at = [281.54, 305.81])
|> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
|> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
|> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
extrude(profile001, length = 100)
|> translate(z=100)
"#
.to_string(),
);
let tmp_dir = std::env::temp_dir();
let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
// Create a temporary file for each of the other files.
let tmp_file = tmp_dir.join(other_file.0);
std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
std::fs::write(&tmp_file, other_file.1).unwrap();
let ExecTestResults { program, exec_ctxt, .. } =
parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
.await
.unwrap();
// Change the other file.
std::fs::write(tmp_file, other_file2.1).unwrap();
let mut new_program = crate::Program::parse_no_errs(code).unwrap();
new_program.compute_digest();
let result = get_changed_program(
CacheInformation {
ast: &program.ast,
settings: &exec_ctxt.settings,
},
CacheInformation {
ast: &new_program.ast,
settings: &exec_ctxt.settings,
},
)
.await;
let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
panic!("Expected CheckImportsOnly, got {:?}", result);
};
assert_eq!(reapply_settings, false);
}
}