Remove Rust test

This commit is contained in:
Mario Zechner 2025-10-29 12:26:21 +01:00
parent 9efea584a0
commit 4cdff21b54
6 changed files with 0 additions and 754 deletions

View File

@ -1,296 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "bindgen"
version = "0.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cc"
version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libloading"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "prettyplease"
version = "0.2.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "spine-c-wasm-test"
version = "0.1.0"
dependencies = [
"bindgen",
"cc",
]
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"

View File

@ -1,13 +0,0 @@
[package]
name = "spine-c-wasm-test"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
[build-dependencies]
cc = "1.0"
bindgen = "0.70"

View File

@ -1,43 +0,0 @@
# Spine-C Rust WASM Test
A minimal test demonstrating Rust FFI with spine-c, proving the no-cpprt workflow for WASM compilation.
## What This Does
This test:
1. Loads a Spine atlas with texture callbacks (`spine_atlas_load_callback`)
2. Loads binary skeleton data (`spine_skeleton_data_load_binary`)
3. Creates a skeleton instance (`spine_skeleton_create`)
4. Runs basic skeleton operations (`spine_skeleton_setup_pose`, `spine_skeleton_update_world_transform_1`)
5. Reads skeleton position (`spine_skeleton_get_x/y`)
6. Cleans up all resources including atlas disposal with texture callbacks
## Build Process
The build script compiles:
- spine-cpp with no-cpprt variant (eliminates C++ standard library)
- spine-c wrapper (provides C-compatible FFI)
- Generates complete Rust FFI bindings via bindgen from spine-c.h
- Links everything into a single Rust executable/library
**Full API Coverage**: Bindgen automatically generates Rust bindings for the entire spine-c API, exposing all public functions without manual FFI maintenance.
spine-c/spine-cpp only rely on libc for `malloc`, `free` and various math functions, which can be easily stubbed on any target platform.
This proves Rust projects can use Spine without C++ stdlib dependencies, enabling WASM compilation via Rust toolchain instead of Emscripten.
## Files
- `src/lib.rs` - Rust FFI test calling spine-c functions via bindgen-generated bindings
- `build.rs` - Compiles spine-cpp-no-cpprt + spine-c via cc crate, generates FFI bindings with bindgen
- Test data: `../../../examples/spineboy/export/spineboy-*` (atlas, skeleton, texture)
## Usage
```bash
cargo test -- --nocapture # Run test with debug output
cargo build # Build native
cargo build --target wasm32-unknown-unknown # Build WASM
```
**Status**: Fully working. Test executable: 1.8 MB. All spine-c functionality operational including atlas disposal.

View File

@ -1,100 +0,0 @@
use std::env;
use std::path::PathBuf;
fn main() {
let target = env::var("TARGET").unwrap();
let is_wasm = target.starts_with("wasm32");
// Build spine-cpp with no-cpprt variant
let spine_cpp_dir = PathBuf::from("../../../spine-cpp");
let spine_c_dir = PathBuf::from("../..");
let mut cpp_build = cc::Build::new();
cpp_build
.cpp(true)
.include(spine_cpp_dir.join("include"))
.include(spine_c_dir.join("include"))
.include(spine_c_dir.join("src"))
.flag("-std=c++11");
// Always avoid C++ runtime (consistent with no-cpprt approach)
cpp_build
.flag("-fno-exceptions")
.flag("-fno-rtti");
// Always avoid C++ runtime (consistent with no-cpprt approach)
cpp_build.flag("-nostdlib++");
// Tell cc crate linker to not link libc++
cpp_build.flag_if_supported("-Wl,-undefined,dynamic_lookup");
cpp_build.cpp_link_stdlib(None);
if is_wasm {
// For WASM, we may need additional setup, but let's first try without extra flags
// The target is already handled by cc-rs when building for wasm32-unknown-unknown
}
// Add spine-cpp source files (no-cpprt variant = all sources + no-cpprt.cpp)
let spine_cpp_src = spine_cpp_dir.join("src");
for entry in std::fs::read_dir(&spine_cpp_src).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "cpp") {
cpp_build.file(&path);
}
}
// Add spine-cpp subdirectories
for subdir in &["spine", "spine-c", "utils"] {
let subdir_path = spine_cpp_src.join(subdir);
if subdir_path.exists() {
for entry in std::fs::read_dir(&subdir_path).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "cpp") {
cpp_build.file(&path);
}
}
}
}
// Add spine-c generated sources
let spine_c_generated = spine_c_dir.join("src/generated");
for entry in std::fs::read_dir(&spine_c_generated).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "cpp") {
cpp_build.file(&path);
}
}
// Add spine-c extensions
cpp_build.file(spine_c_dir.join("src/extensions.cpp"));
// Add no-cpprt.cpp for C++ runtime stubs
cpp_build.file(spine_cpp_dir.join("src/no-cpprt.cpp"));
cpp_build.compile("spine");
// Generate bindings with bindgen
let bindings = bindgen::Builder::default()
.header("../../include/spine-c.h")
.clang_arg("-I../../include")
.clang_arg("-I../../../spine-cpp/include")
.clang_arg("-I../../src")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = std::path::PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Couldn't write bindings!");
// Link libraries - no C++ stdlib since we're using no-cpprt variant
// The no-cpprt.cpp provides minimal runtime stubs
println!("cargo:rerun-if-changed=../../spine-cpp/src");
println!("cargo:rerun-if-changed=../../src");
println!("cargo:rerun-if-changed=../../include/spine-c.h");
}

View File

@ -1,185 +0,0 @@
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_float, c_int, c_void};
// Include the generated bindings
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
// Headless texture loader functions with debug prints
extern "C" fn headless_texture_loader(path: *const c_char) -> *mut c_void {
unsafe {
let path_str = if !path.is_null() {
CStr::from_ptr(path).to_string_lossy()
} else {
"NULL".into()
};
println!("DEBUG: texture_loader called with path: {}", path_str);
let ptr = std::alloc::alloc(std::alloc::Layout::from_size_align(8, 8).unwrap());
println!("DEBUG: texture_loader returning: {:?}", ptr);
ptr as *mut c_void
}
}
extern "C" fn headless_texture_unloader(texture: *mut c_void) -> () {
println!("DEBUG: texture_unloader called with texture: {:?}", texture);
if !texture.is_null() && texture as usize > 1 {
unsafe {
println!("DEBUG: deallocating texture: {:?}", texture);
std::alloc::dealloc(texture as *mut u8, std::alloc::Layout::from_size_align(8, 8).unwrap());
println!("DEBUG: texture deallocation completed");
}
} else {
println!("DEBUG: skipping deallocation (null or invalid pointer)");
}
println!("DEBUG: texture_unloader returning");
}
#[no_mangle]
pub extern "C" fn test_spine_basic() -> c_int {
unsafe {
println!("Starting spine test...");
spine_bone_set_y_down(false);
println!("Set y_down...");
// Load real spineboy atlas data
let atlas_file = std::fs::read_to_string("../../../examples/spineboy/export/spineboy-pma.atlas")
.expect("Failed to read atlas file");
let atlas_data = CString::new(atlas_file).unwrap();
let atlas_dir = CString::new("../../../examples/spineboy/export/").unwrap();
println!("About to load atlas...");
let atlas = spine_atlas_load_callback(
atlas_data.as_ptr(),
atlas_dir.as_ptr(),
Some(headless_texture_loader),
Some(headless_texture_unloader),
);
println!("Atlas loaded: {:?}", atlas);
if atlas.is_null() {
println!("Atlas is null!");
return 1; // Failed to load atlas
}
// Load real spineboy skeleton data (binary format like the C test)
println!("Reading skeleton file...");
let skeleton_file = std::fs::read("../../../examples/spineboy/export/spineboy-pro.skel")
.expect("Failed to read skeleton file");
println!("Skeleton file size: {} bytes", skeleton_file.len());
let skeleton_path = CString::new("../../../examples/spineboy/export/spineboy-pro.skel").unwrap();
println!("About to call spine_skeleton_data_load_binary...");
let result = spine_skeleton_data_load_binary(atlas, skeleton_file.as_ptr(), skeleton_file.len() as i32, skeleton_path.as_ptr());
println!("spine_skeleton_data_load_binary returned: {:?}", result);
if result.is_null() {
println!("Result is null!");
return 2;
}
println!("About to call spine_skeleton_data_result_get_data...");
println!("Result pointer: {:?}", result);
println!("Result is null: {}", result.is_null());
// Try to read the error first to see if result is valid
println!("Checking if result has error...");
let error_ptr = spine_skeleton_data_result_get_error(result);
println!("Error check completed. Error ptr: {:?}", error_ptr);
if !error_ptr.is_null() {
let error_str = CStr::from_ptr(error_ptr);
println!("Found error: {:?}", error_str);
spine_skeleton_data_result_dispose(result);
spine_atlas_dispose(atlas);
return 2;
}
println!("No error found, getting skeleton data...");
let skeleton_data = spine_skeleton_data_result_get_data(result);
if skeleton_data.is_null() {
let error = spine_skeleton_data_result_get_error(result);
if !error.is_null() {
let error_str = CStr::from_ptr(error);
eprintln!("Skeleton data error: {:?}", error_str);
}
spine_skeleton_data_result_dispose(result);
spine_atlas_dispose(atlas);
return 2; // Failed to load skeleton data
}
println!("Skeleton data is valid: {:?}", skeleton_data);
// Test skeleton creation immediately
println!("Creating skeleton...");
let skeleton = spine_skeleton_create(skeleton_data);
println!("Skeleton create returned: {:?}", skeleton);
if skeleton.is_null() {
spine_skeleton_data_result_dispose(result);
spine_atlas_dispose(atlas);
return 3; // Failed to create skeleton
}
// Test basic operations
println!("Calling spine_skeleton_setup_pose...");
spine_skeleton_setup_pose(skeleton);
println!("Setup pose completed");
println!("Calling spine_skeleton_update_world_transform_1...");
spine_skeleton_update_world_transform_1(skeleton, 1); // SPINE_PHYSICS_UPDATE = 1
println!("Update world transform completed");
println!("Getting skeleton position...");
let x = spine_skeleton_get_x(skeleton);
println!("Got x: {}", x);
let y = spine_skeleton_get_y(skeleton);
println!("Got y: {}", y);
// Cleanup
println!("Disposing skeleton...");
spine_skeleton_dispose(skeleton);
println!("Skeleton disposed");
println!("Disposing skeleton data result...");
spine_skeleton_data_result_dispose(result);
println!("Skeleton data result disposed");
// Test atlas disposal to get proper crash backtrace
println!("About to call spine_atlas_dispose - crash expected...");
spine_atlas_dispose(atlas);
println!("Atlas disposal completed successfully");
// Verify we got reasonable values
println!("Verifying values...");
if x.is_finite() && y.is_finite() {
println!("SUCCESS! Test completed successfully");
0 // Success
} else {
println!("FAILED! Invalid values");
4 // Invalid values
}
}
}
// WASM export for web testing
#[cfg(target_arch = "wasm32")]
mod wasm {
use super::*;
use std::os::raw::c_int;
#[no_mangle]
pub extern "C" fn run_spine_test() -> c_int {
test_spine_basic()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spine_basic_works() {
let result = test_spine_basic();
assert_eq!(result, 0, "Spine basic test should succeed");
}
}

View File

@ -1,117 +0,0 @@
#!/bin/bash
# Spine-C WASM Test
#
# Tests spine-c + spine-cpp-no-cpprt compilation to WASM via Rust FFI
set -e
# Change to test directory
cd "$(dirname "$0")"
# Source logging utilities
source ../../../formatters/logging/logging.sh
log_title "Spine-C WASM Test"
# Check if Rust is installed
if ! command -v cargo > /dev/null 2>&1; then
log_fail "Cargo not found - install Rust toolchain"
exit 1
fi
# Check if WASM target is installed
log_action "Checking WASM target"
if RUSTUP_OUTPUT=$(rustup target list --installed 2>&1); then
if echo "$RUSTUP_OUTPUT" | grep -q "wasm32-unknown-unknown"; then
log_ok
else
log_detail "Installing wasm32-unknown-unknown target"
if rustup target add wasm32-unknown-unknown > /dev/null 2>&1; then
log_ok
else
log_fail
log_detail "Failed to install WASM target"
exit 1
fi
fi
else
log_fail
log_detail "Could not check rustup targets"
exit 1
fi
# Build native version first (for comparison)
log_action "Building native version"
if BUILD_OUTPUT=$(cargo build 2>&1); then
log_ok
else
log_fail
log_error_output "$BUILD_OUTPUT"
exit 1
fi
# Test native version
log_action "Testing native version"
if TEST_OUTPUT=$(cargo test 2>&1); then
log_ok
else
log_fail
log_error_output "$TEST_OUTPUT"
exit 1
fi
# Build WASM version
log_action "Building WASM version"
if WASM_BUILD_OUTPUT=$(cargo build --target wasm32-unknown-unknown 2>&1); then
log_ok
else
log_fail
log_error_output "$WASM_BUILD_OUTPUT"
exit 1
fi
# Check WASM output
WASM_FILE="target/wasm32-unknown-unknown/debug/spine_c_wasm_test.wasm"
if [ -f "$WASM_FILE" ]; then
log_action "Analyzing WASM output"
WASM_SIZE=$(du -h "$WASM_FILE" | cut -f1)
log_ok
log_detail "WASM file size: $WASM_SIZE"
# Check for C++ runtime dependencies (should be minimal)
if command -v wasm-objdump > /dev/null 2>&1; then
log_detail "WASM imports:"
wasm-objdump -x "$WASM_FILE" | grep -A 20 "Import\[" | head -20 || true
fi
else
log_fail "WASM file not found: $WASM_FILE"
exit 1
fi
# Test with wasmtime if available
if command -v wasmtime > /dev/null 2>&1; then
log_action "Testing with wasmtime"
# Create a simple test runner
cat > test_runner.wat << 'EOF'
(module
(import "spine" "run_spine_test" (func $run_spine_test (result i32)))
(func (export "_start")
(call $run_spine_test)
(if (i32.ne (i32.const 0))
(then unreachable))
)
)
EOF
if wasmtime test_runner.wat --invoke _start 2>/dev/null; then
log_ok
else
log_detail "Wasmtime test skipped (expected - needs proper test harness)"
fi
rm -f test_runner.wat
else
log_detail "Wasmtime not available - skipping runtime test"
fi
log_summary "✓ WASM compilation successful"
log_detail "This proves spine-cpp-no-cpprt can be used from Rust and compiled to WASM"