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