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