1use std::path::PathBuf;
4
5use kittycad_modeling_cmds::websocket::RawFile;
6#[cfg(test)]
7use serde::Deserialize;
8
9use crate::ConnectionError;
10use crate::ExecError;
11use crate::KclError;
12use crate::KclErrorWithOutputs;
13use crate::Program;
14use crate::engine::new_zoo_client;
15use crate::errors::ExecErrorWithState;
16use crate::execution::EnvironmentRef;
17use crate::execution::ExecState;
18use crate::execution::ExecutorContext;
19use crate::execution::ExecutorSettings;
20
21#[derive(serde::Deserialize, serde::Serialize)]
22pub struct RequestBody {
23 pub kcl_program: String,
24 #[serde(default)]
25 pub test_name: String,
26}
27
28pub async fn execute(code: &str, current_file: Option<PathBuf>) -> Result<(), ExecError> {
30 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
31 let version = program.language_version().map_err(KclErrorWithOutputs::no_outputs)?;
32 let ctx = new_context(true, current_file, true, version).await?;
33 let res = do_execute(&ctx, program, None)
34 .await
35 .map(|_| ())
36 .map_err(|err| err.error);
37 ctx.close().await;
38 res
39}
40
41#[cfg(test)]
42pub struct Snapshot3d {
43 pub image: image::DynamicImage,
45 pub glb: Glb,
47}
48
49async fn execute_locally_and_render_on_engine(
54 ctx: &ExecutorContext,
55 program: Program,
56 deprecation_version_override: Option<&str>,
57) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
58 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
59 let snapshot_png_bytes = ctx
60 .prepare_snapshot()
61 .await
62 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?
63 .contents
64 .0;
65
66 let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
68 .with_guessed_format()
69 .map_err(|e| ExecError::BadPng(e.to_string()))
70 .and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
71 .map_err(|err| ExecErrorWithState::new(err, exec_state.clone(), None))?;
72
73 Ok((exec_state, env_ref, img))
74}
75
76#[cfg(test)]
80async fn execute_export_and_render_locally(
81 ctx: &ExecutorContext,
82 program: Program,
83 deprecation_version_override: Option<&str>,
84) -> Result<(ExecState, EnvironmentRef, Snapshot3d), ExecErrorWithState> {
85 let (exec_state, env_ref) = do_execute(ctx, program, deprecation_version_override).await?;
86
87 let glb_blob_files = match ctx
89 .export(kittycad_modeling_cmds::format::OutputFormat3d::Gltf(
90 kittycad_modeling_cmds::format::gltf::export::Options::builder()
91 .storage(kittycad_modeling_cmds::format::gltf::export::Storage::Binary)
92 .build(),
93 ))
94 .await
95 {
96 Ok(f) => f,
97 Err(err) => {
98 return Err(ExecErrorWithState::new(
99 ExecError::BadExport(format!("Export failed: {err:?}")),
100 exec_state.clone(),
101 None,
102 ));
103 }
104 };
105 if glb_blob_files.len() != 1 {
106 return Err(ExecErrorWithState::new(
107 ExecError::BadExport(format!("Expected 1 glb file, found {}", glb_blob_files.len())),
108 exec_state,
109 None,
110 ));
111 }
112 let glb: Glb = glb_blob_files
113 .into_iter()
114 .next()
115 .unwrap_or_else(|| RawFile {
116 name: String::new(),
117 contents: vec![],
118 })
119 .into();
120 let image = glb_render::render(&glb.bytes)
121 .map_err(|e| ExecErrorWithState::new(ExecError::BadExport(e), exec_state.clone(), None))?;
122
123 let snap_3d = Snapshot3d { image, glb };
124 Ok((exec_state, env_ref, snap_3d))
125}
126
127pub struct Glb {
129 pub name: String,
130 pub bytes: Vec<u8>,
131}
132
133impl From<RawFile> for Glb {
134 fn from(value: RawFile) -> Self {
135 Glb {
136 name: value.name,
137 bytes: value.contents,
138 }
139 }
140}
141
142#[cfg(test)]
143pub enum TestGraphicsArtifact {
144 Image(image::DynamicImage),
145 ImageAndGlb { image: image::DynamicImage, glb: Glb },
146 None,
147}
148
149#[cfg(test)]
150impl TestGraphicsArtifact {
151 pub fn image(self) -> Option<image::DynamicImage> {
152 match self {
153 Self::Image(img) => Some(img),
154 Self::ImageAndGlb { image, .. } => Some(image),
155 Self::None => None,
156 }
157 }
158}
159
160#[cfg(test)]
161#[derive(Deserialize, Debug, Clone, Default)]
162pub enum TestGraphicsParams {
163 EngineRender { reason: String },
165 #[default]
167 ExportAndRender,
168 None,
170}
171
172#[cfg(test)]
173impl TestGraphicsParams {
174 fn geometry_only(&self) -> bool {
175 matches!(self, Self::ExportAndRender | Self::None)
176 }
177 fn from_kcl_sample_spec(no_3d: bool, no_run: bool) -> Self {
181 match (no_3d, no_run) {
182 (true, false) => Self::EngineRender {
183 reason: "KCL sample marked 'no3d'".to_string(),
186 },
187 (false, false) => Self::ExportAndRender,
188 (true, true) | (false, true) => Self::None,
189 }
190 }
191}
192
193#[cfg(test)]
194async fn execute_from_graphics_params(
195 graphics: TestGraphicsParams,
196 program: Program,
197 deprecation_version_override: Option<&str>,
198 ctx: &ExecutorContext,
199) -> Result<(ExecState, EnvironmentRef, TestGraphicsArtifact), ExecErrorWithState> {
200 match graphics {
201 TestGraphicsParams::EngineRender { reason: _r } => {
204 execute_locally_and_render_on_engine(ctx, program, deprecation_version_override)
205 .await
206 .map(|(state, env, image)| (state, env, TestGraphicsArtifact::Image(image)))
207 }
208 TestGraphicsParams::ExportAndRender => {
209 execute_export_and_render_locally(ctx, program, deprecation_version_override)
210 .await
211 .map(|(state, env, snap_3d)| {
212 (
213 state,
214 env,
215 TestGraphicsArtifact::ImageAndGlb {
216 image: snap_3d.image,
217 glb: snap_3d.glb,
218 },
219 )
220 })
221 }
222 TestGraphicsParams::None => do_execute(ctx, program, deprecation_version_override)
223 .await
224 .map(|(state, env)| (state, env, TestGraphicsArtifact::None)),
225 }
226}
227
228#[cfg(test)]
229pub async fn kcl_doc_execute_and_snapshot(
230 code: &str,
231 current_file: Option<PathBuf>,
232 no_3d: bool,
233 no_run: bool,
234) -> Result<TestGraphicsArtifact, ExecError> {
235 let graphics = TestGraphicsParams::from_kcl_sample_spec(no_3d, no_run);
236 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
237 let version = program.language_version().map_err(KclErrorWithOutputs::no_outputs)?;
238 let ctx = new_context(true, current_file, graphics.geometry_only(), version).await?;
239
240 let result: Result<TestGraphicsArtifact, ExecError> = execute_from_graphics_params(graphics, program, None, &ctx)
241 .await
242 .map(|(_exec_state, _env, graphics)| graphics)
243 .map_err(|e| e.error);
244
245 ctx.close().await;
246 result
247}
248
249#[cfg(test)]
253pub async fn execute_sim_test_no_close(
254 ast: Program,
255 current_file: Option<PathBuf>,
256 deprecation_version_override: Option<&str>,
257 graphics: TestGraphicsParams,
258) -> Result<(ExecState, ExecutorContext, EnvironmentRef, TestGraphicsArtifact), ExecErrorWithState> {
259 let heartbeats = Some(5);
260 let version = ast
261 .language_version()
262 .map_err(|error| {
263 let mut error = KclErrorWithOutputs::no_outputs(error);
264 if let Some(path) = ¤t_file {
265 error.filenames.insert(
266 crate::ModuleId::default(),
267 crate::modules::ModulePath::Local {
268 value: crate::TypedPath(path.clone()),
269 original_import_path: None,
270 },
271 );
272 }
273 error
274 })
275 .map_err(ExecError::from)?;
276 let ctx = new_context_with_heartbeats(true, current_file, heartbeats, graphics.geometry_only(), version).await?;
277 let result = execute_from_graphics_params(graphics, ast, deprecation_version_override, &ctx).await;
278 if result.is_err() {
281 ctx.close().await;
282 }
283 result.map(|(state, env, graphics_result)| (state, ctx, env, graphics_result))
284}
285
286pub async fn execute_and_snapshot_no_auth(
287 code: &str,
288 current_file: Option<PathBuf>,
289) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
290 let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
291 let version = program.language_version().map_err(KclErrorWithOutputs::no_outputs)?;
292 let ctx = new_context_engine_graphics(false, current_file, version).await?;
293 let res = execute_locally_and_render_on_engine(&ctx, program, None)
294 .await
295 .map(|(_, env_ref, image)| (image, env_ref))
296 .map_err(|err| err.error);
297 ctx.close().await;
298 res
299}
300
301async fn do_execute(
302 ctx: &ExecutorContext,
303 program: Program,
304 _deprecation_version_override: Option<&str>,
305) -> Result<(ExecState, EnvironmentRef), ExecErrorWithState> {
306 let mut exec_state = ExecState::new(ctx);
307 #[cfg(test)]
308 exec_state.set_deprecation_version_override(_deprecation_version_override);
309 let _ = ctx.send_clear_scene(&mut exec_state, Default::default()).await;
310 let result = ctx.run(&program, &mut exec_state).await;
311 let responses = if result.is_err() {
312 #[cfg(feature = "snapshot-engine-responses")]
313 {
314 Some(exec_state.take_root_module_responses())
315 }
316 #[cfg(not(feature = "snapshot-engine-responses"))]
317 None
318 } else {
319 None
320 };
321 let result = result.map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), responses))?;
322 for issue in exec_state.issues() {
323 if issue.severity.is_err() {
324 return Err(ExecErrorWithState::new(
325 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
326 exec_state.clone(),
327 None,
328 ));
329 }
330 }
331
332 Ok((exec_state, result.0))
333}
334
335pub async fn new_context_engine_graphics(
336 with_auth: bool,
337 current_file: Option<PathBuf>,
338 kcl_version: crate::KclVersion,
339) -> Result<ExecutorContext, ConnectionError> {
340 new_context_with_heartbeats(with_auth, current_file, None, false, kcl_version).await
341}
342
343pub async fn new_context(
344 with_auth: bool,
345 current_file: Option<PathBuf>,
346 geometry_only: bool,
347 kcl_version: crate::KclVersion,
348) -> Result<ExecutorContext, ConnectionError> {
349 new_context_with_heartbeats(with_auth, current_file, None, geometry_only, kcl_version).await
350}
351
352async fn new_context_with_heartbeats(
353 with_auth: bool,
354 current_file: Option<PathBuf>,
355 heartbeats: Option<u64>,
356 geometry_only: bool,
357 kcl_version: crate::KclVersion,
358) -> Result<ExecutorContext, ConnectionError> {
359 let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
360 .map_err(ConnectionError::CouldNotMakeClient)?;
361 if !with_auth {
362 client.set_base_url("https://api.zoo.dev".to_string());
366 }
367
368 let mut settings = ExecutorSettings {
369 highlight_edges: true,
370 enable_ssao: false,
371 show_grid: false,
372 replay: None,
373 project_directory: None,
374 current_file: None,
375 fixed_size_grid: true,
376 skip_artifact_graph: false,
377 heartbeats,
378 default_backface_color: Some("#00D5FF".to_owned()),
379 pool: None,
380 video_res_width: None,
381 video_res_height: None,
382 geometry_only,
383 };
384 if let Some(current_file) = current_file {
385 settings.with_current_file(crate::TypedPath(current_file));
386 }
387 let ctx = ExecutorContext::new(&client, settings, kcl_version)
388 .await
389 .map_err(ConnectionError::Establishing)?;
390 Ok(ctx)
391}
392
393pub async fn execute_and_export_step(
394 code: &str,
395 current_file: Option<PathBuf>,
396) -> Result<
397 (
398 ExecState,
399 EnvironmentRef,
400 Vec<kittycad_modeling_cmds::websocket::RawFile>,
401 ),
402 ExecErrorWithState,
403> {
404 let program = Program::parse_no_errs(code)
405 .map_err(KclErrorWithOutputs::no_outputs)
406 .map_err(ExecError::from)?;
407 let version = program
408 .language_version()
409 .map_err(KclErrorWithOutputs::no_outputs)
410 .map_err(ExecError::from)?;
411 let ctx = new_context(true, current_file, true, version).await?;
412 let mut exec_state = ExecState::new(&ctx);
413 let result = ctx
414 .run(&program, &mut exec_state)
415 .await
416 .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone(), None))?;
417 for issue in exec_state.issues() {
418 if issue.severity.is_err() {
419 return Err(ExecErrorWithState::new(
420 KclErrorWithOutputs::no_outputs(KclError::new_semantic(issue.clone().into())).into(),
421 exec_state.clone(),
422 None,
423 ));
424 }
425 }
426
427 let files = match ctx.export_step(true).await {
428 Ok(f) => f,
429 Err(err) => {
430 return Err(ExecErrorWithState::new(
431 ExecError::BadExport(format!("Export failed: {err:?}")),
432 exec_state.clone(),
433 None,
434 ));
435 }
436 };
437
438 ctx.close().await;
439
440 Ok((exec_state, result.0, files))
441}