1use std::path::PathBuf;
4
5use kittycad_modeling_cmds::websocket::RawFile;
6
7use crate::ConnectionError;
8use crate::ExecError;
9use crate::KclError;
10use crate::KclErrorWithOutputs;
11use crate::Program;
12use crate::engine::new_zoo_client;
13use crate::errors::ExecErrorWithState;
14use crate::execution::EnvironmentRef;
15use crate::execution::ExecState;
16use crate::execution::ExecutorContext;
17use crate::execution::ExecutorSettings;
18
19#[derive(serde::Deserialize, serde::Serialize)]
20pub struct RequestBody {
21 pub kcl_program: String,
22 #[serde(default)]
23 pub test_name: String,
24}
25
26pub async fn execute_and_snapshot(code: &str, current_file: Option<PathBuf>) -> Result<image::DynamicImage, ExecError> {
29 let ctx = new_context(true, current_file).await?;
30 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
31 let res = do_execute_and_snapshot(&ctx, program)
32 .await
33 .map(|(_, _, snap)| snap)
34 .map_err(|err| err.error);
35 ctx.close().await;
36 res
37}
38
39pub async fn execute(code: &str, current_file: Option<PathBuf>) -> Result<(), ExecError> {
41 let ctx = new_context(true, current_file).await?;
42 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
43 let res = do_execute(&ctx, program).await.map(|_| ()).map_err(|err| err.error);
44 ctx.close().await;
45 res
46}
47
48pub struct Snapshot3d {
49 pub image: image::DynamicImage,
51 pub gltf: Vec<RawFile>,
53}
54
55pub async fn execute_and_snapshot_3d(code: &str, current_file: Option<PathBuf>) -> Result<Snapshot3d, ExecError> {
57 let ctx = new_context(true, current_file).await?;
58 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
59 let image = do_execute_and_snapshot(&ctx, program)
60 .await
61 .map(|(_, _, snap)| snap)
62 .map_err(|err| err.error)?;
63 let gltf_res = ctx
64 .export(kittycad_modeling_cmds::format::OutputFormat3d::Gltf(Default::default()))
65 .await;
66 let gltf = match gltf_res {
67 Err(err) if err.message() == "Nothing to export" => Vec::new(),
68 Err(err) => {
69 eprintln!("Error exporting: {}", err.message());
70 Vec::new()
71 }
72 Ok(x) => x,
73 };
74 ctx.close().await;
75 Ok(Snapshot3d { image, gltf })
76}
77#[cfg(test)]
80pub async fn execute_and_snapshot_ast(
81 ast: Program,
82 current_file: Option<PathBuf>,
83 with_export_step: bool,
84) -> Result<
85 (
86 ExecState,
87 ExecutorContext,
88 EnvironmentRef,
89 image::DynamicImage,
90 Option<Vec<u8>>,
91 ),
92 ExecErrorWithState,
93> {
94 let ctx = new_context(true, current_file).await?;
95 let (exec_state, env, img) = match do_execute_and_snapshot(&ctx, ast).await {
96 Ok((exec_state, env_ref, img)) => (exec_state, env_ref, img),
97 Err(err) => {
98 ctx.close().await;
101 return Err(err);
102 }
103 };
104 let mut step = None;
105 if with_export_step {
106 let files = match ctx.export_step(true).await {
107 Ok(f) => f,
108 Err(err) => {
109 ctx.close().await;
111 return Err(ExecErrorWithState::new(
112 ExecError::BadExport(format!("Export failed: {err:?}")),
113 exec_state.clone(),
114 None,
115 ));
116 }
117 };
118
119 step = files.into_iter().next().map(|f| f.contents);
120 }
121 ctx.close().await;
122 Ok((exec_state, ctx, env, img, step))
123}
124
125pub async fn execute_and_snapshot_no_auth(
126 code: &str,
127 current_file: Option<PathBuf>,
128) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
129 let ctx = new_context(false, current_file).await?;
130 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
131 let res = do_execute_and_snapshot(&ctx, program)
132 .await
133 .map(|(_, env_ref, snap)| (snap, env_ref))
134 .map_err(|err| err.error);
135 ctx.close().await;
136 res
137}
138
139async fn do_execute(
140 ctx: &ExecutorContext,
141 program: Program,
142) -> Result<(ExecState, EnvironmentRef), ExecErrorWithState> {
143 let mut exec_state = ExecState::new(ctx);
144 let result = ctx.run(&program, &mut exec_state).await;
145 let responses = if result.is_err() {
146 #[cfg(feature = "snapshot-engine-responses")]
147 {
148 Some(exec_state.take_root_module_responses())
149 }
150 #[cfg(not(feature = "snapshot-engine-responses"))]
151 None
152 } else {
153 None
154 };
155 let result = result.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), responses))?;
156 for issue in exec_state.issues() {
157 if issue.severity.is_err() {
158 return Err(ExecErrorWithState::new(
159 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
160 exec_state.clone(),
161 None,
162 ));
163 }
164 }
165
166 Ok((exec_state, result.0))
167}
168
169async fn do_execute_and_snapshot(
170 ctx: &ExecutorContext,
171 program: Program,
172) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
173 let (exec_state, env_ref) = do_execute(ctx, program).await?;
174 let snapshot_png_bytes = ctx
175 .prepare_snapshot()
176 .await
177 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?
178 .contents
179 .0;
180
181 let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
183 .with_guessed_format()
184 .map_err(|e| ExecError::BadPng(e.to_string()))
185 .and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
186 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?;
187
188 Ok((exec_state, env_ref, img))
189}
190
191pub async fn new_context(with_auth: bool, current_file: Option<PathBuf>) -> Result<ExecutorContext, ConnectionError> {
192 let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
193 .map_err(ConnectionError::CouldNotMakeClient)?;
194 if !with_auth {
195 client.set_base_url("https://api.zoo.dev".to_string());
199 }
200
201 let mut settings = ExecutorSettings {
202 highlight_edges: true,
203 enable_ssao: false,
204 show_grid: false,
205 replay: None,
206 project_directory: None,
207 current_file: None,
208 fixed_size_grid: true,
209 skip_artifact_graph: false,
210 heartbeats: None,
211 };
212 if let Some(current_file) = current_file {
213 settings.with_current_file(crate::TypedPath(current_file));
214 }
215 let ctx = ExecutorContext::new(&client, settings)
216 .await
217 .map_err(ConnectionError::Establishing)?;
218 Ok(ctx)
219}
220
221pub async fn execute_and_export_step(
222 code: &str,
223 current_file: Option<PathBuf>,
224) -> Result<
225 (
226 ExecState,
227 EnvironmentRef,
228 Vec<kittycad_modeling_cmds::websocket::RawFile>,
229 ),
230 ExecErrorWithState,
231> {
232 let ctx = new_context(true, current_file).await?;
233 let mut exec_state = ExecState::new(&ctx);
234 let program = Program::parse_no_errs(code).map_err(|err| {
235 ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone(), None)
236 })?;
237 let result = ctx
238 .run(&program, &mut exec_state)
239 .await
240 .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), None))?;
241 for issue in exec_state.issues() {
242 if issue.severity.is_err() {
243 return Err(ExecErrorWithState::new(
244 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
245 exec_state.clone(),
246 None,
247 ));
248 }
249 }
250
251 let files = match ctx.export_step(true).await {
252 Ok(f) => f,
253 Err(err) => {
254 return Err(ExecErrorWithState::new(
255 ExecError::BadExport(format!("Export failed: {err:?}")),
256 exec_state.clone(),
257 None,
258 ));
259 }
260 };
261
262 ctx.close().await;
263
264 Ok((exec_state, result.0, files))
265}