Skip to main content

kcl_lib/
test_server.rs

1//! Types used to send data to the test server.
2
3use 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
26/// Executes a KCL program. Only returns success or error.
27pub 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    /// Bytes of the snapshot.
41    pub image: image::DynamicImage,
42    /// Glb binary containing mesh and brep data
43    pub glb: Glb,
44}
45
46/// Execute the kcl and ask the engine to render an image
47/// 2d kcl files can't be exported for local render
48/// Fails if geometry_only = true
49/// CTX should be closed by caller.
50pub 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    // Decode the snapshot, return it.
64    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/// Execute the kcl then export the resulting glb and CPU render an image locally
74/// cheaper than engine render since we can use the engine in geometry-only mode.
75/// CTX should be closed by caller.
76#[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    // export glb
85    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
124/// single-file binary blob containing mesh and brep
125pub 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    /// use the 3d engine scene to render an image
160    EngineRender,
161    /// the model is exportable. export and CPU render
162    ExportAndRender,
163    /// the model doesn't need any graphical test output
164    None,
165}
166
167#[cfg(test)]
168impl TestGraphicsParams {
169    fn geometry_only(&self) -> bool {
170        matches!(self, Self::ExportAndRender | Self::None)
171    }
172    /// kcl tests have `no3d` or `norun` flags in their declaration.
173    /// `norun` means "no graphics" and "no3d" means we want graphics but the model can't yet be exported for local rendering.
174    /// Translate these requirements into a more descriptive type here.
175    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
222/// Executes a kcl program and takes a snapshot of the result.
223/// This returns the bytes of the snapshot.
224pub 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/// Executes a KCL program and takes a snapshot without closing the engine
239/// connection. If OK, the caller must close the returned context.
240/// If Err, the context will already be closed within this function.
241#[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                // If there was an error executing the program, return it.
263                // Close the context to avoid any resource leaks.
264                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        // Use prod, don't override based on env vars.
344        // We do this so even in the engine repo, tests that need to run with
345        // no auth can fail in the same way as they would in prod.
346        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}