Skip to content

Commit 7c0861d

Browse files
committed
Add test
1 parent 4fd1999 commit 7c0861d

2 files changed

Lines changed: 172 additions & 82 deletions

File tree

crates/vespera_macro/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ rstest = "0.26"
2424
insta = "1.46"
2525
tempfile = "3"
2626
serial_test = "3"
27+
28+
[lints.rust]
29+
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }

crates/vespera_macro/src/lib.rs

Lines changed: 169 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ fn process_route_attribute(
5656
}
5757

5858
/// route attribute macro
59+
#[cfg_attr(coverage_nightly, coverage(off))]
5960
#[proc_macro_attribute]
6061
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
6162
match process_route_attribute(attr.into(), item.into()) {
@@ -109,6 +110,7 @@ fn process_derive_schema(input: &syn::DeriveInput) -> (StructMetadata, proc_macr
109110
/// Derive macro for Schema
110111
///
111112
/// Supports `#[schema(name = "CustomName")]` attribute to set custom OpenAPI schema name.
113+
#[cfg_attr(coverage_nightly, coverage(off))]
112114
#[proc_macro_derive(Schema, attributes(schema))]
113115
pub fn derive_schema(input: TokenStream) -> TokenStream {
114116
let input = syn::parse_macro_input!(input as syn::DeriveInput);
@@ -162,6 +164,7 @@ pub fn derive_schema(input: TokenStream) -> TokenStream {
162164
/// // For list endpoints, only return summary fields
163165
/// let list_schema = schema!(User, pick = ["id", "name"]);
164166
/// ```
167+
#[cfg_attr(coverage_nightly, coverage(off))]
165168
#[proc_macro]
166169
pub fn schema(input: TokenStream) -> TokenStream {
167170
let input = syn::parse_macro_input!(input as schema_macro::SchemaInput);
@@ -229,6 +232,7 @@ pub fn schema(input: TokenStream) -> TokenStream {
229232
/// // ...
230233
/// }
231234
/// ```
235+
#[cfg_attr(coverage_nightly, coverage(off))]
232236
#[proc_macro]
233237
pub fn schema_type(input: TokenStream) -> TokenStream {
234238
let input = syn::parse_macro_input!(input as schema_macro::SchemaTypeInput);
@@ -669,46 +673,49 @@ fn generate_and_write_openapi(
669673
Ok((docs_info, redoc_info))
670674
}
671675

672-
#[proc_macro]
673-
pub fn vespera(input: TokenStream) -> TokenStream {
674-
let input = syn::parse_macro_input!(input as AutoRouterInput);
675-
let processed = process_vespera_input(input);
676-
676+
/// Process vespera macro - extracted for testability
677+
fn process_vespera_macro(
678+
processed: &ProcessedVesperaInput,
679+
schema_storage: &[StructMetadata],
680+
) -> syn::Result<proc_macro2::TokenStream> {
677681
let folder_path = find_folder_path(&processed.folder_name);
678682
if !folder_path.exists() {
679-
return syn::Error::new(
683+
return Err(syn::Error::new(
680684
Span::call_site(),
681685
format!("Folder not found: {}", processed.folder_name),
682-
)
683-
.to_compile_error()
684-
.into();
686+
));
685687
}
686688

687-
let mut metadata = match collect_metadata(&folder_path, &processed.folder_name) {
688-
Ok(m) => m,
689-
Err(e) => {
690-
return syn::Error::new(
691-
Span::call_site(),
692-
format!("Failed to collect metadata: {}", e),
693-
)
694-
.to_compile_error()
695-
.into();
696-
}
697-
};
698-
metadata
699-
.structs
700-
.extend(SCHEMA_STORAGE.lock().unwrap().clone());
701-
702-
let (docs_info, redoc_info) = match generate_and_write_openapi(&processed, &metadata) {
703-
Ok(info) => info,
704-
Err(e) => {
705-
return syn::Error::new(Span::call_site(), e)
706-
.to_compile_error()
707-
.into();
708-
}
709-
};
689+
let mut metadata = collect_metadata(&folder_path, &processed.folder_name).map_err(|e| {
690+
syn::Error::new(
691+
Span::call_site(),
692+
format!("Failed to collect metadata: {}", e),
693+
)
694+
})?;
695+
metadata.structs.extend(schema_storage.iter().cloned());
696+
697+
let (docs_info, redoc_info) = generate_and_write_openapi(processed, &metadata)
698+
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
699+
700+
Ok(generate_router_code(
701+
&metadata,
702+
docs_info,
703+
redoc_info,
704+
&processed.merge,
705+
))
706+
}
707+
708+
#[cfg_attr(coverage_nightly, coverage(off))]
709+
#[proc_macro]
710+
pub fn vespera(input: TokenStream) -> TokenStream {
711+
let input = syn::parse_macro_input!(input as AutoRouterInput);
712+
let processed = process_vespera_input(input);
713+
let schema_storage = SCHEMA_STORAGE.lock().unwrap();
710714

711-
generate_router_code(&metadata, docs_info, redoc_info, &processed.merge).into()
715+
match process_vespera_macro(&processed, &schema_storage) {
716+
Ok(tokens) => tokens.into(),
717+
Err(e) => e.to_compile_error().into(),
718+
}
712719
}
713720

714721
fn find_folder_path(folder_name: &str) -> std::path::PathBuf {
@@ -971,72 +978,62 @@ impl Parse for ExportAppInput {
971978
/// // pub fn router() -> axum::Router { ... }
972979
/// // }
973980
/// ```
974-
#[proc_macro]
975-
pub fn export_app(input: TokenStream) -> TokenStream {
976-
let ExportAppInput { name, dir } = syn::parse_macro_input!(input as ExportAppInput);
977-
978-
let folder_name = dir
979-
.map(|d| d.value())
980-
.or_else(|| std::env::var("VESPERA_DIR").ok())
981-
.unwrap_or_else(|| "routes".to_string());
982-
983-
let folder_path = find_folder_path(&folder_name);
981+
///
982+
/// Process export_app macro - extracted for testability
983+
fn process_export_app(
984+
name: &syn::Ident,
985+
folder_name: &str,
986+
schema_storage: &[StructMetadata],
987+
manifest_dir: &str,
988+
) -> syn::Result<proc_macro2::TokenStream> {
989+
let folder_path = find_folder_path(folder_name);
984990
if !folder_path.exists() {
985-
return syn::Error::new(
991+
return Err(syn::Error::new(
986992
Span::call_site(),
987993
format!("Folder not found: {}", folder_name),
988-
)
989-
.to_compile_error()
990-
.into();
994+
));
991995
}
992996

993-
let mut metadata = match collect_metadata(&folder_path, &folder_name) {
994-
Ok(m) => m,
995-
Err(e) => {
996-
return syn::Error::new(
997-
Span::call_site(),
998-
format!("Failed to collect metadata: {}", e),
999-
)
1000-
.to_compile_error()
1001-
.into();
1002-
}
1003-
};
1004-
metadata
1005-
.structs
1006-
.extend(SCHEMA_STORAGE.lock().unwrap().clone());
997+
let mut metadata = collect_metadata(&folder_path, folder_name).map_err(|e| {
998+
syn::Error::new(
999+
Span::call_site(),
1000+
format!("Failed to collect metadata: {}", e),
1001+
)
1002+
})?;
1003+
metadata.structs.extend(schema_storage.iter().cloned());
10071004

10081005
// Generate OpenAPI spec JSON string
10091006
let openapi_doc = generate_openapi_doc_with_metadata(None, None, None, &metadata);
1010-
let spec_json = match serde_json::to_string(&openapi_doc) {
1011-
Ok(json) => json,
1012-
Err(e) => {
1013-
return syn::Error::new(
1014-
Span::call_site(),
1015-
format!("Failed to serialize OpenAPI spec: {}", e),
1016-
)
1017-
.to_compile_error()
1018-
.into();
1019-
}
1020-
};
1007+
let spec_json = serde_json::to_string(&openapi_doc).map_err(|e| {
1008+
syn::Error::new(
1009+
Span::call_site(),
1010+
format!("Failed to serialize OpenAPI spec: {}", e),
1011+
)
1012+
})?;
10211013

10221014
// Write spec to temp file for compile-time merging by parent apps
1023-
// The file is written to target/vespera/{StructName}.openapi.json
10241015
let name_str = name.to_string();
1025-
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
1026-
// Find target directory (go up from manifest dir to workspace root if needed)
1027-
let manifest_path = Path::new(&manifest_dir);
1016+
let manifest_path = Path::new(manifest_dir);
10281017
let target_dir = find_target_dir(manifest_path);
10291018
let vespera_dir = target_dir.join("vespera");
1030-
std::fs::create_dir_all(&vespera_dir)
1031-
.unwrap_or_else(|e| panic!("Failed to create vespera dir {:?}: {}", vespera_dir, e));
1019+
std::fs::create_dir_all(&vespera_dir).map_err(|e| {
1020+
syn::Error::new(
1021+
Span::call_site(),
1022+
format!("Failed to create vespera dir {:?}: {}", vespera_dir, e),
1023+
)
1024+
})?;
10321025
let spec_file = vespera_dir.join(format!("{}.openapi.json", name_str));
1033-
std::fs::write(&spec_file, &spec_json)
1034-
.unwrap_or_else(|e| panic!("Failed to write spec file {:?}: {}", spec_file, e));
1026+
std::fs::write(&spec_file, &spec_json).map_err(|e| {
1027+
syn::Error::new(
1028+
Span::call_site(),
1029+
format!("Failed to write spec file {:?}: {}", spec_file, e),
1030+
)
1031+
})?;
10351032

10361033
// Generate router code (without docs routes, no merge)
10371034
let router_code = generate_router_code(&metadata, None, None, &[]);
10381035

1039-
quote! {
1036+
Ok(quote! {
10401037
/// Auto-generated vespera app struct
10411038
pub struct #name;
10421039

@@ -1050,8 +1047,24 @@ pub fn export_app(input: TokenStream) -> TokenStream {
10501047
#router_code
10511048
}
10521049
}
1050+
})
1051+
}
1052+
1053+
#[cfg_attr(coverage_nightly, coverage(off))]
1054+
#[proc_macro]
1055+
pub fn export_app(input: TokenStream) -> TokenStream {
1056+
let ExportAppInput { name, dir } = syn::parse_macro_input!(input as ExportAppInput);
1057+
let folder_name = dir
1058+
.map(|d| d.value())
1059+
.or_else(|| std::env::var("VESPERA_DIR").ok())
1060+
.unwrap_or_else(|| "routes".to_string());
1061+
let schema_storage = SCHEMA_STORAGE.lock().unwrap();
1062+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
1063+
1064+
match process_export_app(&name, &folder_name, &schema_storage, &manifest_dir) {
1065+
Ok(tokens) => tokens.into(),
1066+
Err(e) => e.to_compile_error().into(),
10531067
}
1054-
.into()
10551068
}
10561069

10571070
#[cfg(test)]
@@ -2537,4 +2550,78 @@ pub fn get_users() -> String {
25372550
assert!(result.is_ok(), "Method {} should be valid", method);
25382551
}
25392552
}
2553+
2554+
// ========== Tests for process_vespera_macro ==========
2555+
2556+
#[test]
2557+
fn test_process_vespera_macro_folder_not_found() {
2558+
let processed = ProcessedVesperaInput {
2559+
folder_name: "nonexistent_folder_xyz_123".to_string(),
2560+
openapi_file_names: vec![],
2561+
title: None,
2562+
version: None,
2563+
docs_url: None,
2564+
redoc_url: None,
2565+
servers: None,
2566+
merge: vec![],
2567+
};
2568+
let result = process_vespera_macro(&processed, &[]);
2569+
assert!(result.is_err());
2570+
let err = result.unwrap_err().to_string();
2571+
assert!(err.contains("Folder not found"));
2572+
}
2573+
2574+
// ========== Tests for process_export_app ==========
2575+
2576+
#[test]
2577+
fn test_process_export_app_folder_not_found() {
2578+
let name: syn::Ident = syn::parse_quote!(TestApp);
2579+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
2580+
let result = process_export_app(
2581+
&name,
2582+
"nonexistent_folder_xyz",
2583+
&[],
2584+
&temp_dir.path().to_string_lossy(),
2585+
);
2586+
assert!(result.is_err());
2587+
let err = result.unwrap_err().to_string();
2588+
assert!(err.contains("Folder not found"));
2589+
}
2590+
2591+
// ========== Tests for generate_and_write_openapi with merge ==========
2592+
2593+
#[test]
2594+
fn test_generate_and_write_openapi_with_merge_no_manifest_dir() {
2595+
// When CARGO_MANIFEST_DIR is not set or merge is empty, it should work normally
2596+
let processed = ProcessedVesperaInput {
2597+
folder_name: "routes".to_string(),
2598+
openapi_file_names: vec![],
2599+
title: Some("Test".to_string()),
2600+
version: None,
2601+
docs_url: Some("/docs".to_string()),
2602+
redoc_url: None,
2603+
servers: None,
2604+
merge: vec![syn::parse_quote!(app::TestApp)], // Has merge but no valid manifest dir
2605+
};
2606+
let metadata = CollectedMetadata::new();
2607+
// This should still work - merge logic is skipped when CARGO_MANIFEST_DIR lookup fails
2608+
let result = generate_and_write_openapi(&processed, &metadata);
2609+
assert!(result.is_ok());
2610+
}
2611+
2612+
// ========== Tests for find_folder_path ==========
2613+
2614+
#[test]
2615+
fn test_find_folder_path_absolute_path() {
2616+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
2617+
let absolute_path = temp_dir.path().to_string_lossy().to_string();
2618+
2619+
// When given an absolute path that exists, it should return it
2620+
let result = find_folder_path(&absolute_path);
2621+
// The function tries src/{folder_name} first, then falls back to the folder_name directly
2622+
assert!(
2623+
result.to_string_lossy().contains(&absolute_path)
2624+
|| result == Path::new(&absolute_path)
2625+
);
2626+
}
25402627
}

0 commit comments

Comments
 (0)