Skip to content

Commit d25fafa

Browse files
committed
Add test
1 parent 57ad145 commit d25fafa

6 files changed

Lines changed: 639 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ target
22
lcov.info
33
coverage
44
build_rs_cov.profraw
5+
.sisyphus/

crates/vespera_macro/src/openapi_generator.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,4 +1154,181 @@ pub fn get_config() -> Config {
11541154
let schemas = doc.components.as_ref().unwrap().schemas.as_ref().unwrap();
11551155
assert!(schemas.contains_key("Config"));
11561156
}
1157+
1158+
// ======== Tests for uncovered lines ========
1159+
1160+
#[test]
1161+
fn test_fallback_struct_finding_in_route_files() {
1162+
// Test line 65: fallback loop that finds struct in any route file when direct search fails
1163+
let temp_dir = TempDir::new().expect("Failed to create temp dir");
1164+
1165+
// Create TWO route files - struct is in second file, route references it from first
1166+
let route1_content = r#"
1167+
pub fn get_users() -> Vec<User> {
1168+
vec![]
1169+
}
1170+
"#;
1171+
let route1_file = create_temp_file(&temp_dir, "users.rs", route1_content);
1172+
1173+
let route2_content = r#"
1174+
fn default_name() -> String {
1175+
"Guest".to_string()
1176+
}
1177+
1178+
struct User {
1179+
#[serde(default = "default_name")]
1180+
name: String,
1181+
}
1182+
1183+
pub fn get_user() -> User {
1184+
User { name: "Alice".to_string() }
1185+
}
1186+
"#;
1187+
let route2_file = create_temp_file(&temp_dir, "user.rs", route2_content);
1188+
1189+
let mut metadata = CollectedMetadata::new();
1190+
// Add struct but point to route1 (which doesn't contain the struct)
1191+
// This forces the fallback loop to search other route files
1192+
metadata.structs.push(StructMetadata {
1193+
name: "User".to_string(),
1194+
definition: r#"struct User { #[serde(default = "default_name")] name: String }"#
1195+
.to_string(),
1196+
});
1197+
// Add BOTH routes - the first doesn't contain User struct, so fallback searches the second
1198+
metadata.routes.push(RouteMetadata {
1199+
method: "GET".to_string(),
1200+
path: "/users".to_string(),
1201+
function_name: "get_users".to_string(),
1202+
module_path: "test::users".to_string(),
1203+
file_path: route1_file.to_string_lossy().to_string(),
1204+
signature: "fn get_users() -> Vec<User>".to_string(),
1205+
error_status: None,
1206+
tags: None,
1207+
description: None,
1208+
});
1209+
metadata.routes.push(RouteMetadata {
1210+
method: "GET".to_string(),
1211+
path: "/user".to_string(),
1212+
function_name: "get_user".to_string(),
1213+
module_path: "test::user".to_string(),
1214+
file_path: route2_file.to_string_lossy().to_string(),
1215+
signature: "fn get_user() -> User".to_string(),
1216+
error_status: None,
1217+
tags: None,
1218+
description: None,
1219+
});
1220+
1221+
let doc = generate_openapi_doc_with_metadata(None, None, None, &metadata);
1222+
1223+
// Struct should be found via fallback and processed
1224+
assert!(doc.components.as_ref().unwrap().schemas.is_some());
1225+
let schemas = doc.components.as_ref().unwrap().schemas.as_ref().unwrap();
1226+
assert!(schemas.contains_key("User"));
1227+
}
1228+
1229+
#[test]
1230+
fn test_process_default_functions_with_no_properties() {
1231+
// Test line 152: early return when schema.properties is None
1232+
// This happens when a struct has no named fields (unit struct or tuple struct)
1233+
use vespera_core::schema::Schema;
1234+
1235+
let struct_item: syn::ItemStruct = syn::parse_str("struct Empty;").unwrap();
1236+
let file_ast: syn::File = syn::parse_str("fn foo() {}").unwrap();
1237+
let mut schema = Schema::object();
1238+
schema.properties = None; // Explicitly set to None
1239+
1240+
// This should return early without panic
1241+
process_default_functions(&struct_item, &file_ast, &mut schema);
1242+
1243+
// Schema should remain unchanged
1244+
assert!(schema.properties.is_none());
1245+
}
1246+
1247+
#[test]
1248+
fn test_extract_value_from_expr_int_parse_failure() {
1249+
// Test line 253: int parse failure (overflow)
1250+
// Create an integer literal that's too large to parse as i64
1251+
// Use a literal that syn will parse but i64::parse will fail on
1252+
let expr: syn::Expr = syn::parse_str("999999999999999999999999999999").unwrap();
1253+
let value = extract_value_from_expr(&expr);
1254+
assert!(value.is_none());
1255+
}
1256+
1257+
#[test]
1258+
fn test_extract_value_from_expr_float_parse_failure() {
1259+
// Test line 260: float parse failure
1260+
// Create a float literal that's too large/invalid
1261+
let expr: syn::Expr = syn::parse_str("1e999999").unwrap();
1262+
let value = extract_value_from_expr(&expr);
1263+
// This may parse successfully to infinity or fail - either way should handle it
1264+
// The important thing is no panic
1265+
let _ = value;
1266+
}
1267+
1268+
#[test]
1269+
fn test_extract_value_from_expr_method_call_with_nested_receiver() {
1270+
// Test lines 275-276: recursive extraction from method call receiver
1271+
// When receiver is not a direct string literal, it tries to extract recursively
1272+
// But the recursive call also won't find a Lit, so it returns None
1273+
// This test verifies the recursive path is exercised (line 275-276)
1274+
let expr: syn::Expr = syn::parse_str(r#"("hello").to_string()"#).unwrap();
1275+
let value = extract_value_from_expr(&expr);
1276+
// The receiver is a Paren expression - recursive call is made but returns None
1277+
// because Paren is not handled in the match
1278+
assert!(value.is_none());
1279+
}
1280+
1281+
#[test]
1282+
fn test_extract_value_from_expr_method_call_with_non_literal_receiver() {
1283+
// Test lines 275-276: recursive extraction fails for non-literal
1284+
let expr: syn::Expr = syn::parse_str(r#"some_var.to_string()"#).unwrap();
1285+
let value = extract_value_from_expr(&expr);
1286+
// Cannot extract value from a variable
1287+
assert!(value.is_none());
1288+
}
1289+
1290+
#[test]
1291+
fn test_extract_value_from_expr_method_call_chained_to_string() {
1292+
// Test lines 275-276: another case where recursive extraction is attempted
1293+
// Chained method calls: 42.to_string() has int literal as receiver
1294+
let expr: syn::Expr = syn::parse_str(r#"42.to_string()"#).unwrap();
1295+
let value = extract_value_from_expr(&expr);
1296+
// Line 275 recursive call extracts 42 as Number, then line 276 returns it
1297+
assert_eq!(value, Some(serde_json::Value::Number(42.into())));
1298+
}
1299+
1300+
#[test]
1301+
fn test_get_type_default_empty_path_segments() {
1302+
// Test line 307: empty path segments returns None
1303+
// Create a type with empty path segments
1304+
1305+
// Use parse to create a valid type, then we verify the normal path works
1306+
let ty: syn::Type = syn::parse_str("::String").unwrap();
1307+
// This has segments, so it should work
1308+
let value = get_type_default(&ty);
1309+
// Global path ::String still has "String" as last segment
1310+
assert!(value.is_some());
1311+
1312+
// Test reference type (non-path type)
1313+
let ref_ty: syn::Type = syn::parse_str("&str").unwrap();
1314+
let ref_value = get_type_default(&ref_ty);
1315+
// Reference is not a Path type, so returns None via line 310
1316+
assert!(ref_value.is_none());
1317+
}
1318+
1319+
#[test]
1320+
fn test_get_type_default_tuple_type() {
1321+
// Test line 310: non-Path type returns None
1322+
let ty: syn::Type = syn::parse_str("(i32, String)").unwrap();
1323+
let value = get_type_default(&ty);
1324+
assert!(value.is_none());
1325+
}
1326+
1327+
#[test]
1328+
fn test_get_type_default_array_type() {
1329+
// Test line 310: array type returns None
1330+
let ty: syn::Type = syn::parse_str("[i32; 3]").unwrap();
1331+
let value = get_type_default(&ty);
1332+
assert!(value.is_none());
1333+
}
11571334
}

crates/vespera_macro/src/parser/operation.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,4 +526,126 @@ mod tests {
526526
assert_body(&op, &expected_body);
527527
assert_responses(&op, &expected_resps);
528528
}
529+
530+
// ======== Tests for uncovered lines ========
531+
532+
#[test]
533+
fn test_single_path_param_with_single_type() {
534+
// Test line 55: Path<T> with single type (not tuple) and exactly ONE path param
535+
// This exercises the branch: path_params.len() == 1 with non-tuple type
536+
let op = build("fn get(Path(id): Path<i32>) -> String", "/users/{id}", None);
537+
538+
// Should have exactly 1 path parameter with Integer type
539+
let params = op.parameters.as_ref().expect("parameters expected");
540+
assert_eq!(params.len(), 1);
541+
assert_eq!(params[0].name, "id");
542+
assert_eq!(param_schema_type(&params[0]), Some(SchemaType::Integer));
543+
}
544+
545+
#[test]
546+
fn test_single_path_param_with_string_type() {
547+
// Another test for line 55: Path<String> with single path param
548+
let op = build(
549+
"fn get(Path(id): Path<String>) -> String",
550+
"/users/{user_id}",
551+
None,
552+
);
553+
554+
let params = op.parameters.as_ref().expect("parameters expected");
555+
assert_eq!(params.len(), 1);
556+
assert_eq!(params[0].name, "user_id");
557+
assert_eq!(param_schema_type(&params[0]), Some(SchemaType::String));
558+
}
559+
560+
#[test]
561+
fn test_non_path_extractor_with_query() {
562+
// Test lines 85, 89: non-Path extractor handling
563+
// When input is Query<T>, it should NOT be treated as Path
564+
let op = build(
565+
"fn search(Query(params): Query<QueryParams>) -> String",
566+
"/search",
567+
None,
568+
);
569+
570+
// Query params should be extended to parameters (line 89)
571+
// But QueryParams is not in known_schemas/struct_definitions so it won't appear
572+
// The key is that it doesn't treat Query as a Path extractor (line 85 returns false)
573+
assert!(op.request_body.is_none()); // Query is not a body
574+
}
575+
576+
#[test]
577+
fn test_non_path_extractor_with_state() {
578+
// Test lines 85, 89: State<T> should be ignored (not Path)
579+
let op = build(
580+
"fn handler(State(state): State<AppState>) -> String",
581+
"/handler",
582+
None,
583+
);
584+
585+
// State is not a path extractor, and State params are typically ignored
586+
// line 85 returns false, so line 89 extends parameters (but State is usually filtered out)
587+
assert!(op.parameters.is_none() || op.parameters.as_ref().unwrap().is_empty());
588+
}
589+
590+
#[test]
591+
fn test_string_body_fallback() {
592+
// Test lines 100-107: String as last arg becomes text/plain body
593+
let op = build("fn upload(content: String) -> String", "/upload", None);
594+
595+
let body = op.request_body.as_ref().expect("request body expected");
596+
assert!(body.content.contains_key("text/plain"));
597+
let media = body.content.get("text/plain").unwrap();
598+
match media.schema.as_ref().unwrap() {
599+
SchemaRef::Inline(schema) => {
600+
assert_eq!(schema.schema_type, Some(SchemaType::String));
601+
}
602+
_ => panic!("expected inline schema"),
603+
}
604+
}
605+
606+
#[test]
607+
fn test_str_ref_body_fallback() {
608+
// Test lines 100-106: &str as last arg becomes text/plain body
609+
let op = build("fn upload(content: &str) -> String", "/upload", None);
610+
611+
let body = op.request_body.as_ref().expect("request body expected");
612+
assert!(body.content.contains_key("text/plain"));
613+
}
614+
615+
#[test]
616+
fn test_type_reference_with_string() {
617+
// Test lines 100-102, 104: Type::Reference branch - &String
618+
let op = build("fn upload(content: &String) -> String", "/upload", None);
619+
620+
// &String reference should be detected as string type
621+
// Line 101-102 checks if Type::Reference elem is a Path with String/str
622+
let body = op.request_body.as_ref().expect("request body expected");
623+
assert!(body.content.contains_key("text/plain"));
624+
}
625+
626+
#[test]
627+
fn test_non_string_last_arg_not_body() {
628+
// Test line 107: last arg that's NOT String/&str should NOT become body
629+
let op = build("fn process(count: i32) -> String", "/process", None);
630+
631+
// i32 is not String/&str, so line 107 returns false, no body created
632+
// However, bare i32 without extractor is also ignored
633+
assert!(op.request_body.is_none());
634+
}
635+
636+
#[test]
637+
fn test_multiple_path_params_with_single_type() {
638+
// Test line 57-60: multiple path params but single type - uses type for all
639+
let op = build(
640+
"fn get(Path(id): Path<String>) -> String",
641+
"/shops/{shop_id}/items/{item_id}",
642+
None,
643+
);
644+
645+
// Both params should use String type
646+
let params = op.parameters.as_ref().expect("parameters expected");
647+
assert_eq!(params.len(), 2);
648+
assert_eq!(param_schema_type(&params[0]), Some(SchemaType::String));
649+
assert_eq!(param_schema_type(&params[1]), Some(SchemaType::String));
650+
}
529651
}

0 commit comments

Comments
 (0)