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