11use crate :: gen;
22use anyhow:: { bail, Result } ;
33use camino:: { Utf8Path , Utf8PathBuf } ;
4+ use camino_tempfile:: tempdir;
45use std:: fs:: { copy, create_dir_all, File } ;
56use std:: io:: Write ;
67use std:: process:: Command ;
@@ -15,15 +16,134 @@ pub struct CompileSource {
1516 pub config_path : Option < Utf8PathBuf > ,
1617}
1718
19+ /// Test execution options
20+ #[ derive( Debug , Clone ) ]
21+ pub struct TestConfig {
22+ /// Custom output directory for test files
23+ pub custom_output_dir : Option < Utf8PathBuf > ,
24+ /// Preserve test files after completion
25+ pub no_delete : bool ,
26+ /// Delay in seconds after test failure (0 = no delay; None = default)
27+ pub failure_delay_secs : Option < u64 > ,
28+ }
29+
30+ impl Default for TestConfig {
31+ fn default ( ) -> Self {
32+ Self {
33+ custom_output_dir : None ,
34+ no_delete : false ,
35+ failure_delay_secs : None ,
36+ }
37+ }
38+ }
39+
40+ impl TestConfig {
41+ /// Build from environment variables
42+ /// - UNIFFI_DART_TEST_DIR: custom output dir
43+ /// - UNIFFI_DART_NO_DELETE: preserve files
44+ /// - UNIFFI_DART_FAILURE_DELAY: failure delay (seconds)
45+ pub fn from_env ( ) -> Self {
46+ let mut config = Self :: default ( ) ;
47+
48+ if let Ok ( test_dir) = std:: env:: var ( "UNIFFI_DART_TEST_DIR" ) {
49+ config. custom_output_dir = Some ( Utf8PathBuf :: from ( test_dir) ) ;
50+ }
51+ if std:: env:: var ( "UNIFFI_DART_NO_DELETE" ) . is_ok ( ) {
52+ config. no_delete = true ;
53+ }
54+ if let Ok ( delay_str) = std:: env:: var ( "UNIFFI_DART_FAILURE_DELAY" ) {
55+ if let Ok ( delay) = delay_str. parse :: < u64 > ( ) {
56+ config. failure_delay_secs = Some ( delay) ;
57+ }
58+ }
59+
60+ config
61+ }
62+
63+ pub fn with_no_delete ( mut self , no_delete : bool ) -> Self {
64+ self . no_delete = no_delete;
65+ self
66+ }
67+
68+ pub fn with_output_dir < P : Into < Utf8PathBuf > > ( mut self , dir : P ) -> Self {
69+ self . custom_output_dir = Some ( dir. into ( ) ) ;
70+ self
71+ }
72+
73+ pub fn with_failure_delay ( mut self , delay_secs : u64 ) -> Self {
74+ self . failure_delay_secs = Some ( delay_secs) ;
75+ self
76+ }
77+ }
78+
79+ /// Run a test with default options (env vars honored)
80+ ///
81+ /// Env overrides:
82+ /// - UNIFFI_DART_TEST_DIR
83+ /// - UNIFFI_DART_NO_DELETE
84+ /// - UNIFFI_DART_FAILURE_DELAY
1885pub fn run_test ( fixture : & str , udl_path : & str , config_path : Option < & str > ) -> Result < ( ) > {
19- // Use .tmp_tests/ directory in project root for easier debugging
20- // Navigate to project root (fixtures are 2 levels deep: fixtures/fixture_name/)
21- let tmp_tests_dir = Utf8Path :: new ( "../../.tmp_tests" ) ;
22- create_dir_all ( & tmp_tests_dir) ?;
86+ run_test_with_config ( fixture, udl_path, config_path, & TestConfig :: from_env ( ) )
87+ }
88+
89+ /// Run a test with explicit configuration
90+ pub fn run_test_with_config (
91+ fixture : & str ,
92+ udl_path : & str ,
93+ config_path : Option < & str > ,
94+ test_config : & TestConfig ,
95+ ) -> Result < ( ) > {
96+ run_test_impl ( fixture, udl_path, config_path, test_config)
97+ }
98+
99+ /// Run a test with an explicit output directory (convenience wrapper)
100+ pub fn run_test_with_output_dir (
101+ fixture : & str ,
102+ udl_path : & str ,
103+ config_path : Option < & str > ,
104+ custom_output_dir : Option < & Utf8Path > ,
105+ ) -> Result < ( ) > {
106+ let mut config = TestConfig :: default ( ) ;
107+ config. custom_output_dir = custom_output_dir. map ( |p| p. to_owned ( ) ) ;
108+ run_test_impl ( fixture, udl_path, config_path, & config)
109+ }
110+
111+ /// Test execution (core implementation)
112+ fn run_test_impl (
113+ fixture : & str ,
114+ udl_path : & str ,
115+ config_path : Option < & str > ,
116+ test_config : & TestConfig ,
117+ ) -> Result < ( ) > {
118+ // Resolve project root (cargo may change CWD when running tests)
119+ let project_root = find_project_root ( ) ?;
23120
24121 let script_path = Utf8Path :: new ( "." ) . canonicalize_utf8 ( ) ?;
25122 let test_helper = UniFFITestHelper :: new ( fixture) ?;
26- let out_dir = test_helper. create_out_dir ( & tmp_tests_dir, & script_path) ?;
123+
124+ // Function-scope guard to keep temp dir alive until function end
125+ let mut _temp_guard: Option < _ > = None ;
126+
127+ // Resolve output dir: custom → temp (with optional preservation)
128+ let out_dir = if let Some ( custom_dir) = & test_config. custom_output_dir {
129+ let resolved_dir = if custom_dir. is_absolute ( ) {
130+ custom_dir. clone ( )
131+ } else {
132+ project_root. join ( custom_dir)
133+ } ;
134+ create_dir_all ( & resolved_dir) ?;
135+ test_helper. create_out_dir ( & resolved_dir, & script_path) ?
136+ } else {
137+ let temp_dir = tempdir ( ) ?;
138+ let out_dir = test_helper. create_out_dir ( temp_dir. path ( ) , & script_path) ?;
139+ if test_config. no_delete {
140+ // Keep temp directory alive when no_delete is set
141+ std:: mem:: forget ( temp_dir) ;
142+ } else {
143+ _temp_guard = Some ( temp_dir) ;
144+ }
145+ out_dir
146+ } ;
27147
28148 let udl_path = Utf8Path :: new ( "." ) . canonicalize_utf8 ( ) ?. join ( udl_path) ;
29149 let config_path = if let Some ( path) = config_path {
@@ -34,6 +154,10 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res
34154
35155 println ! ( "{out_dir}" ) ;
36156
157+ if test_config. no_delete {
158+ println ! ( "Test files will be preserved after completion (no-delete mode)" ) ;
159+ }
160+
37161 let mut pubspec = File :: create ( out_dir. join ( "pubspec.yaml" ) ) ?;
38162 pubspec. write (
39163 b"
@@ -59,8 +183,9 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res
59183 config_path. as_deref ( ) ,
60184 Some ( & out_dir) ,
61185 & test_helper. cdylib_path ( ) ?,
62- false , // library_mode
186+ false ,
63187 ) ?;
188+
64189 // Copy fixture test files to output directory
65190 let test_glob_pattern = "test/*.dart" ;
66191 for file in glob:: glob ( test_glob_pattern) ?. filter_map ( Result :: ok) {
@@ -72,35 +197,74 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res
72197 copy ( & file, test_outdir. join ( filename) ) ?;
73198 }
74199
75- // Format the generated Dart code before running tests (best- effort)
200+ // Best effort formatting
76201 let mut format_command = Command :: new ( "dart" ) ;
77202 format_command. current_dir ( & out_dir) . arg ( "format" ) . arg ( "." ) ;
78203 match format_command. spawn ( ) . and_then ( |mut c| c. wait ( ) ) {
79204 Ok ( status) if status. success ( ) => { }
80205 Ok ( _) | Err ( _) => {
81206 println ! ( "WARNING: dart format unavailable or failed; continuing with tests anyway" ) ;
82207 if std:: env:: var ( "CI" ) . is_err ( ) {
83- // skip in CI environment
84208 thread:: sleep ( Duration :: from_secs ( 1 ) ) ;
85209 }
86210 }
87211 }
88212
89- // Run the test script against compiled bindings
213+ // Run tests
90214 let mut command = Command :: new ( "dart" ) ;
91215 command. current_dir ( & out_dir) . arg ( "test" ) ;
92216 let status = command. spawn ( ) ?. wait ( ) ?;
93217 if !status. success ( ) {
94218 println ! ( "FAILED" ) ;
95- if std:: env:: var ( "CI" ) . is_err ( ) {
96- // skip in CI environment
97- thread:: sleep ( Duration :: from_secs ( 2 ) ) ;
219+
220+ // Optional delay after failure (skipped on CI)
221+ let delay_secs = test_config. failure_delay_secs . unwrap_or ( 2 ) ;
222+ if delay_secs > 0 && std:: env:: var ( "CI" ) . is_err ( ) {
223+ println ! ( "Waiting {} seconds before cleanup..." , delay_secs) ;
224+ thread:: sleep ( Duration :: from_secs ( delay_secs) ) ;
98225 }
226+
99227 bail ! ( "running `dart` to run test script failed ({:?})" , command) ;
100228 }
101229 Ok ( ( ) )
102230}
103231
232+ /// Locate the workspace root:
233+ /// - CARGO_WORKSPACE_ROOT if set
234+ /// - ascend until a Cargo.toml with [workspace]
235+ fn find_project_root ( ) -> Result < Utf8PathBuf > {
236+ if let Ok ( ws_root) = std:: env:: var ( "CARGO_WORKSPACE_ROOT" ) {
237+ if let Some ( p) = Utf8Path :: from_path ( std:: path:: Path :: new ( & ws_root) ) {
238+ return Ok ( p. to_owned ( ) ) ;
239+ }
240+ }
241+
242+ let mut current = std:: env:: current_dir ( )
243+ . map_err ( |e| anyhow:: anyhow!( "Failed to get current directory: {}" , e) ) ?;
244+
245+ loop {
246+ let cargo_toml = current. join ( "Cargo.toml" ) ;
247+ if cargo_toml. exists ( ) {
248+ if let Ok ( content) = std:: fs:: read_to_string ( & cargo_toml) {
249+ if content. contains ( "[workspace]" ) {
250+ return Utf8Path :: from_path ( & current)
251+ . ok_or_else ( || anyhow:: anyhow!( "Project root path is not valid UTF-8" ) )
252+ . map ( |p| p. to_owned ( ) ) ;
253+ }
254+ }
255+ }
256+ if let Some ( parent) = current. parent ( ) {
257+ current = parent. to_owned ( ) ;
258+ } else {
259+ break ;
260+ }
261+ }
262+
263+ Utf8Path :: from_path ( & std:: env:: current_dir ( ) ?)
264+ . ok_or_else ( || anyhow:: anyhow!( "Current directory path is not valid UTF-8" ) )
265+ . map ( |p| p. to_owned ( ) )
266+ }
267+
104268pub fn get_compile_sources ( ) -> Result < Vec < CompileSource > > {
105269 todo ! ( "Not implemented" )
106270}
0 commit comments