Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ og-fixtures/*
rust-toolchain.toml
internal_todo.md
ISSUES.md
.tmp_tests/
convert_fixtures.sh
.DS_Store
188 changes: 176 additions & 12 deletions src/testing.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::gen;
use anyhow::{bail, Result};
use camino::{Utf8Path, Utf8PathBuf};
use camino_tempfile::tempdir;
use std::fs::{copy, create_dir_all, File};
use std::io::Write;
use std::process::Command;
Expand All @@ -15,15 +16,134 @@ pub struct CompileSource {
pub config_path: Option<Utf8PathBuf>,
}

/// Test execution options
#[derive(Debug, Clone)]
pub struct TestConfig {
/// Custom output directory for test files
pub custom_output_dir: Option<Utf8PathBuf>,
/// Preserve test files after completion
pub no_delete: bool,
/// Delay in seconds after test failure (0 = no delay; None = default)
pub failure_delay_secs: Option<u64>,
}

impl Default for TestConfig {
fn default() -> Self {
Self {
custom_output_dir: None,
no_delete: false,
failure_delay_secs: None,
}
}
}

impl TestConfig {
/// Build from environment variables
/// - UNIFFI_DART_TEST_DIR: custom output dir
/// - UNIFFI_DART_NO_DELETE: preserve files
/// - UNIFFI_DART_FAILURE_DELAY: failure delay (seconds)
pub fn from_env() -> Self {
let mut config = Self::default();

if let Ok(test_dir) = std::env::var("UNIFFI_DART_TEST_DIR") {
config.custom_output_dir = Some(Utf8PathBuf::from(test_dir));
}
if std::env::var("UNIFFI_DART_NO_DELETE").is_ok() {
config.no_delete = true;
}
if let Ok(delay_str) = std::env::var("UNIFFI_DART_FAILURE_DELAY") {
if let Ok(delay) = delay_str.parse::<u64>() {
config.failure_delay_secs = Some(delay);
}
}

config
}

pub fn with_no_delete(mut self, no_delete: bool) -> Self {
self.no_delete = no_delete;
self
}

pub fn with_output_dir<P: Into<Utf8PathBuf>>(mut self, dir: P) -> Self {
self.custom_output_dir = Some(dir.into());
self
}

pub fn with_failure_delay(mut self, delay_secs: u64) -> Self {
self.failure_delay_secs = Some(delay_secs);
self
}
}

/// Run a test with default options (env vars honored)
///
/// Env overrides:
/// - UNIFFI_DART_TEST_DIR
/// - UNIFFI_DART_NO_DELETE
/// - UNIFFI_DART_FAILURE_DELAY
pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Result<()> {
// Use .tmp_tests/ directory in project root for easier debugging
// Navigate to project root (fixtures are 2 levels deep: fixtures/fixture_name/)
let tmp_tests_dir = Utf8Path::new("../../.tmp_tests");
create_dir_all(&tmp_tests_dir)?;
run_test_with_config(fixture, udl_path, config_path, &TestConfig::from_env())
}

/// Run a test with explicit configuration
pub fn run_test_with_config(
fixture: &str,
udl_path: &str,
config_path: Option<&str>,
test_config: &TestConfig,
) -> Result<()> {
run_test_impl(fixture, udl_path, config_path, test_config)
}

/// Run a test with an explicit output directory (convenience wrapper)
pub fn run_test_with_output_dir(
fixture: &str,
udl_path: &str,
config_path: Option<&str>,
custom_output_dir: Option<&Utf8Path>,
) -> Result<()> {
let mut config = TestConfig::default();
config.custom_output_dir = custom_output_dir.map(|p| p.to_owned());
run_test_impl(fixture, udl_path, config_path, &config)
}

/// Test execution (core implementation)
fn run_test_impl(
fixture: &str,
udl_path: &str,
config_path: Option<&str>,
test_config: &TestConfig,
) -> Result<()> {
// Resolve project root (cargo may change CWD when running tests)
let project_root = find_project_root()?;

let script_path = Utf8Path::new(".").canonicalize_utf8()?;
let test_helper = UniFFITestHelper::new(fixture)?;
let out_dir = test_helper.create_out_dir(&tmp_tests_dir, &script_path)?;

// Function-scope guard to keep temp dir alive until function end
let mut _temp_guard: Option<_> = None;

// Resolve output dir: custom → temp (with optional preservation)
let out_dir = if let Some(custom_dir) = &test_config.custom_output_dir {
let resolved_dir = if custom_dir.is_absolute() {
custom_dir.clone()
} else {
project_root.join(custom_dir)
};
create_dir_all(&resolved_dir)?;
test_helper.create_out_dir(&resolved_dir, &script_path)?
} else {
let temp_dir = tempdir()?;
let out_dir = test_helper.create_out_dir(temp_dir.path(), &script_path)?;
if test_config.no_delete {
// Keep temp directory alive when no_delete is set
std::mem::forget(temp_dir);
} else {
_temp_guard = Some(temp_dir);
}
out_dir
};

let udl_path = Utf8Path::new(".").canonicalize_utf8()?.join(udl_path);
let config_path = if let Some(path) = config_path {
Expand All @@ -34,6 +154,10 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res

println!("{out_dir}");

if test_config.no_delete {
println!("Test files will be preserved after completion (no-delete mode)");
}

let mut pubspec = File::create(out_dir.join("pubspec.yaml"))?;
pubspec.write(
b"
Expand All @@ -59,8 +183,9 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res
config_path.as_deref(),
Some(&out_dir),
&test_helper.cdylib_path()?,
false, // library_mode
false,
)?;

// Copy fixture test files to output directory
let test_glob_pattern = "test/*.dart";
for file in glob::glob(test_glob_pattern)?.filter_map(Result::ok) {
Expand All @@ -72,35 +197,74 @@ pub fn run_test(fixture: &str, udl_path: &str, config_path: Option<&str>) -> Res
copy(&file, test_outdir.join(filename))?;
}

// Format the generated Dart code before running tests (best-effort)
// Best effort formatting
let mut format_command = Command::new("dart");
format_command.current_dir(&out_dir).arg("format").arg(".");
match format_command.spawn().and_then(|mut c| c.wait()) {
Ok(status) if status.success() => {}
Ok(_) | Err(_) => {
println!("WARNING: dart format unavailable or failed; continuing with tests anyway");
if std::env::var("CI").is_err() {
// skip in CI environment
thread::sleep(Duration::from_secs(1));
}
}
}

// Run the test script against compiled bindings
// Run tests
let mut command = Command::new("dart");
command.current_dir(&out_dir).arg("test");
let status = command.spawn()?.wait()?;
if !status.success() {
println!("FAILED");
if std::env::var("CI").is_err() {
// skip in CI environment
thread::sleep(Duration::from_secs(2));

// Optional delay after failure (skipped on CI)
let delay_secs = test_config.failure_delay_secs.unwrap_or(2);
if delay_secs > 0 && std::env::var("CI").is_err() {
println!("Waiting {} seconds before cleanup...", delay_secs);
thread::sleep(Duration::from_secs(delay_secs));
}

bail!("running `dart` to run test script failed ({:?})", command);
}
Ok(())
}

/// Locate the workspace root:
/// - CARGO_WORKSPACE_ROOT if set
/// - ascend until a Cargo.toml with [workspace]
fn find_project_root() -> Result<Utf8PathBuf> {
if let Ok(ws_root) = std::env::var("CARGO_WORKSPACE_ROOT") {
if let Some(p) = Utf8Path::from_path(std::path::Path::new(&ws_root)) {
return Ok(p.to_owned());
}
}

let mut current = std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?;

loop {
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
return Utf8Path::from_path(&current)
.ok_or_else(|| anyhow::anyhow!("Project root path is not valid UTF-8"))
.map(|p| p.to_owned());
}
}
}
if let Some(parent) = current.parent() {
current = parent.to_owned();
} else {
break;
}
}

Utf8Path::from_path(&std::env::current_dir()?)
.ok_or_else(|| anyhow::anyhow!("Current directory path is not valid UTF-8"))
.map(|p| p.to_owned())
}

pub fn get_compile_sources() -> Result<Vec<CompileSource>> {
todo!("Not implemented")
}
Loading