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(code: &str, current_file: Option<PathBuf>) -> Result<(), ExecError> {
28 let ctx = new_context(true, current_file, true).await?;
29 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
30 let res = do_execute(&ctx, program, None)
31 .await
32 .map(|_| ())
33 .map_err(|err| err.error);
34 ctx.close().await;
35 res
36}
37
38#[cfg(test)]
39pub struct Snapshot3d {
40 pub image: image::DynamicImage,
42 pub glb: Glb,
44}
45
46pub async fn execute_locally_and_render_on_engine(
51 ctx: &ExecutorContext,
52 program: Program,
53 deprecation_version_override: Option<&str>,
54) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
55 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
56 let snapshot_png_bytes = ctx
57 .prepare_snapshot()
58 .await
59 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?
60 .contents
61 .0;
62
63 let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
65 .with_guessed_format()
66 .map_err(|e| ExecError::BadPng(e.to_string()))
67 .and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
68 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?;
69
70 Ok((exec_state, env_ref, img))
71}
72
73#[cfg(test)]
77pub async fn execute_export_and_render_locally(
78 ctx: &ExecutorContext,
79 program: Program,
80 deprecation_version_override: Option<&str>,
81) -> Result<(ExecState, EnvironmentRef, Snapshot3d), ExecErrorWithState> {
82 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
83
84 let glb_blob_files = match ctx
86 .export(kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
87 kittycad_modeling_cmds::format::gltf::export::Options::builder()
88 .storage(kittycad_modeling_cmds::format::gltf::export::Storage::Binary)
89 .build(),
90 ))
91 .await
92 {
93 Ok(f) => f,
94 Err(err) => {
95 return Err(ExecErrorWithState::new(
96 ExecError::BadExport(format!("Export failed: {err:?}")),
97 exec_state.clone(),
98 None,
99 ));
100 }
101 };
102 if glb_blob_files.len() != 1 {
103 return Err(ExecErrorWithState::new(
104 ExecError::BadExport(format!("Expected 1 glb file, found {}", glb_blob_files.len())),
105 exec_state,
106 None,
107 ));
108 }
109 let glb: Glb = glb_blob_files
110 .into_iter()
111 .next()
112 .unwrap_or_else(|| RawFile {
113 name: String::new(),
114 contents: vec![],
115 })
116 .into();
117 let image = glb_render::render(&glb.bytes)
118 .map_err(|e| ExecErrorWithState::new(ExecError::BadExport(e), exec_state.clone(), None))?;
119
120 let snap_3d = Snapshot3d { image, glb };
121 Ok((exec_state, env_ref, snap_3d))
122}
123
124pub struct Glb {
126 pub name: String,
127 pub bytes: Vec<u8>,
128}
129
130impl From<RawFile> for Glb {
131 fn from(value: RawFile) -> Self {
132 Glb {
133 name: value.name,
134 bytes: value.contents,
135 }
136 }
137}
138
139#[cfg(test)]
140pub enum TestGraphicsArtifact {
141 Image(image::DynamicImage),
142 ImageAndGlb { image: image::DynamicImage, glb: Glb },
143 None,
144}
145
146#[cfg(test)]
147impl TestGraphicsArtifact {
148 pub fn image(self) -> Option<image::DynamicImage> {
149 match self {
150 Self::Image(img) => Some(img),
151 Self::ImageAndGlb { image, .. } => Some(image),
152 Self::None => None,
153 }
154 }
155}
156
157#[cfg(test)]
158enum TestGraphicsParams {
159 EngineRender,
161 ExportAndRender,
163 None,
165}
166
167#[cfg(test)]
168impl TestGraphicsParams {
169 fn geometry_only(&self) -> bool {
170 matches!(self, Self::ExportAndRender | Self::None)
171 }
172 fn from_kcl_sample_spec(no_3d: bool, no_run: bool) -> Self {
176 match (no_3d, no_run) {
177 (true, false) => Self::EngineRender,
178 (false, false) => Self::ExportAndRender,
179 (true, true) | (false, true) => Self::None,
180 }
181 }
182}
183
184#[cfg(test)]
185pub async fn kcl_doc_execute_and_snapshot(
186 code: &str,
187 current_file: Option<PathBuf>,
188 no_3d: bool,
189 no_run: bool,
190) -> Result<TestGraphicsArtifact, ExecError> {
191 let graphics = TestGraphicsParams::from_kcl_sample_spec(no_3d, no_run);
192 let ctx = new_context(true, current_file, graphics.geometry_only()).await?;
193 let program = match Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs) {
194 Ok(program) => program,
195 Err(e) => {
196 ctx.close().await;
197 return Err(e.into());
198 }
199 };
200
201 let result: Result<TestGraphicsArtifact, ExecError> = match graphics {
202 TestGraphicsParams::EngineRender => execute_locally_and_render_on_engine(&ctx, program, None)
203 .await
204 .map(|(_, _, image)| TestGraphicsArtifact::Image(image))
205 .map_err(|err| err.error),
206 TestGraphicsParams::ExportAndRender => execute_export_and_render_locally(&ctx, program, None)
207 .await
208 .map(|(_, _, snap_3d)| TestGraphicsArtifact::ImageAndGlb {
209 image: snap_3d.image,
210 glb: snap_3d.glb,
211 })
212 .map_err(|err| err.error),
213 TestGraphicsParams::None => do_execute(&ctx, program, None)
214 .await
215 .map_err(|err| err.error)
216 .map(|_| TestGraphicsArtifact::None),
217 };
218 ctx.close().await;
219 result
220}
221
222pub async fn execute_and_snapshot_legacy_sim_test(
225 code: &str,
226 current_file: Option<PathBuf>,
227) -> Result<image::DynamicImage, ExecError> {
228 let ctx = new_context_engine_graphics(true, current_file).await?;
229 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
230 let res = execute_locally_and_render_on_engine(&ctx, program, None)
231 .await
232 .map(|(_, _, img)| img)
233 .map_err(|err| err.error);
234 ctx.close().await;
235 res
236}
237
238#[cfg(test)]
242pub async fn execute_and_snapshot_ast_no_close(
243 ast: Program,
244 current_file: Option<PathBuf>,
245 deprecation_version_override: Option<&str>,
246) -> Result<(ExecState, ExecutorContext, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
247 execute_and_snapshot_ast_with_heartbeats(ast, current_file, deprecation_version_override, Some(5)).await
248}
249
250#[cfg(test)]
251async fn execute_and_snapshot_ast_with_heartbeats(
252 ast: Program,
253 current_file: Option<PathBuf>,
254 deprecation_version_override: Option<&str>,
255 heartbeats: Option<u64>,
256) -> Result<(ExecState, ExecutorContext, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
257 let ctx = new_context_with_heartbeats(true, current_file, heartbeats, false).await?;
258 let (exec_state, env, image) =
259 match execute_locally_and_render_on_engine(&ctx, ast, deprecation_version_override).await {
260 Ok((exec_state, env_ref, image)) => (exec_state, env_ref, image),
261 Err(err) => {
262 ctx.close().await;
265 return Err(err);
266 }
267 };
268 Ok((exec_state, ctx, env, image))
269}
270
271pub async fn execute_and_snapshot_no_auth(
272 code: &str,
273 current_file: Option<PathBuf>,
274) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
275 let ctx = new_context_engine_graphics(false, current_file).await?;
276 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
277 let res = execute_locally_and_render_on_engine(&ctx, program, None)
278 .await
279 .map(|(_, env_ref, image)| (image, env_ref))
280 .map_err(|err| err.error);
281 ctx.close().await;
282 res
283}
284
285async fn do_execute(
286 ctx: &ExecutorContext,
287 program: Program,
288 _deprecation_version_override: Option<&str>,
289) -> Result<(ExecState, EnvironmentRef), ExecErrorWithState> {
290 let mut exec_state = ExecState::new(ctx);
291 #[cfg(test)]
292 exec_state.set_deprecation_version_override(_deprecation_version_override);
293 let _ = ctx.send_clear_scene(&mut exec_state, Default::default()).await;
294 let result = ctx.run(&program, &mut exec_state).await;
295 let responses = if result.is_err() {
296 #[cfg(feature = "snapshot-engine-responses")]
297 {
298 Some(exec_state.take_root_module_responses())
299 }
300 #[cfg(not(feature = "snapshot-engine-responses"))]
301 None
302 } else {
303 None
304 };
305 let result = result.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), responses))?;
306 for issue in exec_state.issues() {
307 if issue.severity.is_err() {
308 return Err(ExecErrorWithState::new(
309 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
310 exec_state.clone(),
311 None,
312 ));
313 }
314 }
315
316 Ok((exec_state, result.0))
317}
318
319pub async fn new_context_engine_graphics(
320 with_auth: bool,
321 current_file: Option<PathBuf>,
322) -> Result<ExecutorContext, ConnectionError> {
323 new_context_with_heartbeats(with_auth, current_file, None, false).await
324}
325
326pub async fn new_context(
327 with_auth: bool,
328 current_file: Option<PathBuf>,
329 geometry_only: bool,
330) -> Result<ExecutorContext, ConnectionError> {
331 new_context_with_heartbeats(with_auth, current_file, None, geometry_only).await
332}
333
334async fn new_context_with_heartbeats(
335 with_auth: bool,
336 current_file: Option<PathBuf>,
337 heartbeats: Option<u64>,
338 geometry_only: bool,
339) -> Result<ExecutorContext, ConnectionError> {
340 let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
341 .map_err(ConnectionError::CouldNotMakeClient)?;
342 if !with_auth {
343 client.set_base_url("https://api.zoo.dev".to_string());
347 }
348
349 let mut settings = ExecutorSettings {
350 highlight_edges: true,
351 enable_ssao: false,
352 show_grid: false,
353 replay: None,
354 project_directory: None,
355 current_file: None,
356 fixed_size_grid: true,
357 skip_artifact_graph: false,
358 heartbeats,
359 default_backface_color: Some("#00D5FF".to_owned()),
360 pool: None,
361 video_res_width: None,
362 video_res_height: None,
363 geometry_only,
364 };
365 if let Some(current_file) = current_file {
366 settings.with_current_file(crate::TypedPath(current_file));
367 }
368 let ctx = ExecutorContext::new(&client, settings)
369 .await
370 .map_err(ConnectionError::Establishing)?;
371 Ok(ctx)
372}
373
374pub async fn execute_and_export_step(
375 code: &str,
376 current_file: Option<PathBuf>,
377) -> Result<
378 (
379 ExecState,
380 EnvironmentRef,
381 Vec<kittycad_modeling_cmds::websocket::RawFile>,
382 ),
383 ExecErrorWithState,
384> {
385 let ctx = new_context(true, current_file, true).await?;
386 let mut exec_state = ExecState::new(&ctx);
387 let program = Program::parse_no_errs(code).map_err(|err| {
388 ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone(), None)
389 })?;
390 let result = ctx
391 .run(&program, &mut exec_state)
392 .await
393 .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), None))?;
394 for issue in exec_state.issues() {
395 if issue.severity.is_err() {
396 return Err(ExecErrorWithState::new(
397 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
398 exec_state.clone(),
399 None,
400 ));
401 }
402 }
403
404 let files = match ctx.export_step(true).await {
405 Ok(f) => f,
406 Err(err) => {
407 return Err(ExecErrorWithState::new(
408 ExecError::BadExport(format!("Export failed: {err:?}")),
409 exec_state.clone(),
410 None,
411 ));
412 }
413 };
414
415 ctx.close().await;
416
417 Ok((exec_state, result.0, files))
418}