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_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    /// 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
49pub 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    // Decode the snapshot, return it.
63    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/// Execute the kcl then export the resulting glb and CPU render an image locally
73/// cheaper than engine render since we can use the engine in geometry-only mode.
74#[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    // export glb
83    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            // Close the context to avoid any resource leaks.
94            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
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 = 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
216/// Executes a kcl program and takes a snapshot of the result.
217/// This returns the bytes of the snapshot.
218pub 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/// Executes a KCL program and takes a snapshot without closing the engine
233/// connection. If OK, the caller must close the returned context.
234/// If Err, the context will already be closed within this function.
235#[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                // If there was an error executing the program, return it.
257                // Close the context to avoid any resource leaks.
258                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        // Use prod, don't override based on env vars.
338        // We do this so even in the engine repo, tests that need to run with
339        // no auth can fail in the same way as they would in prod.
340        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}