Skip to content

Commit c1e8fc9

Browse files
committed
feat(testing): default to temporary directories with configurable output options
- Use camino-tempfile::tempdir by default with automatic cleanup - Add TestConfig for full control over test execution - Support env overrides: UNIFFI_DART_TEST_DIR, UNIFFI_DART_NO_DELETE, UNIFFI_DART_FAILURE_DELAY - Resolve relative custom dirs against workspace root - Preserve temp files when no-delete is set (uses std::mem::forget) - Remove .tmp_tests references and folder - temp dirs are cleaner default Backwards compatible: existing run_test() continues to work.
1 parent 76dda88 commit c1e8fc9

File tree

5 files changed

+190
-16
lines changed

5 files changed

+190
-16
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ og-fixtures/*
1010
rust-toolchain.toml
1111
internal_todo.md
1212
ISSUES.md
13-
.tmp_tests/
1413
convert_fixtures.sh
1514
.DS_Store
1615
ISSUES.md

src/gen/compounds.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@ pub struct MapCodeType {
194194

195195
impl MapCodeType {
196196
pub fn new(self_type: Type, key: Type, value: Type) -> Self {
197-
Self { self_type, key, value }
197+
Self {
198+
self_type,
199+
key,
200+
value,
201+
}
198202
}
199203

200204
fn key(&self) -> &Type {

src/gen/oracle.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,11 @@ impl<T: AsType> AsCodeType for T {
688688
self.as_type(),
689689
*inner_type,
690690
)),
691-
Type::Map { key_type, value_type, .. } => Box::new(compounds::MapCodeType::new(
691+
Type::Map {
692+
key_type,
693+
value_type,
694+
..
695+
} => Box::new(compounds::MapCodeType::new(
692696
self.as_type(),
693697
*key_type,
694698
*value_type,

src/gen/render/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ impl<T: AsType> AsRenderable for T {
9494
self.as_type(),
9595
*inner_type,
9696
)),
97-
Type::Map { key_type, value_type } => Box::new(compounds::MapCodeType::new(
97+
Type::Map {
98+
key_type,
99+
value_type,
100+
} => Box::new(compounds::MapCodeType::new(
98101
self.as_type(),
99102
*key_type,
100103
*value_type,

src/testing.rs

Lines changed: 176 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::gen;
22
use anyhow::{bail, Result};
33
use camino::{Utf8Path, Utf8PathBuf};
4+
use camino_tempfile::tempdir;
45
use std::fs::{copy, create_dir_all, File};
56
use std::io::Write;
67
use 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
1885
pub 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+
104268
pub fn get_compile_sources() -> Result<Vec<CompileSource>> {
105269
todo!("Not implemented")
106270
}

0 commit comments

Comments
 (0)