1#![allow(clippy::style)]
4
5mod example_tests;
6#[cfg(test)]
7mod tests;
8mod unbox;
9
10use std::collections::HashMap;
11
12use convert_case::Casing;
13use inflector::{cases::camelcase::to_camel_case, Inflector};
14use once_cell::sync::Lazy;
15use quote::{format_ident, quote, quote_spanned, ToTokens};
16use regex::Regex;
17use serde::Deserialize;
18use serde_tokenstream::{from_tokenstream, Error};
19use syn::{
20 parse::{Parse, ParseStream},
21 Attribute, Signature, Visibility,
22};
23use unbox::unbox;
24
25#[proc_macro_attribute]
26pub fn stdlib(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
27 do_output(do_stdlib(attr.into(), item.into()))
28}
29
30#[proc_macro_attribute]
31pub fn for_each_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
32 example_tests::do_for_each_example_test(item.into()).into()
33}
34
35#[proc_macro_attribute]
36pub fn for_all_example_test(_attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
37 example_tests::do_for_all_example_test(item.into()).into()
38}
39
40#[derive(Deserialize, Debug)]
42struct ArgMetadata {
43 docs: String,
45
46 #[serde(default)]
49 include_in_snippet: bool,
50
51 #[serde(default)]
53 snippet_value: Option<String>,
54
55 #[serde(default)]
57 snippet_value_array: Option<Vec<String>>,
58}
59
60#[derive(Deserialize, Debug)]
61struct StdlibMetadata {
62 name: String,
64
65 #[serde(default)]
67 tags: Vec<String>,
68
69 #[serde(default)]
72 unpublished: bool,
73
74 #[serde(default)]
77 deprecated: bool,
78
79 #[serde(default)]
83 feature_tree_operation: bool,
84
85 #[serde(default)]
88 unlabeled_first: bool,
89
90 #[serde(default)]
92 args: HashMap<String, ArgMetadata>,
93}
94
95fn do_stdlib(
96 attr: proc_macro2::TokenStream,
97 item: proc_macro2::TokenStream,
98) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
99 let metadata = from_tokenstream(&attr)?;
100 do_stdlib_inner(metadata, attr, item)
101}
102
103fn do_output(res: Result<(proc_macro2::TokenStream, Vec<Error>), Error>) -> proc_macro::TokenStream {
104 match res {
105 Err(err) => err.to_compile_error().into(),
106 Ok((stdlib_docs, errors)) => {
107 let compiler_errors = errors.iter().map(|err| err.to_compile_error());
108
109 let output = quote! {
110 #stdlib_docs
111 #( #compiler_errors )*
112 };
113
114 output.into()
115 }
116 }
117}
118
119fn do_stdlib_inner(
120 metadata: StdlibMetadata,
121 _attr: proc_macro2::TokenStream,
122 item: proc_macro2::TokenStream,
123) -> Result<(proc_macro2::TokenStream, Vec<Error>), Error> {
124 let ast: ItemFnForSignature = syn::parse2(item.clone())?;
125
126 let mut errors = Vec::new();
127
128 if ast.sig.constness.is_some() {
129 errors.push(Error::new_spanned(
130 &ast.sig.constness,
131 "stdlib functions may not be const functions",
132 ));
133 }
134
135 if ast.sig.unsafety.is_some() {
136 errors.push(Error::new_spanned(
137 &ast.sig.unsafety,
138 "stdlib functions may not be unsafe",
139 ));
140 }
141
142 if ast.sig.abi.is_some() {
143 errors.push(Error::new_spanned(
144 &ast.sig.abi,
145 "stdlib functions may not use an alternate ABI",
146 ));
147 }
148
149 if !ast.sig.generics.params.is_empty() {
150 if ast.sig.generics.params.iter().any(|generic_type| match generic_type {
151 syn::GenericParam::Lifetime(_) => false,
152 syn::GenericParam::Type(_) => true,
153 syn::GenericParam::Const(_) => true,
154 }) {
155 errors.push(Error::new_spanned(
156 &ast.sig.generics,
157 "Stdlib functions may not be generic over types or constants, only lifetimes.",
158 ));
159 }
160 }
161
162 if ast.sig.variadic.is_some() {
163 errors.push(Error::new_spanned(&ast.sig.variadic, "no language C here"));
164 }
165
166 let name = metadata.name;
167
168 let name_cleaned = name.strip_suffix("2d").unwrap_or(name.as_str());
171 let name_cleaned = name.strip_suffix("3d").unwrap_or(name_cleaned);
172 if !name_cleaned.is_camel_case() {
173 errors.push(Error::new_spanned(
174 &ast.sig.ident,
175 format!("stdlib function names must be in camel case: `{}`", name),
176 ));
177 }
178
179 let name_ident = format_ident!("{}", name.to_case(convert_case::Case::UpperCamel));
180 let name_str = name.to_string();
181
182 let fn_name = &ast.sig.ident;
183 let fn_name_str = fn_name.to_string().replace("inner_", "");
184 let fn_name_ident = format_ident!("{}", fn_name_str);
185 let boxed_fn_name_ident = format_ident!("boxed_{}", fn_name_str);
186 let _visibility = &ast.vis;
187
188 let doc_info = extract_doc_from_attrs(&ast.attrs);
189 let comment_text = {
190 let mut buf = String::new();
191 buf.push_str("Std lib function: ");
192 buf.push_str(&name_str);
193 if let Some(s) = &doc_info.summary {
194 buf.push_str("\n");
195 buf.push_str(&s);
196 }
197 if let Some(s) = &doc_info.description {
198 buf.push_str("\n");
199 buf.push_str(&s);
200 }
201 buf
202 };
203 let description_doc_comment = quote! {
204 #[doc = #comment_text]
205 };
206
207 let summary = if let Some(summary) = doc_info.summary {
208 quote! { #summary }
209 } else {
210 quote! { "" }
211 };
212 let description = if let Some(description) = doc_info.description {
213 quote! { #description }
214 } else {
215 quote! { "" }
216 };
217
218 if doc_info.code_blocks.is_empty() {
219 errors.push(Error::new_spanned(
220 &ast.sig,
221 "stdlib functions must have at least one code block",
222 ));
223 }
224
225 for code_block in doc_info.code_blocks.iter() {
227 if !code_block.0.contains(&name) {
228 errors.push(Error::new_spanned(
229 &ast.sig,
230 format!(
231 "stdlib functions must have the function name `{}` in the code block",
232 name
233 ),
234 ));
235 }
236 }
237
238 let test_code_blocks = doc_info
239 .code_blocks
240 .iter()
241 .enumerate()
242 .map(|(index, (code_block, norun))| {
243 if !norun {
244 generate_code_block_test(&fn_name_str, code_block, index)
245 } else {
246 quote! {}
247 }
248 })
249 .collect::<Vec<_>>();
250
251 let (cb, norun): (Vec<_>, Vec<_>) = doc_info.code_blocks.into_iter().unzip();
252 let code_blocks = quote! {
253 let code_blocks = vec![#(#cb),*];
254 let norun = vec![#(#norun),*];
255 code_blocks.iter().zip(norun).map(|(cb, norun)| {
256 let program = crate::Program::parse_no_errs(cb).unwrap();
257
258 let mut options: crate::parsing::ast::types::FormatOptions = Default::default();
259 options.insert_final_newline = false;
260 (program.ast.recast(&options, 0), norun)
261 }).collect::<Vec<(String, bool)>>()
262 };
263
264 let tags = metadata
265 .tags
266 .iter()
267 .map(|tag| {
268 quote! { #tag.to_string() }
269 })
270 .collect::<Vec<_>>();
271
272 let deprecated = if metadata.deprecated {
273 quote! { true }
274 } else {
275 quote! { false }
276 };
277
278 let unpublished = if metadata.unpublished {
279 quote! { true }
280 } else {
281 quote! { false }
282 };
283
284 let feature_tree_operation = if metadata.feature_tree_operation {
285 quote! { true }
286 } else {
287 quote! { false }
288 };
289
290 let docs_crate = get_crate(None);
291
292 let mut arg_types = Vec::new();
298 for (i, arg) in ast.sig.inputs.iter().enumerate() {
299 let arg_name = match arg {
301 syn::FnArg::Receiver(pat) => {
302 let span = pat.self_token.span.unwrap();
303 span.source_text().unwrap().to_string()
304 }
305 syn::FnArg::Typed(pat) => match &*pat.pat {
306 syn::Pat::Ident(ident) => ident.ident.to_string(),
307 _ => {
308 errors.push(Error::new_spanned(
309 &pat.pat,
310 "stdlib functions may not use destructuring patterns",
311 ));
312 continue;
313 }
314 },
315 }
316 .trim_start_matches('_')
317 .to_string();
318 if arg_name == "exec_state" || arg_name == "args" {
320 continue;
321 }
322
323 let ty = match arg {
324 syn::FnArg::Receiver(pat) => pat.ty.as_ref().into_token_stream(),
325 syn::FnArg::Typed(pat) => pat.ty.as_ref().into_token_stream(),
326 };
327
328 let (ty_string, ty_ident) = clean_ty_string(ty.to_string().as_str());
329
330 let ty_string = rust_type_to_openapi_type(&ty_string);
331 let required = !ty_ident.to_string().starts_with("Option <");
332 let Some(arg_meta) = metadata.args.get(&arg_name) else {
333 errors.push(Error::new_spanned(arg, format!("arg {arg_name} not found")));
334 continue;
335 };
336 let description = arg_meta.docs.clone();
337 let include_in_snippet = required || arg_meta.include_in_snippet;
338 let snippet_value = arg_meta.snippet_value.clone();
339 let snippet_value_array = arg_meta.snippet_value_array.clone();
340 if snippet_value.is_some() && snippet_value_array.is_some() {
341 errors.push(Error::new_spanned(arg, format!("arg {arg_name} has set both snippet_value and snippet_value array, but at most one of these may be set. Please delete one of them.")));
342 }
343 let label_required = !(i == 0 && metadata.unlabeled_first);
344 let camel_case_arg_name = to_camel_case(&arg_name);
345 if ty_string != "ExecState" && ty_string != "Args" {
346 let schema = quote! {
347 generator.root_schema_for::<#ty_ident>()
348 };
349 let q0 = quote! {
350 name: #camel_case_arg_name.to_string(),
351 type_: #ty_string.to_string(),
352 schema: #schema,
353 required: #required,
354 label_required: #label_required,
355 description: #description.to_string(),
356 include_in_snippet: #include_in_snippet,
357 };
358 let q1 = if let Some(snippet_value) = snippet_value {
359 quote! {
360 snippet_value: Some(#snippet_value.to_owned()),
361 }
362 } else {
363 quote! {
364 snippet_value: None,
365 }
366 };
367 let q2 = if let Some(snippet_value_array) = snippet_value_array {
368 quote! {
369 snippet_value_array: Some(vec![
370 #(#snippet_value_array.to_owned()),*
371 ]),
372 }
373 } else {
374 quote! {
375 snippet_value_array: None,
376 }
377 };
378 arg_types.push(quote! {
379 #docs_crate::StdLibFnArg {
380 #q0
381 #q1
382 #q2
383 }
384 });
385 }
386 }
387
388 let return_type_inner = match &ast.sig.output {
389 syn::ReturnType::Default => quote! { () },
390 syn::ReturnType::Type(_, ty) => {
391 match &**ty {
393 syn::Type::Path(syn::TypePath { path, .. }) => {
394 let path = &path.segments;
395 if path.len() == 1 {
396 let seg = &path[0];
397 if seg.ident == "Result" {
398 if let syn::PathArguments::AngleBracketed(syn::AngleBracketedGenericArguments {
399 args,
400 ..
401 }) = &seg.arguments
402 {
403 if args.len() == 2 || args.len() == 1 {
404 let mut args = args.iter();
405 let ok = args.next().unwrap();
406 if let syn::GenericArgument::Type(ty) = ok {
407 let ty = unbox(ty.clone());
408 quote! { #ty }
409 } else {
410 quote! { () }
411 }
412 } else {
413 quote! { () }
414 }
415 } else {
416 quote! { () }
417 }
418 } else {
419 let ty = unbox(*ty.clone());
420 quote! { #ty }
421 }
422 } else {
423 quote! { () }
424 }
425 }
426 _ => {
427 quote! { () }
428 }
429 }
430 }
431 };
432
433 let ret_ty_string = return_type_inner.to_string().replace(' ', "");
434 let return_type = if !ret_ty_string.is_empty() || ret_ty_string != "()" {
435 let ret_ty_string = rust_type_to_openapi_type(&ret_ty_string);
436 quote! {
437 let schema = generator.root_schema_for::<#return_type_inner>();
438 Some(#docs_crate::StdLibFnArg {
439 name: "".to_string(),
440 type_: #ret_ty_string.to_string(),
441 schema,
442 required: true,
443 label_required: true,
444 description: String::new(),
445 include_in_snippet: true,
446 snippet_value: None,
447 snippet_value_array: None,
448 })
449 }
450 } else {
451 quote! {
452 None
453 }
454 };
455
456 let span = ast.sig.ident.span();
462 let const_struct = quote_spanned! {span=>
463 pub(crate) const #name_ident: #name_ident = #name_ident {};
464 };
465
466 let test_mod_name = format_ident!("test_examples_{}", fn_name_str);
467
468 let stream = quote! {
471 #[cfg(test)]
472 mod #test_mod_name {
473 #(#test_code_blocks)*
474 }
475
476 #[allow(non_camel_case_types, missing_docs)]
478 #description_doc_comment
479 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, schemars::JsonSchema, ts_rs::TS)]
480 #[ts(export)]
481 pub(crate) struct #name_ident {}
482 #[allow(non_upper_case_globals, missing_docs)]
484 #description_doc_comment
485 #const_struct
486
487 fn #boxed_fn_name_ident(
488 exec_state: &mut crate::execution::ExecState,
489 args: crate::std::Args,
490 ) -> std::pin::Pin<
491 Box<dyn std::future::Future<Output = anyhow::Result<crate::execution::KclValue, crate::errors::KclError>> + Send + '_>,
492 > {
493 Box::pin(#fn_name_ident(exec_state, args))
494 }
495
496 impl #docs_crate::StdLibFn for #name_ident
497 {
498 fn name(&self) -> String {
499 #name_str.to_string()
500 }
501
502 fn summary(&self) -> String {
503 #summary.to_string()
504 }
505
506 fn description(&self) -> String {
507 #description.to_string()
508 }
509
510 fn tags(&self) -> Vec<String> {
511 vec![#(#tags),*]
512 }
513
514 fn args(&self, inline_subschemas: bool) -> Vec<#docs_crate::StdLibFnArg> {
515 let mut settings = schemars::gen::SchemaSettings::openapi3();
516 settings.inline_subschemas = inline_subschemas;
518 let mut generator = schemars::gen::SchemaGenerator::new(settings);
519
520 vec![#(#arg_types),*]
521 }
522
523 fn return_value(&self, inline_subschemas: bool) -> Option<#docs_crate::StdLibFnArg> {
524 let mut settings = schemars::gen::SchemaSettings::openapi3();
525 settings.inline_subschemas = inline_subschemas;
527 let mut generator = schemars::gen::SchemaGenerator::new(settings);
528
529 #return_type
530 }
531
532 fn unpublished(&self) -> bool {
533 #unpublished
534 }
535
536 fn deprecated(&self) -> bool {
537 #deprecated
538 }
539
540 fn feature_tree_operation(&self) -> bool {
541 #feature_tree_operation
542 }
543
544 fn examples(&self) -> Vec<(String, bool)> {
545 #code_blocks
546 }
547
548 fn std_lib_fn(&self) -> crate::std::StdFn {
549 #boxed_fn_name_ident
550 }
551
552 fn clone_box(&self) -> Box<dyn #docs_crate::StdLibFn> {
553 Box::new(self.clone())
554 }
555 }
556
557 #item
558 };
559
560 if !errors.is_empty() {
562 errors.insert(0, Error::new_spanned(&ast.sig, ""));
563 }
564
565 Ok((stream, errors))
566}
567
568#[allow(dead_code)]
569fn to_compile_errors(errors: Vec<syn::Error>) -> proc_macro2::TokenStream {
570 let compile_errors = errors.iter().map(syn::Error::to_compile_error);
571 quote!(#(#compile_errors)*)
572}
573
574fn get_crate(var: Option<String>) -> proc_macro2::TokenStream {
575 if let Some(s) = var {
576 if let Ok(ts) = syn::parse_str(s.as_str()) {
577 return ts;
578 }
579 }
580 quote!(crate::docs)
581}
582
583#[derive(Debug)]
584struct DocInfo {
585 pub summary: Option<String>,
586 pub description: Option<String>,
587 pub code_blocks: Vec<(String, bool)>,
588}
589
590fn extract_doc_from_attrs(attrs: &[syn::Attribute]) -> DocInfo {
591 let doc = syn::Ident::new("doc", proc_macro2::Span::call_site());
592 let raw_lines = attrs.iter().flat_map(|attr| {
593 if let syn::Meta::NameValue(nv) = &attr.meta {
594 if nv.path.is_ident(&doc) {
595 if let syn::Expr::Lit(syn::ExprLit {
596 lit: syn::Lit::Str(s), ..
597 }) = &nv.value
598 {
599 return normalize_comment_string(s.value());
600 }
601 }
602 }
603 Vec::new()
604 });
605
606 let mut code_blocks: Vec<(String, bool)> = Vec::new();
608 let mut code_block: Option<(String, bool)> = None;
609 let mut parsed_lines = Vec::new();
610 for line in raw_lines {
611 if line.starts_with("```") {
612 if let Some((inner_code_block, norun)) = code_block {
613 code_blocks.push((inner_code_block.trim().to_owned(), norun));
614 code_block = None;
615 } else {
616 let norun = line.contains("kcl,norun") || line.contains("kcl,no_run");
617 code_block = Some((String::new(), norun));
618 }
619
620 continue;
621 }
622 if let Some((code_block, _)) = &mut code_block {
623 code_block.push_str(&line);
624 code_block.push('\n');
625 } else {
626 parsed_lines.push(line);
627 }
628 }
629
630 if let Some((code_block, norun)) = code_block {
631 code_blocks.push((code_block.trim().to_string(), norun));
632 }
633
634 let mut summary = None;
635 let mut description: Option<String> = None;
636 for line in parsed_lines {
637 if line.is_empty() {
638 if let Some(desc) = &mut description {
639 if !desc.is_empty() && !desc.ends_with('\n') {
641 if desc.ends_with(' ') {
642 desc.pop().unwrap();
643 }
644 desc.push_str("\n\n");
645 }
646 } else if summary.is_some() {
647 description = Some(String::new());
648 }
649 continue;
650 }
651
652 if let Some(desc) = &mut description {
653 desc.push_str(&line);
654 desc.push(' ');
656 continue;
657 }
658
659 if summary.is_none() {
660 summary = Some(String::new());
661 }
662 match &mut summary {
663 Some(summary) => {
664 summary.push_str(&line);
665 summary.push(' ');
667 }
668 None => unreachable!(),
669 }
670 }
671
672 if let Some(s) = &mut summary {
674 while s.ends_with(' ') || s.ends_with('\n') {
675 s.pop().unwrap();
676 }
677
678 if s.is_empty() {
679 summary = None;
680 }
681 }
682
683 if let Some(d) = &mut description {
684 while d.ends_with(' ') || d.ends_with('\n') {
685 d.pop().unwrap();
686 }
687
688 if d.is_empty() {
689 description = None;
690 }
691 }
692
693 DocInfo {
694 summary,
695 description,
696 code_blocks,
697 }
698}
699
700fn normalize_comment_string(s: String) -> Vec<String> {
701 s.split('\n')
702 .map(|s| {
703 s.strip_prefix(' ').unwrap_or(s).trim_end().to_owned()
707 })
708 .collect()
709}
710
711#[derive(Clone)]
714struct ItemFnForSignature {
715 pub attrs: Vec<Attribute>,
716 pub vis: Visibility,
717 pub sig: Signature,
718 pub _block: proc_macro2::TokenStream,
719}
720
721impl Parse for ItemFnForSignature {
722 fn parse(input: ParseStream) -> syn::parse::Result<Self> {
723 let attrs = input.call(Attribute::parse_outer)?;
724 let vis: Visibility = input.parse()?;
725 let sig: Signature = input.parse()?;
726 let block = input.parse()?;
727 Ok(ItemFnForSignature {
728 attrs,
729 vis,
730 sig,
731 _block: block,
732 })
733 }
734}
735
736fn clean_ty_string(t: &str) -> (String, proc_macro2::TokenStream) {
737 let mut ty_string = t
738 .replace("& 'a", "")
739 .replace('&', "")
740 .replace("mut", "")
741 .replace("< 'a >", "")
742 .replace(' ', "");
743 if ty_string.starts_with("ExecState") {
744 ty_string = "ExecState".to_string();
745 }
746 if ty_string.starts_with("Args") {
747 ty_string = "Args".to_string();
748 }
749 let ty_string = ty_string.trim().to_string();
750 let ty_ident = if ty_string.starts_with("Vec<") {
751 let ty_string = ty_string.trim_start_matches("Vec<").trim_end_matches('>');
752 let (_, ty_ident) = clean_ty_string(&ty_string);
753 quote! {
754 Vec<#ty_ident>
755 }
756 } else if ty_string.starts_with("kittycad::types::") {
757 let ty_string = ty_string.trim_start_matches("kittycad::types::").trim_end_matches('>');
758 let ty_ident = format_ident!("{}", ty_string);
759 quote! {
760 kittycad::types::#ty_ident
761 }
762 } else if ty_string.starts_with("Option<") {
763 let ty_string = ty_string.trim_start_matches("Option<").trim_end_matches('>');
764 let (_, ty_ident) = clean_ty_string(&ty_string);
765 quote! {
766 Option<#ty_ident>
767 }
768 } else if let Some((inner_array_type, num)) = parse_array_type(&ty_string) {
769 let ty_string = inner_array_type.to_owned();
770 let (_, ty_ident) = clean_ty_string(&ty_string);
771 quote! {
772 [#ty_ident; #num]
773 }
774 } else if ty_string.starts_with("Box<") {
775 let ty_string = ty_string.trim_start_matches("Box<").trim_end_matches('>');
776 let (_, ty_ident) = clean_ty_string(&ty_string);
777 quote! {
778 #ty_ident
779 }
780 } else {
781 let ty_ident = format_ident!("{}", ty_string);
782 quote! {
783 #ty_ident
784 }
785 };
786
787 (ty_string, ty_ident)
788}
789
790fn rust_type_to_openapi_type(t: &str) -> String {
791 let mut t = t.to_string();
792 if t.starts_with("Vec<") {
795 t = t.replace("Vec<", "[").replace('>', "]");
796 }
797 if t.starts_with("Box<") {
798 t = t.replace("Box<", "").replace('>', "");
799 }
800 if t.starts_with("Option<") {
801 t = t.replace("Option<", "").replace('>', "");
802 }
803
804 if t == "[TyF64;2]" {
805 return "Point2d".to_owned();
806 }
807 if t == "[TyF64;3]" {
808 return "Point3d".to_owned();
809 }
810
811 if let Some((inner_type, _length)) = parse_array_type(&t) {
812 t = format!("[{inner_type}]")
813 }
814
815 if t == "f64" || t == "TyF64" || t == "u32" || t == "NonZeroU32" {
816 return "number".to_string();
817 } else if t == "str" || t == "String" {
818 return "string".to_string();
819 } else {
820 return t.replace("f64", "number").replace("TyF64", "number").to_string();
821 }
822}
823
824fn parse_array_type(type_name: &str) -> Option<(&str, usize)> {
825 static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\[([a-zA-Z0-9<>]+); ?(\d+)\]").unwrap());
826 let cap = RE.captures(type_name)?;
827 let inner_type = cap.get(1)?;
828 let length = cap.get(2)?.as_str().parse().ok()?;
829 Some((inner_type.as_str(), length))
830}
831
832fn generate_code_block_test(fn_name: &str, code_block: &str, index: usize) -> proc_macro2::TokenStream {
835 let test_name = format_ident!("kcl_test_example_{}{}", fn_name, index);
836 let test_name_mock = format_ident!("test_mock_example_{}{}", fn_name, index);
837 let output_test_name_str = format!("serial_test_example_{}{}", fn_name, index);
838
839 quote! {
840 #[tokio::test(flavor = "multi_thread")]
841 async fn #test_name_mock() -> miette::Result<()> {
842 let program = crate::Program::parse_no_errs(#code_block).unwrap();
843 let ctx = crate::ExecutorContext {
844 engine: std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().await.unwrap())),
845 fs: std::sync::Arc::new(crate::fs::FileManager::new()),
846 stdlib: std::sync::Arc::new(crate::std::StdLib::new()),
847 settings: Default::default(),
848 context_type: crate::execution::ContextType::Mock,
849 };
850
851 if let Err(e) = ctx.run(&program, &mut crate::execution::ExecState::new(&ctx)).await {
852 return Err(miette::Report::new(crate::errors::Report {
853 error: e.error,
854 filename: format!("{}{}", #fn_name, #index),
855 kcl_source: #code_block.to_string(),
856 }));
857 }
858 Ok(())
859 }
860
861 #[tokio::test(flavor = "multi_thread", worker_threads = 5)]
862 async fn #test_name() -> miette::Result<()> {
863 let code = #code_block;
864 let result = match crate::test_server::execute_and_snapshot(code, None).await {
866 Err(crate::errors::ExecError::Kcl(e)) => {
867 return Err(miette::Report::new(crate::errors::Report {
868 error: e.error,
869 filename: format!("{}{}", #fn_name, #index),
870 kcl_source: #code_block.to_string(),
871 }));
872 }
873 Err(other_err)=> panic!("{}", other_err),
874 Ok(img) => img,
875 };
876 twenty_twenty::assert_image(&format!("tests/outputs/{}.png", #output_test_name_str), &result, 0.99);
877 Ok(())
878 }
879 }
880}