kcl_lib/
test_server.rs

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