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