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