use std::sync::Arc;
use indexmap::IndexMap;
use itertools::EitherOrBoth;
use itertools::Itertools;
use tokio::sync::RwLock;
use crate::ExecOutcome;
use crate::ExecutorContext;
use crate::execution::EnvironmentRef;
use crate::execution::ExecutorSettings;
use crate::execution::annotations;
use crate::execution::memory::Stack;
use crate::execution::state::ModuleInfoMap;
use crate::execution::state::{self as exec_state};
use crate::front::Object;
use crate::modules::ModuleId;
use crate::modules::ModulePath;
use crate::modules::ModuleSource;
use crate::parsing::ast::types::Annotation;
use crate::parsing::ast::types::Node;
use crate::parsing::ast::types::Program;
use crate::walk::Node as WalkNode;
lazy_static::lazy_static! {
static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
}
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<SketchModeState> {
let old_mem = PREV_MEMORY.read().await;
old_mem.clone()
}
pub(crate) async fn write_old_memory(mem: SketchModeState) {
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;
}
#[derive(Debug, Clone)]
pub struct CacheInformation<'a> {
pub ast: &'a Node<Program>,
pub settings: &'a ExecutorSettings,
}
#[derive(Debug, Clone)]
pub(super) struct GlobalState {
pub(super) main: ModuleState,
pub(super) exec_state: exec_state::GlobalState,
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 {
ExecOutcome {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.root_module_artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
#[cfg(feature = "artifact-graph")]
scene_objects: self.exec_state.root_module_artifacts.scene_objects,
#[cfg(feature = "artifact-graph")]
source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
#[cfg(feature = "artifact-graph")]
var_solutions: self.exec_state.root_module_artifacts.var_solutions,
issues: self.exec_state.issues,
default_planes: ctx.engine.get_default_planes().read().await.clone(),
}
}
}
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
pub(super) ast: Node<Program>,
pub(super) exec_state: exec_state::ModuleState,
pub(super) result_env: EnvironmentRef,
}
#[derive(Debug, Clone)]
pub(crate) struct SketchModeState {
pub stack: Stack,
pub module_infos: ModuleInfoMap,
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
pub id_to_source: IndexMap<ModuleId, ModuleSource>,
#[cfg_attr(not(feature = "artifact-graph"), expect(dead_code))]
pub scene_objects: Vec<Object>,
}
#[cfg(test)]
impl SketchModeState {
pub(crate) fn new_for_tests() -> Self {
Self {
stack: Stack::new_for_tests(),
module_infos: ModuleInfoMap::default(),
path_to_source_id: Default::default(),
id_to_source: Default::default(),
scene_objects: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub(super) enum CacheResult {
ReExecute {
clear_scene: bool,
reapply_settings: bool,
program: Node<Program>,
},
CheckImportsOnly {
reapply_settings: bool,
ast: Node<Program>,
},
NoAction(bool),
}
pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
let mut reapply_settings = false;
if old.settings != new.settings {
reapply_settings = true;
}
if old.ast == new.ast {
if !old.ast.has_import_statements() {
return CacheResult::NoAction(reapply_settings);
}
return CacheResult::CheckImportsOnly {
reapply_settings,
ast: old.ast.clone(),
};
}
let mut old_ast = old.ast.clone();
let mut new_ast = new.ast.clone();
old_ast.compute_digest();
new_ast.compute_digest();
if old_ast.digest == new_ast.digest {
if !old.ast.has_import_statements() {
return CacheResult::NoAction(reapply_settings);
}
return CacheResult::CheckImportsOnly {
reapply_settings,
ast: old.ast.clone(),
};
}
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) => {
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,
}
})
{
return CacheResult::ReExecute {
clear_scene: true,
reapply_settings: true,
program: new.ast.clone(),
};
}
generate_changed_program(old_ast, new_ast, reapply_settings)
}
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()
}) {
return CacheResult::ReExecute {
clear_scene: true,
reapply_settings,
program: new_ast,
};
}
match new_ast.body.len().cmp(&old_ast.body.len()) {
std::cmp::Ordering::Less => {
CacheResult::ReExecute {
clear_scene: true,
reapply_settings,
program: new_ast,
}
}
std::cmp::Ordering::Greater => {
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 => {
CacheResult::NoAction(reapply_settings)
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::execution::ExecTestResults;
use crate::execution::parse_execute;
use crate::execution::parse_execute_with_project_dir;
#[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));
exec_ctxt.close().await;
}
#[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));
exec_ctxt.close().await;
}
#[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));
exec_ctxt.close().await;
}
#[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));
exec_ctxt.close().await;
}
#[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();
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));
exec_ctxt.close().await;
}
#[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();
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));
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));
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));
exec_ctxt.close().await;
}
#[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,
}
);
exec_ctxt.close().await;
}
#[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,
}
);
exec_ctxt.close().await;
}
#[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());
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);
exec_ctxt.close().await;
}
#[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());
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();
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);
exec_ctxt.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_added_outer_attribute() {
let old_code = r#"import "tests/inputs/cube.step"
"#;
let new_code = r#"@(coords = opengl)
import "tests/inputs/cube.step"
"#;
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: false,
program: new_program.ast,
}
);
exec_ctxt.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_changed_program_different_outer_attribute() {
let old_code = r#"@(coords = vulkan)
import "tests/inputs/cube.step"
"#;
let new_code = r#"@(coords = opengl)
import "tests/inputs/cube.step"
"#;
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: false,
program: new_program.ast,
}
);
exec_ctxt.close().await;
}
}