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_engine_graphics(true, current_file).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(
50 ctx: &ExecutorContext,
51 program: Program,
52 deprecation_version_override: Option<&str>,
53) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
54 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
55 let snapshot_png_bytes = ctx
56 .prepare_snapshot()
57 .await
58 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?
59 .contents
60 .0;
61
62 let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
64 .with_guessed_format()
65 .map_err(|e| ExecError::BadPng(e.to_string()))
66 .and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
67 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?;
68
69 Ok((exec_state, env_ref, img))
70}
71
72#[cfg(test)]
75pub async fn execute_export_and_render_locally(
76 ctx: &ExecutorContext,
77 program: Program,
78 deprecation_version_override: Option<&str>,
79) -> Result<(ExecState, EnvironmentRef, Snapshot3d), ExecErrorWithState> {
80 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
81
82 let glb_blob_files = match ctx
84 .export(kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
85 kittycad_modeling_cmds::format::gltf::export::Options::builder()
86 .storage(kittycad_modeling_cmds::format::gltf::export::Storage::Binary)
87 .build(),
88 ))
89 .await
90 {
91 Ok(f) => f,
92 Err(err) => {
93 ctx.close().await;
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 = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
194
195 let result = match graphics {
196 TestGraphicsParams::EngineRender => execute_locally_and_render_on_engine(&ctx, program, None)
197 .await
198 .map(|(_, _, image)| TestGraphicsArtifact::Image(image))
199 .map_err(|err| err.error)?,
200 TestGraphicsParams::ExportAndRender => execute_export_and_render_locally(&ctx, program, None)
201 .await
202 .map(|(_, _, snap_3d)| TestGraphicsArtifact::ImageAndGlb {
203 image: snap_3d.image,
204 glb: snap_3d.glb,
205 })
206 .map_err(|err| err.error)?,
207 TestGraphicsParams::None => {
208 _ = do_execute(&ctx, program, None).await.map_err(|err| err.error)?;
209 TestGraphicsArtifact::None
210 }
211 };
212 ctx.close().await;
213 Ok(result)
214}
215
216pub async fn execute_and_snapshot_legacy_sim_test(
219 code: &str,
220 current_file: Option<PathBuf>,
221) -> Result<image::DynamicImage, ExecError> {
222 let ctx = new_context_engine_graphics(true, current_file).await?;
223 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
224 let res = execute_locally_and_render_on_engine(&ctx, program, None)
225 .await
226 .map(|(_, _, img)| img)
227 .map_err(|err| err.error);
228 ctx.close().await;
229 res
230}
231
232#[cfg(test)]
236pub async fn execute_and_snapshot_ast_no_close(
237 ast: Program,
238 current_file: Option<PathBuf>,
239 deprecation_version_override: Option<&str>,
240) -> Result<(ExecState, ExecutorContext, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
241 execute_and_snapshot_ast_with_heartbeats(ast, current_file, deprecation_version_override, Some(5)).await
242}
243
244#[cfg(test)]
245async fn execute_and_snapshot_ast_with_heartbeats(
246 ast: Program,
247 current_file: Option<PathBuf>,
248 deprecation_version_override: Option<&str>,
249 heartbeats: Option<u64>,
250) -> Result<(ExecState, ExecutorContext, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
251 let ctx = new_context_with_heartbeats(true, current_file, heartbeats, false).await?;
252 let (exec_state, env, image) =
253 match execute_locally_and_render_on_engine(&ctx, ast, deprecation_version_override).await {
254 Ok((exec_state, env_ref, image)) => (exec_state, env_ref, image),
255 Err(err) => {
256 ctx.close().await;
259 return Err(err);
260 }
261 };
262 Ok((exec_state, ctx, env, image))
263}
264
265pub async fn execute_and_snapshot_no_auth(
266 code: &str,
267 current_file: Option<PathBuf>,
268) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
269 let ctx = new_context_engine_graphics(false, current_file).await?;
270 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
271 let res = execute_locally_and_render_on_engine(&ctx, program, None)
272 .await
273 .map(|(_, env_ref, image)| (image, env_ref))
274 .map_err(|err| err.error);
275 ctx.close().await;
276 res
277}
278
279async fn do_execute(
280 ctx: &ExecutorContext,
281 program: Program,
282 _deprecation_version_override: Option<&str>,
283) -> Result<(ExecState, EnvironmentRef), ExecErrorWithState> {
284 let mut exec_state = ExecState::new(ctx);
285 #[cfg(test)]
286 exec_state.set_deprecation_version_override(_deprecation_version_override);
287 let _ = ctx.send_clear_scene(&mut exec_state, Default::default()).await;
288 let result = ctx.run(&program, &mut exec_state).await;
289 let responses = if result.is_err() {
290 #[cfg(feature = "snapshot-engine-responses")]
291 {
292 Some(exec_state.take_root_module_responses())
293 }
294 #[cfg(not(feature = "snapshot-engine-responses"))]
295 None
296 } else {
297 None
298 };
299 let result = result.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), responses))?;
300 for issue in exec_state.issues() {
301 if issue.severity.is_err() {
302 return Err(ExecErrorWithState::new(
303 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
304 exec_state.clone(),
305 None,
306 ));
307 }
308 }
309
310 Ok((exec_state, result.0))
311}
312
313pub async fn new_context_engine_graphics(
314 with_auth: bool,
315 current_file: Option<PathBuf>,
316) -> Result<ExecutorContext, ConnectionError> {
317 new_context_with_heartbeats(with_auth, current_file, None, false).await
318}
319
320pub async fn new_context(
321 with_auth: bool,
322 current_file: Option<PathBuf>,
323 geometry_only: bool,
324) -> Result<ExecutorContext, ConnectionError> {
325 new_context_with_heartbeats(with_auth, current_file, None, geometry_only).await
326}
327
328async fn new_context_with_heartbeats(
329 with_auth: bool,
330 current_file: Option<PathBuf>,
331 heartbeats: Option<u64>,
332 geometry_only: bool,
333) -> Result<ExecutorContext, ConnectionError> {
334 let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
335 .map_err(ConnectionError::CouldNotMakeClient)?;
336 if !with_auth {
337 client.set_base_url("https://api.zoo.dev".to_string());
341 }
342
343 let mut settings = ExecutorSettings {
344 highlight_edges: true,
345 enable_ssao: false,
346 show_grid: false,
347 replay: None,
348 project_directory: None,
349 current_file: None,
350 fixed_size_grid: true,
351 skip_artifact_graph: false,
352 heartbeats,
353 default_backface_color: Some("#00D5FF".to_owned()),
354 geometry_only,
355 };
356 if let Some(current_file) = current_file {
357 settings.with_current_file(crate::TypedPath(current_file));
358 }
359 let ctx = ExecutorContext::new(&client, settings)
360 .await
361 .map_err(ConnectionError::Establishing)?;
362 Ok(ctx)
363}
364
365pub async fn execute_and_export_step(
366 code: &str,
367 current_file: Option<PathBuf>,
368) -> Result<
369 (
370 ExecState,
371 EnvironmentRef,
372 Vec<kittycad_modeling_cmds::websocket::RawFile>,
373 ),
374 ExecErrorWithState,
375> {
376 let ctx = new_context_engine_graphics(true, current_file).await?;
377 let mut exec_state = ExecState::new(&ctx);
378 let program = Program::parse_no_errs(code).map_err(|err| {
379 ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone(), None)
380 })?;
381 let result = ctx
382 .run(&program, &mut exec_state)
383 .await
384 .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), None))?;
385 for issue in exec_state.issues() {
386 if issue.severity.is_err() {
387 return Err(ExecErrorWithState::new(
388 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
389 exec_state.clone(),
390 None,
391 ));
392 }
393 }
394
395 let files = match ctx.export_step(true).await {
396 Ok(f) => f,
397 Err(err) => {
398 return Err(ExecErrorWithState::new(
399 ExecError::BadExport(format!("Export failed: {err:?}")),
400 exec_state.clone(),
401 None,
402 ));
403 }
404 };
405
406 ctx.close().await;
407
408 Ok((exec_state, result.0, files))
409}