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