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::{
8    ConnectionError, ExecError, KclError, KclErrorWithOutputs, Program,
9    engine::new_zoo_client,
10    errors::ExecErrorWithState,
11    execution::{EnvironmentRef, ExecState, ExecutorContext, ExecutorSettings},
12};
13
14#[derive(serde::Deserialize, serde::Serialize)]
15pub struct RequestBody {
16    pub kcl_program: String,
17    #[serde(default)]
18    pub test_name: String,
19}
20
21/// Executes a kcl program and takes a snapshot of the result.
22/// This returns the bytes of the snapshot.
23pub async fn execute_and_snapshot(code: &str, current_file: Option<PathBuf>) -> Result<image::DynamicImage, ExecError> {
24    let ctx = new_context(true, current_file).await?;
25    let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
26    let res = do_execute_and_snapshot(&ctx, program)
27        .await
28        .map(|(_, _, snap)| snap)
29        .map_err(|err| err.error);
30    ctx.close().await;
31    res
32}
33
34pub struct Snapshot3d {
35    /// Bytes of the snapshot.
36    pub image: image::DynamicImage,
37    /// Various GLTF files for the resulting export.
38    pub gltf: Vec<RawFile>,
39}
40
41/// Executes a kcl program and takes a snapshot of the result.
42pub async fn execute_and_snapshot_3d(code: &str, current_file: Option<PathBuf>) -> Result<Snapshot3d, ExecError> {
43    let ctx = new_context(true, current_file).await?;
44    let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
45    let image = do_execute_and_snapshot(&ctx, program)
46        .await
47        .map(|(_, _, snap)| snap)
48        .map_err(|err| err.error)?;
49    let gltf_res = ctx
50        .export(kittycad_modeling_cmds::format::OutputFormat3d::Gltf(Default::default()))
51        .await;
52    let gltf = match gltf_res {
53        Err(err) if err.message() == "Nothing to export" => Vec::new(),
54        Err(err) => {
55            eprintln!("Error exporting: {}", err.message());
56            Vec::new()
57        }
58        Ok(x) => x,
59    };
60    ctx.close().await;
61    Ok(Snapshot3d { image, gltf })
62}
63/// Executes a kcl program and takes a snapshot of the result.
64/// This returns the bytes of the snapshot.
65#[cfg(test)]
66pub async fn execute_and_snapshot_ast(
67    ast: Program,
68    current_file: Option<PathBuf>,
69    with_export_step: bool,
70) -> Result<
71    (
72        ExecState,
73        ExecutorContext,
74        EnvironmentRef,
75        image::DynamicImage,
76        Option<Vec<u8>>,
77    ),
78    ExecErrorWithState,
79> {
80    let ctx = new_context(true, current_file).await?;
81    let (exec_state, env, img) = match do_execute_and_snapshot(&ctx, ast).await {
82        Ok((exec_state, env_ref, img)) => (exec_state, env_ref, img),
83        Err(err) => {
84            // If there was an error executing the program, return it.
85            // Close the context to avoid any resource leaks.
86            ctx.close().await;
87            return Err(err);
88        }
89    };
90    let mut step = None;
91    if with_export_step {
92        let files = match ctx.export_step(true).await {
93            Ok(f) => f,
94            Err(err) => {
95                // Close the context to avoid any resource leaks.
96                ctx.close().await;
97                return Err(ExecErrorWithState::new(
98                    ExecError::BadExport(format!("Export failed: {err:?}")),
99                    exec_state.clone(),
100                ));
101            }
102        };
103
104        step = files.into_iter().next().map(|f| f.contents);
105    }
106    ctx.close().await;
107    Ok((exec_state, ctx, env, img, step))
108}
109
110pub async fn execute_and_snapshot_no_auth(
111    code: &str,
112    current_file: Option<PathBuf>,
113) -> Result<(image::DynamicImage, EnvironmentRef), ExecError> {
114    let ctx = new_context(false, current_file).await?;
115    let program = Program::parse_no_errs(code).map_err(KclErrorWithOutputs::no_outputs)?;
116    let res = do_execute_and_snapshot(&ctx, program)
117        .await
118        .map(|(_, env_ref, snap)| (snap, env_ref))
119        .map_err(|err| err.error);
120    ctx.close().await;
121    res
122}
123
124async fn do_execute_and_snapshot(
125    ctx: &ExecutorContext,
126    program: Program,
127) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
128    let mut exec_state = ExecState::new(ctx);
129    let result = ctx
130        .run(&program, &mut exec_state)
131        .await
132        .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?;
133    for e in exec_state.errors() {
134        if e.severity.is_err() {
135            return Err(ExecErrorWithState::new(
136                KclErrorWithOutputs::no_outputs(KclError::new_semantic(e.clone().into())).into(),
137                exec_state.clone(),
138            ));
139        }
140    }
141    let snapshot_png_bytes = ctx
142        .prepare_snapshot()
143        .await
144        .map_err(|err| ExecErrorWithState::new(err, exec_state.clone()))?
145        .contents
146        .0;
147
148    // Decode the snapshot, return it.
149    let img = image::ImageReader::new(std::io::Cursor::new(snapshot_png_bytes))
150        .with_guessed_format()
151        .map_err(|e| ExecError::BadPng(e.to_string()))
152        .and_then(|x| x.decode().map_err(|e| ExecError::BadPng(e.to_string())))
153        .map_err(|err| ExecErrorWithState::new(err, exec_state.clone()))?;
154
155    Ok((exec_state, result.0, img))
156}
157
158pub async fn new_context(with_auth: bool, current_file: Option<PathBuf>) -> Result<ExecutorContext, ConnectionError> {
159    let mut client = new_zoo_client(if with_auth { None } else { Some("bad_token".to_string()) }, None)
160        .map_err(ConnectionError::CouldNotMakeClient)?;
161    if !with_auth {
162        // Use prod, don't override based on env vars.
163        // We do this so even in the engine repo, tests that need to run with
164        // no auth can fail in the same way as they would in prod.
165        client.set_base_url("https://api.zoo.dev".to_string());
166    }
167
168    let mut settings = ExecutorSettings {
169        highlight_edges: true,
170        enable_ssao: false,
171        show_grid: false,
172        replay: None,
173        project_directory: None,
174        current_file: None,
175        fixed_size_grid: true,
176    };
177    if let Some(current_file) = current_file {
178        settings.with_current_file(crate::TypedPath(current_file));
179    }
180    let ctx = ExecutorContext::new(&client, settings)
181        .await
182        .map_err(ConnectionError::Establishing)?;
183    Ok(ctx)
184}
185
186pub async fn execute_and_export_step(
187    code: &str,
188    current_file: Option<PathBuf>,
189) -> Result<
190    (
191        ExecState,
192        EnvironmentRef,
193        Vec<kittycad_modeling_cmds::websocket::RawFile>,
194    ),
195    ExecErrorWithState,
196> {
197    let ctx = new_context(true, current_file).await?;
198    let mut exec_state = ExecState::new(&ctx);
199    let program = Program::parse_no_errs(code)
200        .map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?;
201    let result = ctx
202        .run(&program, &mut exec_state)
203        .await
204        .map_err(|err| ExecErrorWithState::new(err.into(), exec_state.clone()))?;
205    for e in exec_state.errors() {
206        if e.severity.is_err() {
207            return Err(ExecErrorWithState::new(
208                KclErrorWithOutputs::no_outputs(KclError::new_semantic(e.clone().into())).into(),
209                exec_state.clone(),
210            ));
211        }
212    }
213
214    let files = match ctx.export_step(true).await {
215        Ok(f) => f,
216        Err(err) => {
217            return Err(ExecErrorWithState::new(
218                ExecError::BadExport(format!("Export failed: {err:?}")),
219                exec_state.clone(),
220            ));
221        }
222    };
223
224    ctx.close().await;
225
226    Ok((exec_state, result.0, files))
227}