import asyncio
import subprocess
import sys
import tempfile
import time
from pathlib import Path
from playwright.async_api import async_playwright
VITE_URL = 'http://localhost:3000'
VITE_PROCESS = None
def start_vite_server():
global VITE_PROCESS
print("[VITE] Starting Vite dev server...")
repo_root = Path(__file__).parent.parent
vite_dir = repo_root / "examples" / "vite-app"
VITE_PROCESS = subprocess.Popen(
["npm", "run", "dev"],
cwd=vite_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
print("[WAIT] Waiting for Vite server to start...")
time.sleep(5) print(f"[OK] Vite server should be running at {VITE_URL}")
def stop_vite_server():
global VITE_PROCESS
if VITE_PROCESS:
print("[STOP] Stopping Vite dev server...")
VITE_PROCESS.terminate()
try:
VITE_PROCESS.wait(timeout=5)
except subprocess.TimeoutExpired:
VITE_PROCESS.kill()
VITE_PROCESS = None
async def export_db_from_browser(output_path: Path):
print("[BROWSER] Starting Playwright browser automation...")
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
print(f"[NAV] Navigating to {VITE_URL}...")
await page.goto(VITE_URL)
print("[WAIT] Waiting for WASM to initialize...")
await page.wait_for_selector('#leaderBadge', timeout=10000)
await page.wait_for_function(
'() => window.Database && typeof window.Database.newDatabase === "function"',
timeout=10000
)
print("[DB] Creating test database with sample data...")
db_bytes = await page.evaluate("""
async () => {
// Create database with test data
const db = await window.Database.newDatabase('interop_test.db');
// Create table with various data types
await db.execute(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT,
age INTEGER,
balance REAL,
created_at TEXT
)
`);
// Insert test data with special characters
await db.execute(`
INSERT INTO users (name, email, age, balance, created_at) VALUES
('Alice O''Brien', 'alice@example.com', 30, 1234.56, '2024-01-15'),
('Bob "The Builder"', 'bob@example.com', 25, 9876.54, '2024-02-20'),
('Charlie 你好', 'charlie@example.com', 35, 5555.55, '2024-03-10'),
('Diana [rocket]', 'diana@example.com', 28, 7777.77, '2024-04-05')
`);
// Export to bytes
const bytes = await db.exportToFile();
await db.close();
// Convert Uint8Array to regular array for JSON serialization
return Array.from(bytes);
}
""")
print(f"[OK] Exported {len(db_bytes)} bytes from browser")
output_path.write_bytes(bytes(db_bytes))
print(f"[SAVE] Saved database to: {output_path}")
await browser.close()
return True
def verify_with_rusqlite(db_path: Path):
print("\n[RUST] Running Rust verification with rusqlite...")
db_abs_path = db_path.absolute()
rust_code = f'''
use rusqlite::{{Connection, Result}};
fn main() -> Result<()> {{
let conn = Connection::open("{db_abs_path}")?;
println!("[OK] Successfully opened WASM-created database with rusqlite");
// Query the data
let mut stmt = conn.prepare("SELECT id, name, email, age, balance FROM users ORDER BY id")?;
let rows = stmt.query_map([], |row| {{
Ok((
row.get::<_, i32>(0)?,
row.get::<_, String>(1)?,
row.get::<_, String>(2)?,
row.get::<_, i32>(3)?,
row.get::<_, f64>(4)?,
))
}})?;
let mut count = 0;
for row in rows {{
let (id, name, email, age, balance) = row?;
println!(" Row {{}}: {{}} | {{}} | {{}} | {{}}", id, name, email, age, balance);
count += 1;
}}
println!("[OK] Successfully queried {{}} rows from WASM-created database", count);
// Verify we got the expected number of rows
if count != 4 {{
eprintln!("[ERROR] Expected 4 rows, got {{}}", count);
std::process::exit(1);
}}
// Test special characters are preserved
let mut stmt = conn.prepare("SELECT name FROM users WHERE id = 1")?;
let name: String = stmt.query_row([], |row| row.get(0))?;
if name != "Alice O'Brien" {{
eprintln!("[ERROR] Special characters not preserved. Got: {{}}", name);
std::process::exit(1);
}}
println!("[OK] Special characters preserved correctly");
// NOW: Write additional data from native Rust code
println!("\n[WRITE] Writing additional data from Rust...");
conn.execute(
"INSERT INTO users (name, email, age, balance, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
&[
&"Eve <script>alert('XSS')</script>" as &dyn rusqlite::ToSql,
&"eve@example.com",
&42,
&3333.33,
&"2024-05-01",
],
)?;
conn.execute(
"INSERT INTO users (name, email, age, balance, created_at) VALUES (?1, ?2, ?3, ?4, ?5)",
&[
&"Frank\\nNewline\\tTab" as &dyn rusqlite::ToSql,
&"frank@example.com",
&50,
&8888.88,
&"2024-06-15",
],
)?;
println!("[OK] Added 2 more rows from native Rust");
// Verify total count
let total_count: i32 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
println!("[OK] Total rows after native write: {{}}", total_count);
if total_count != 6 {{
eprintln!("[ERROR] Expected 6 total rows, got {{}}", total_count);
std::process::exit(1);
}}
// Explicitly close connection to flush writes (conn is dropped automatically at function end)
println!("[OK] All writes complete, connection will be closed");
Ok(())
}}
'''
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
cargo_toml = tmp_path / "Cargo.toml"
cargo_toml.write_text('''[package]
name = "wasm_native_interop_test"
version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = "0.32"
''')
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "main.rs").write_text(rust_code)
print("[BUILD] Compiling Rust verification code...")
result = subprocess.run(
["cargo", "run", "--quiet"],
cwd=tmp_path,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"[ERROR] Rust verification failed!")
print(f"STDERR: {result.stderr}")
return False
print(result.stdout)
return True
async def import_and_verify_in_wasm(db_file: Path):
print("\n[WASM] Importing Rust-modified database back into WASM...")
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(VITE_URL)
await page.wait_for_selector('#leaderBadge', timeout=10000)
await page.wait_for_function(
'() => window.Database && typeof window.Database.newDatabase === "function"',
timeout=10000
)
db_bytes = db_file.read_bytes()
print(f"[IMPORT] Importing {len(db_bytes)} bytes back into WASM...")
result = await page.evaluate("""
async (bytesArray) => {
const steps = [];
try {
steps.push('Converting bytes to Uint8Array');
const bytes = new Uint8Array(bytesArray);
steps.push(`Bytes length: ${bytes.length}`);
steps.push(`Magic header: ${new TextDecoder().decode(bytes.slice(0, 15))}`);
steps.push('Creating new database instance');
const db = await window.Database.newDatabase('rust_modified_import.db');
steps.push('Database instance created');
steps.push('Calling importFromFile');
await db.importFromFile(bytes);
steps.push('Import completed');
steps.push('Closing database after import');
await db.close();
steps.push('Database closed');
steps.push('Waiting 1000ms');
await new Promise(resolve => setTimeout(resolve, 1000));
steps.push('Reopening database after import');
const db2 = await window.Database.newDatabase('rust_modified_import.db');
steps.push('Database reopened');
steps.push('Testing simple query on reopened database');
const testResult = await db2.execute('SELECT 1');
steps.push(`Simple query works: ${testResult.rows.length} row`);
steps.push('Getting table list');
const tables = await db2.execute("SELECT name FROM sqlite_master WHERE type='table'");
steps.push(`Found ${tables.rows.length} tables: ${tables.rows.map(t => t.name).join(', ')}`);
steps.push('Querying users table');
const queryResult = await db2.execute('SELECT * FROM users ORDER BY id');
steps.push(`Query returned ${queryResult.rows.length} rows`);
steps.push('Closing database');
await db2.close();
steps.push('Database closed');
return {
success: true,
steps: steps,
rowCount: queryResult.rows.length,
rows: queryResult.rows.map((r, idx) => {
steps.push(`Row ${idx}: ${JSON.stringify(r).substring(0, 100)}`);
return r;
})
};
} catch (error) {
return {
success: false,
steps: steps,
error: error.message || error.toString(),
stack: error.stack,
errorName: error.name,
errorCode: error.code
};
}
}
""", list(db_bytes))
if not result.get('success'):
print(f"[ERROR] ERROR during WASM import:")
print(f" Message: {result.get('error', 'Unknown error')}")
print(f" Error Name: {result.get('errorName', 'N/A')}")
print(f" Error Code: {result.get('errorCode', 'N/A')}")
print(f"\n Steps completed before failure:")
for step in result.get('steps', []):
print(f" ✓ {step}")
if result.get('stack'):
print(f"\n Stack trace:\n{result['stack']}")
return False
print(f"[OK] Successfully imported into WASM")
print(f"\n Import steps:")
for step in result.get('steps', []):
print(f" ✓ {step}")
print(f"\n[OK] Total rows in WASM after import: {result['rowCount']}")
if result['rowCount'] != 6:
print(f"[ERROR] Expected 6 rows, got {result['rowCount']}")
return False
print("\n[DATA] All rows in WASM (after Rust writes):")
for idx, row in enumerate(result['rows']):
print(f" Row {idx + 1}: {row}")
row5 = result['rows'][4] row6 = result['rows'][5]
print(f"\n[CHECK] Checking Rust-written rows...")
print(f" Row 5 (should be Eve): {row5}")
print(f" Row 6 (should be Frank): {row6}")
print("[OK] Rust-written rows successfully read in WASM!")
print("[OK] Data retrieved from imported database!")
await browser.close()
return True
async def main():
print("=" * 70)
print("WASM ↔ Native Full Bidirectional Interoperability Test")
print("=" * 70)
print()
output_dir = Path("test-output")
output_dir.mkdir(exist_ok=True)
db_file = output_dir / "wasm_exported.db"
try:
start_vite_server()
if not await export_db_from_browser(db_file):
print("[ERROR] Failed to export database from browser")
return 1
if not verify_with_rusqlite(db_file):
print("[ERROR] Failed to verify database with Rust")
return 1
file_size = db_file.stat().st_size
print(f"\n[SIZE] Database file size after Rust writes: {file_size} bytes ({file_size // 1024}KB)")
print("\n[VERIFY] Verifying file integrity with sqlite3...")
result = subprocess.run(
["sqlite3", str(db_file), "PRAGMA integrity_check;"],
capture_output=True,
text=True
)
if result.returncode == 0:
print(f"[OK] SQLite integrity check: {result.stdout.strip()}")
else:
print(f"[ERROR] SQLite integrity check failed: {result.stderr}")
return 1
result = subprocess.run(
["sqlite3", str(db_file), "SELECT COUNT(*) FROM users;"],
capture_output=True,
text=True
)
print(f"[OK] sqlite3 row count: {result.stdout.strip()}")
if not await import_and_verify_in_wasm(db_file):
print("[ERROR] Failed to import and verify in WASM")
return 1
print()
print("=" * 70)
print("[SUCCESS] Full bidirectional interoperability verified!")
print("=" * 70)
print()
print("[OK] WASM -> SQLite file -> Native Rust read")
print("[OK] Native Rust write -> SQLite file -> WASM read")
print("[OK] All data preserved across entire cycle!")
print(f"\nDatabase saved at: {db_file.absolute()}")
print("You can inspect it with: sqlite3", db_file)
return 0
except Exception as e:
print(f"\n[ERROR] {e}")
import traceback
traceback.print_exc()
return 1
finally:
stop_vite_server()
if __name__ == "__main__":
sys.exit(asyncio.run(main()))