future_form_ffi 0.1.0

FFI support for future_form: host-driven polling, opaque handles, and effect slots
Documentation
//! Black-box end-to-end tests: same Counter trait, multiple execution models.
//!
//! ## Basic counter (no effects)
//! - `tokio_host_e2e` — always runs, no extra dependencies
//! - `go_ffi_e2e` — requires `go` on `$PATH` (use `nix develop .#ffi`)
//! - `java_ffi_e2e` — requires `java` + `javac` on `$PATH` (use `nix develop .#ffi`)
//! - `python_ffi_e2e` — requires `python3` on `$PATH` (use `nix develop .#ffi`)
//!
//! ## Effect-handling counter (sans-IO with host-fulfilled effects)
//! - `go_effects_e2e` — requires `go`
//! - `java_effects_e2e` — requires `java` + `javac`
//! - `python_effects_e2e` — requires `python3`
//!
//! ## Key-value store (per-future `Arc<EffectSlot>`, concurrent operations)
//! - `go_kv_e2e` — requires `go`
//! - `java_kv_e2e` — requires `java` + `javac`
//! - `python_kv_e2e` — requires `python3`
//!
//! Tests requiring foreign toolchains are marked `#[ignore]` so that
//! `cargo test --workspace` correctly reports them as _ignored_ rather
//! than silently passing. The FFI CI pipeline runs them with
//! `--include-ignored` inside `nix develop .#ffi`.

use std::{
    path::{Path, PathBuf},
    process::{Command, Output},
};

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn examples_dir() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("examples")
}

fn counter_simple_dir() -> PathBuf {
    examples_dir().join("counter_simple")
}

fn counter_effects_dir() -> PathBuf {
    examples_dir().join("counter_effects")
}

fn kv_store_dir() -> PathBuf {
    examples_dir().join("key_value_store")
}

fn set_lib_path(cmd: &mut Command, lib_dir: &Path) {
    if cfg!(target_os = "macos") {
        cmd.env("DYLD_LIBRARY_PATH", lib_dir);
    } else {
        cmd.env("LD_LIBRARY_PATH", lib_dir);
    }
}

fn build_rust_bridge() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let rust_bridge = counter_simple_dir().join("rust_bridge");

    let build = Command::new("cargo")
        .args(["build", "--manifest-path"])
        .arg(rust_bridge.join("Cargo.toml"))
        .output()?;

    assert!(
        build.status.success(),
        "rust_bridge build failed:\n{}",
        String::from_utf8_lossy(&build.stderr)
    );

    Ok(rust_bridge.join("target/debug"))
}

fn assert_all_passed(output: &Output, label: &str) {
    assert_passed(output, label, 4);
}

fn assert_passed(output: &Output, label: &str, expected: u32) {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        output.status.success(),
        "{label} failed (exit {}):\nstdout:\n{stdout}\nstderr:\n{stderr}",
        output.status,
    );

    let expected_str = format!("{expected} passed, 0 failed");
    assert!(
        stdout.contains(&expected_str),
        "not all {label} tests passed (expected \"{expected_str}\"):\n{stdout}"
    );
}

#[test]
fn tokio_host_e2e() -> TestResult {
    println!("--- example: counter_simple / tokio_host ---");
    let tokio_dir = counter_simple_dir().join("tokio_host");

    let output = Command::new("cargo")
        .args(["run", "--manifest-path"])
        .arg(tokio_dir.join("Cargo.toml"))
        .output()?;

    assert_all_passed(&output, "tokio_host");
    Ok(())
}

#[test]
#[ignore = "requires `go` — run with `nix develop .#ffi` or `--include-ignored`"]
fn go_ffi_e2e() -> TestResult {
    println!("--- example: counter_simple / go_host ---");
    let lib_dir = build_rust_bridge()?;

    let mut go_cmd = Command::new("go");
    go_cmd
        .args(["run", "."])
        .current_dir(counter_simple_dir().join("go_host"));
    set_lib_path(&mut go_cmd, &lib_dir);

    let output = go_cmd.output()?;
    assert_all_passed(&output, "Go FFI");
    Ok(())
}

#[test]
#[ignore = "requires `javac` — run with `nix develop .#ffi` or `--include-ignored`"]
fn java_ffi_e2e() -> TestResult {
    println!("--- example: counter_simple / java_host ---");
    let lib_dir = build_rust_bridge()?;
    let java_dir = counter_simple_dir().join("java_host");

    let compile = Command::new("javac")
        .arg(java_dir.join("CounterHost.java"))
        .arg("-d")
        .arg(java_dir.join("out"))
        .output()?;

    assert!(
        compile.status.success(),
        "javac failed:\n{}",
        String::from_utf8_lossy(&compile.stderr)
    );

    let mut java_cmd = Command::new("java");
    java_cmd
        .arg("-cp")
        .arg(java_dir.join("out"))
        .arg(format!("-Djava.library.path={}", lib_dir.display()))
        .arg("CounterHost");
    set_lib_path(&mut java_cmd, &lib_dir);

    let output = java_cmd.output()?;
    assert_all_passed(&output, "Java FFI");
    Ok(())
}

#[test]
#[ignore = "requires `python3` — run with `nix develop .#ffi` or `--include-ignored`"]
fn python_ffi_e2e() -> TestResult {
    println!("--- example: counter_simple / python_host ---");
    build_rust_bridge()?;

    let output = Command::new("python3")
        .arg(counter_simple_dir().join("python_host/counter_host.py"))
        .output()?;

    assert_all_passed(&output, "Python FFI");
    Ok(())
}

// -----------------------------------------------------------------------
// Effect-handling counter (sans-IO with host-fulfilled effects)
// -----------------------------------------------------------------------

fn build_effects_bridge() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let effects_bridge = counter_effects_dir().join("effects_bridge");

    let build = Command::new("cargo")
        .args(["build", "--manifest-path"])
        .arg(effects_bridge.join("Cargo.toml"))
        .output()?;

    assert!(
        build.status.success(),
        "effects_bridge build failed:\n{}",
        String::from_utf8_lossy(&build.stderr)
    );

    Ok(effects_bridge.join("target/debug"))
}

#[test]
#[ignore = "requires `go` — run with `nix develop .#ffi` or `--include-ignored`"]
fn go_effects_e2e() -> TestResult {
    println!("--- example: counter_effects / go_host ---");
    let lib_dir = build_effects_bridge()?;

    let mut go_cmd = Command::new("go");
    go_cmd
        .args(["run", "."])
        .current_dir(counter_effects_dir().join("go_host"));
    set_lib_path(&mut go_cmd, &lib_dir);

    let output = go_cmd.output()?;
    assert_passed(&output, "Go effects FFI", 6);
    Ok(())
}

#[test]
#[ignore = "requires `javac` — run with `nix develop .#ffi` or `--include-ignored`"]
fn java_effects_e2e() -> TestResult {
    println!("--- example: counter_effects / java_host ---");
    let lib_dir = build_effects_bridge()?;
    let java_dir = counter_effects_dir().join("java_host");

    let compile = Command::new("javac")
        .arg(java_dir.join("EffectsHost.java"))
        .arg("-d")
        .arg(java_dir.join("out"))
        .output()?;

    assert!(
        compile.status.success(),
        "javac failed:\n{}",
        String::from_utf8_lossy(&compile.stderr)
    );

    let mut java_cmd = Command::new("java");
    java_cmd
        .arg("-cp")
        .arg(java_dir.join("out"))
        .arg(format!("-Djava.library.path={}", lib_dir.display()))
        .arg("EffectsHost");
    set_lib_path(&mut java_cmd, &lib_dir);

    let output = java_cmd.output()?;
    assert_passed(&output, "Java effects FFI", 6);
    Ok(())
}

#[test]
#[ignore = "requires `python3` — run with `nix develop .#ffi` or `--include-ignored`"]
fn python_effects_e2e() -> TestResult {
    println!("--- example: counter_effects / python_host ---");
    build_effects_bridge()?;

    let output = Command::new("python3")
        .arg(counter_effects_dir().join("python_host/effects_host.py"))
        .output()?;

    assert_passed(&output, "Python effects FFI", 6);
    Ok(())
}

// -----------------------------------------------------------------------
// Key-value store (per-future Arc<EffectSlot>, concurrent operations)
// -----------------------------------------------------------------------

fn build_kv_bridge() -> Result<PathBuf, Box<dyn std::error::Error>> {
    let kv_bridge = kv_store_dir().join("kv_bridge");

    let build = Command::new("cargo")
        .args(["build", "--manifest-path"])
        .arg(kv_bridge.join("Cargo.toml"))
        .output()?;

    assert!(
        build.status.success(),
        "kv_bridge build failed:\n{}",
        String::from_utf8_lossy(&build.stderr)
    );

    Ok(kv_bridge.join("target/debug"))
}

#[test]
#[ignore = "requires `go` — run with `nix develop .#ffi` or `--include-ignored`"]
fn go_kv_e2e() -> TestResult {
    println!("--- example: key_value_store / go_host ---");
    let lib_dir = build_kv_bridge()?;

    let mut go_cmd = Command::new("go");
    go_cmd
        .args(["run", "."])
        .current_dir(kv_store_dir().join("go_host"));
    set_lib_path(&mut go_cmd, &lib_dir);

    let output = go_cmd.output()?;
    assert_passed(&output, "Go KV FFI", 7);
    Ok(())
}

#[test]
#[ignore = "requires `javac` — run with `nix develop .#ffi` or `--include-ignored`"]
fn java_kv_e2e() -> TestResult {
    println!("--- example: key_value_store / java_host ---");
    let lib_dir = build_kv_bridge()?;
    let java_dir = kv_store_dir().join("java_host");

    let compile = Command::new("javac")
        .arg(java_dir.join("KvHost.java"))
        .arg("-d")
        .arg(java_dir.join("out"))
        .output()?;

    assert!(
        compile.status.success(),
        "javac failed:\n{}",
        String::from_utf8_lossy(&compile.stderr)
    );

    let mut java_cmd = Command::new("java");
    java_cmd
        .arg("-cp")
        .arg(java_dir.join("out"))
        .arg(format!("-Djava.library.path={}", lib_dir.display()))
        .arg("KvHost");
    set_lib_path(&mut java_cmd, &lib_dir);

    let output = java_cmd.output()?;
    assert_passed(&output, "Java KV FFI", 7);
    Ok(())
}

#[test]
#[ignore = "requires `python3` — run with `nix develop .#ffi` or `--include-ignored`"]
fn python_kv_e2e() -> TestResult {
    println!("--- example: key_value_store / python_host ---");
    build_kv_bridge()?;

    let output = Command::new("python3")
        .arg(kv_store_dir().join("python_host/kv_host.py"))
        .output()?;

    assert_passed(&output, "Python KV FFI", 7);
    Ok(())
}