kcl_lib/
test_server.rs

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