Skip to content

Commit d50bf11

Browse files
committed
Add testcase
1 parent f6a4fef commit d50bf11

5 files changed

Lines changed: 267 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vespera_macro/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ anyhow = "1.0"
2323
rstest = "0.26"
2424
insta = "1.46"
2525
tempfile = "3"
26+
serial_test = "3"

crates/vespera_macro/src/schema_macro/inline_types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ pub fn generate_inline_type_definition(inline_type: &InlineRelationType) -> Toke
259259
#[cfg(test)]
260260
mod tests {
261261
use super::*;
262+
use serial_test::serial;
262263

263264
#[test]
264265
fn test_generate_inline_type_definition() {
@@ -796,6 +797,7 @@ mod tests {
796797
// These require setting up a temp directory with model files
797798

798799
#[test]
800+
#[serial]
799801
fn test_generate_inline_relation_type_with_file_lookup() {
800802
use tempfile::TempDir;
801803

@@ -865,6 +867,7 @@ pub struct Model {
865867
}
866868

867869
#[test]
870+
#[serial]
868871
fn test_generate_inline_relation_type_no_relations_with_file_lookup() {
869872
use tempfile::TempDir;
870873

@@ -930,6 +933,7 @@ pub struct Model {
930933
}
931934

932935
#[test]
936+
#[serial]
933937
fn test_generate_inline_relation_type_file_not_found() {
934938
use tempfile::TempDir;
935939

@@ -971,6 +975,7 @@ pub struct Model {
971975
}
972976

973977
#[test]
978+
#[serial]
974979
fn test_generate_inline_relation_type_no_relations_file_not_found() {
975980
use tempfile::TempDir;
976981

crates/vespera_macro/src/schema_macro/mod.rs

Lines changed: 196 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,10 @@ pub fn generate_schema_type_code(
9999
find_struct_from_path(&input.source_type, schema_name_hint)
100100
{
101101
struct_def_owned = found;
102-
// Use the module path from the file lookup if the extracted one is empty
103-
if source_module_path.is_empty() {
104-
source_module_path = module_path;
105-
}
102+
// Always use the module path from file lookup for qualified paths
103+
// The file lookup derives module path from actual file location, which is more accurate
104+
// for resolving relative paths like `super::user::Entity`
105+
source_module_path = module_path;
106106
&struct_def_owned
107107
} else if let Some(found) = schema_storage.iter().find(|s| s.name == source_type_name) {
108108
found
@@ -637,6 +637,7 @@ pub fn generate_schema_type_code(
637637
#[cfg(test)]
638638
mod tests {
639639
use super::*;
640+
use serial_test::serial;
640641

641642
fn create_test_struct_metadata(name: &str, definition: &str) -> StructMetadata {
642643
StructMetadata::new(name.to_string(), definition.to_string())
@@ -1450,6 +1451,7 @@ mod tests {
14501451
}
14511452

14521453
#[test]
1454+
#[serial]
14531455
fn test_generate_schema_type_code_qualified_path_file_lookup_success() {
14541456
// Coverage for lines 76, 78-79, 81
14551457
// Tests: qualified path found via file lookup, module_path used when source is empty
@@ -1502,6 +1504,7 @@ pub struct Model {
15021504
}
15031505

15041506
#[test]
1507+
#[serial]
15051508
fn test_generate_schema_type_code_simple_name_file_lookup_fallback() {
15061509
// Coverage for lines 100, 103-104
15071510
// Tests: simple name (not in storage) found via file lookup with schema_name hint
@@ -1560,6 +1563,7 @@ pub struct Model {
15601563
// ============================================================
15611564

15621565
#[test]
1566+
#[serial]
15631567
fn test_generate_schema_type_code_has_many_explicit_pick_inline_type() {
15641568
// Coverage for lines 258-260, 262-263, 265, 267-268
15651569
// Tests: HasMany is explicitly picked, inline type is generated
@@ -1625,6 +1629,7 @@ pub struct Model {
16251629
}
16261630

16271631
#[test]
1632+
#[serial]
16281633
fn test_generate_schema_type_code_has_many_explicit_pick_file_not_found() {
16291634
// Coverage for line 270
16301635
// Tests: HasMany is explicitly picked but target file not found - should skip field
@@ -1684,6 +1689,7 @@ pub struct Model {
16841689
// ============================================================
16851690

16861691
#[test]
1692+
#[serial]
16871693
fn test_generate_schema_type_code_belongs_to_circular_inline_optional() {
16881694
// Coverage for lines 277-278, 281-282, 285, 288-289, 294
16891695
// Tests: BelongsTo with circular reference, optional field (is_optional = true)
@@ -1750,6 +1756,7 @@ pub struct Model {
17501756
}
17511757

17521758
#[test]
1759+
#[serial]
17531760
fn test_generate_schema_type_code_has_one_circular_inline_required() {
17541761
// Coverage for lines 277-278, 281-282, 285, 291, 294
17551762
// Tests: HasOne with circular reference, required field (is_optional = false)
@@ -1818,6 +1825,191 @@ pub struct Model {
18181825
}
18191826

18201827
#[test]
1828+
#[serial]
1829+
fn test_generate_schema_type_code_belongs_to_circular_inline_required_file() {
1830+
// Coverage for line 291 specifically
1831+
// Tests: BelongsTo with circular reference AND required FK (is_optional = false)
1832+
// This requires file-based lookup with:
1833+
// 1. #[sea_orm(from = "required_fk")] where required_fk is NOT Option<T>
1834+
// 2. Circular reference between two models
1835+
use tempfile::TempDir;
1836+
1837+
let temp_dir = TempDir::new().unwrap();
1838+
let src_dir = temp_dir.path().join("src");
1839+
let models_dir = src_dir.join("models");
1840+
std::fs::create_dir_all(&models_dir).unwrap();
1841+
1842+
// Create user.rs with Model that references memo (circular)
1843+
let user_model = r#"
1844+
#[sea_orm(table_name = "users")]
1845+
pub struct Model {
1846+
pub id: i32,
1847+
pub name: String,
1848+
pub memo_id: i32,
1849+
#[sea_orm(belongs_to, from = "memo_id", to = "id")]
1850+
pub memo: BelongsTo<super::memo::Entity>,
1851+
}
1852+
"#;
1853+
std::fs::write(models_dir.join("user.rs"), user_model).unwrap();
1854+
1855+
// Create memo.rs with Model that references user (completing the circle)
1856+
// Note: using flag-style `belongs_to` with `from = "user_id"`
1857+
let memo_model = r#"
1858+
#[sea_orm(table_name = "memos")]
1859+
pub struct Model {
1860+
pub id: i32,
1861+
pub title: String,
1862+
pub user_id: i32,
1863+
#[sea_orm(belongs_to, from = "user_id", to = "id")]
1864+
pub user: BelongsTo<super::user::Entity>,
1865+
}
1866+
"#;
1867+
std::fs::write(models_dir.join("memo.rs"), memo_model).unwrap();
1868+
1869+
// Save original CARGO_MANIFEST_DIR
1870+
let original_manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok();
1871+
// SAFETY: This is a test that runs single-threaded
1872+
unsafe { std::env::set_var("CARGO_MANIFEST_DIR", temp_dir.path()) };
1873+
1874+
// Generate schema from memo - has BelongsTo user which has circular ref back
1875+
// The user_id field is required (not Option), so is_optional = false
1876+
// This should generate Box<...> instead of Option<Box<...>>
1877+
let tokens = quote!(MemoSchema from crate::models::memo::Model);
1878+
let input: SchemaTypeInput = syn::parse2(tokens).unwrap();
1879+
let storage: Vec<StructMetadata> = vec![];
1880+
1881+
let result = generate_schema_type_code(&input, &storage);
1882+
1883+
// Restore CARGO_MANIFEST_DIR
1884+
// SAFETY: This is a test that runs single-threaded
1885+
unsafe {
1886+
if let Some(dir) = original_manifest_dir {
1887+
std::env::set_var("CARGO_MANIFEST_DIR", dir);
1888+
} else {
1889+
std::env::remove_var("CARGO_MANIFEST_DIR");
1890+
}
1891+
}
1892+
1893+
assert!(result.is_ok(), "Should generate schema: {:?}", result.err());
1894+
let (tokens, _metadata) = result.unwrap();
1895+
let output = tokens.to_string();
1896+
// Should have inline type definition for circular relation
1897+
assert!(
1898+
output.contains("MemoSchema"),
1899+
"Should contain MemoSchema: {}",
1900+
output
1901+
);
1902+
assert!(
1903+
output.contains("user"),
1904+
"Should contain user field: {}",
1905+
output
1906+
);
1907+
// BelongsTo with required FK (user_id: i32) should generate Box<...> not Option<Box<...>>
1908+
// This hits line 291: quote! { Box<#inline_type_name> }
1909+
assert!(
1910+
output.contains("pub user : Box <"),
1911+
"BelongsTo with required FK should generate Box<>, not Option<Box<>>. Output: {}",
1912+
output
1913+
);
1914+
}
1915+
1916+
#[test]
1917+
fn test_seaorm_relation_required_fk_directly() {
1918+
// Test the convert_relation_type_to_schema_with_info function directly
1919+
// to verify is_optional = false when FK is required
1920+
use crate::schema_macro::seaorm::{
1921+
convert_relation_type_to_schema_with_info, extract_belongs_to_from_field,
1922+
is_field_optional_in_struct,
1923+
};
1924+
1925+
// Use the same attribute format that works in seaorm tests: belongs_to (flag), not belongs_to = "..."
1926+
let struct_def = r#"
1927+
#[sea_orm(table_name = "memos")]
1928+
pub struct Model {
1929+
pub id: i32,
1930+
pub user_id: i32,
1931+
#[sea_orm(belongs_to, from = "user_id", to = "id")]
1932+
pub user: BelongsTo<super::user::Entity>,
1933+
}
1934+
"#;
1935+
let parsed_struct: syn::ItemStruct = syn::parse_str(struct_def).unwrap();
1936+
1937+
// Get the user field
1938+
let fields_named = match &parsed_struct.fields {
1939+
syn::Fields::Named(f) => f,
1940+
_ => panic!("Expected named fields"),
1941+
};
1942+
1943+
let user_field = fields_named
1944+
.named
1945+
.iter()
1946+
.find(|f| f.ident.as_ref().map(|i| i == "user").unwrap_or(false))
1947+
.expect("user field not found");
1948+
1949+
// Debug: Check if extract_belongs_to_from_field works
1950+
let fk_field = extract_belongs_to_from_field(&user_field.attrs);
1951+
assert_eq!(
1952+
fk_field,
1953+
Some("user_id".to_string()),
1954+
"Should extract FK field from attribute"
1955+
);
1956+
1957+
// Debug: Check if is_field_optional_in_struct works
1958+
let is_fk_optional = is_field_optional_in_struct(&parsed_struct, "user_id");
1959+
assert!(!is_fk_optional, "user_id: i32 should not be optional");
1960+
1961+
let result = convert_relation_type_to_schema_with_info(
1962+
&user_field.ty,
1963+
&user_field.attrs,
1964+
&parsed_struct,
1965+
&[
1966+
"crate".to_string(),
1967+
"models".to_string(),
1968+
"memo".to_string(),
1969+
],
1970+
user_field.ident.clone().unwrap(),
1971+
);
1972+
1973+
assert!(result.is_some(), "Should convert BelongsTo relation");
1974+
let (_, rel_info) = result.unwrap();
1975+
assert_eq!(rel_info.relation_type, "BelongsTo");
1976+
// The FK field user_id is i32 (not Option), so is_optional should be false
1977+
assert!(
1978+
!rel_info.is_optional,
1979+
"BelongsTo with required FK (user_id: i32) should have is_optional = false"
1980+
);
1981+
}
1982+
1983+
#[test]
1984+
fn test_extract_belongs_to_from_field_with_equals_value() {
1985+
// Test that extract_belongs_to_from_field works with belongs_to = "..." format
1986+
use crate::schema_macro::seaorm::extract_belongs_to_from_field;
1987+
1988+
// Format 1: belongs_to (flag style) - known to work
1989+
let attrs1: Vec<syn::Attribute> = vec![syn::parse_quote!(
1990+
#[sea_orm(belongs_to, from = "user_id", to = "id")]
1991+
)];
1992+
let result1 = extract_belongs_to_from_field(&attrs1);
1993+
assert_eq!(
1994+
result1,
1995+
Some("user_id".to_string()),
1996+
"Flag style should work"
1997+
);
1998+
1999+
// Format 2: belongs_to = "..." (value style) - testing this
2000+
let attrs2: Vec<syn::Attribute> = vec![syn::parse_quote!(
2001+
#[sea_orm(belongs_to = "super::user::Entity", from = "user_id", to = "id")]
2002+
)];
2003+
let result2 = extract_belongs_to_from_field(&attrs2);
2004+
assert_eq!(
2005+
result2,
2006+
Some("user_id".to_string()),
2007+
"Value style should also work"
2008+
);
2009+
}
2010+
2011+
#[test]
2012+
#[serial]
18212013
fn test_generate_schema_type_code_qualified_path_with_nonempty_module_path() {
18222014
// Coverage for line 78 (the else branch where source_module_path is NOT empty)
18232015
// Tests: qualified path with explicit module segments that are not empty

0 commit comments

Comments
 (0)