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