use std::process::Command;
use camino::{Utf8Path, Utf8PathBuf};
use serde_json::Value;
use tempfile::TempDir;
#[test]
fn validate_and_emit_ir_work() {
let temp = TempDir::new().expect("tempdir");
let ir = temp_path(&temp, "demo.ir.json");
run_tripley_rpc(&[
"validate",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
]);
run_tripley_rpc(&[
"emit-ir",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
ir.as_str(),
]);
let text = std::fs::read_to_string(ir).expect("read ir");
assert!(text.contains("\"package\": \"kiosk.card\""));
assert!(text.contains("\"name\": \"CardReader\""));
}
#[test]
fn validate_can_emit_json() {
let output = run_tripley_rpc(&[
"--format",
"json",
"validate",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
]);
let json = stdout_json(output);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "validate");
}
#[test]
fn compat_accepts_same_idl() {
let output = run_tripley_rpc(&[
"compat",
"--old",
kiosk_idl().as_str(),
"--old-import-root",
kiosk_root().as_str(),
"--new",
kiosk_idl().as_str(),
"--new-import-root",
kiosk_root().as_str(),
]);
assert!(String::from_utf8_lossy(&output.stdout).contains("unchanged"));
}
#[test]
fn compat_can_emit_json() {
let output = run_tripley_rpc(&[
"--format",
"json",
"compat",
"--old",
kiosk_idl().as_str(),
"--old-import-root",
kiosk_root().as_str(),
"--new",
kiosk_idl().as_str(),
"--new-import-root",
kiosk_root().as_str(),
]);
let json = stdout_json(output);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "compat");
assert_eq!(json["report"]["overall"], "unchanged");
}
#[test]
fn fmt_check_and_emit_ir_can_emit_json() {
let temp = TempDir::new().expect("tempdir");
let ir = temp_path(&temp, "demo.ir.json");
let fmt = run_tripley_rpc(&[
"--format",
"json",
"fmt",
"--in",
kiosk_idl().as_str(),
"--check",
]);
let json = stdout_json(fmt);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "fmt");
let emit = run_tripley_rpc(&[
"--format",
"json",
"emit-ir",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
ir.as_str(),
]);
let json = stdout_json(emit);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "emit-ir");
assert!(ir.exists());
}
#[test]
fn generate_rust_can_emit_json() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "rust-json-sdk");
let generated = run_tripley_rpc(&[
"--format",
"json",
"generate",
"rust",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
out.as_str(),
"--side",
"client",
]);
let json = stdout_json(generated);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "generate rust");
assert_eq!(json["side"], "client");
}
#[test]
fn generate_rust_writes_sdk_files_directly_to_output_dir() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "sdk");
run_tripley_rpc(&[
"generate",
"rust",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
out.as_str(),
"--crate",
"kiosk_sdk",
]);
for file in [
"activation.rs",
"client.rs",
"mod.rs",
"server.rs",
"services.rs",
"types.rs",
] {
assert!(out.join(file).exists(), "missing generated file {file}");
}
}
#[test]
fn generate_rust_can_write_client_or_server_side_only() {
let temp = TempDir::new().expect("tempdir");
let client_out = temp_path(&temp, "client-sdk");
let server_out = temp_path(&temp, "server-sdk");
run_tripley_rpc(&[
"generate",
"rust",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
client_out.as_str(),
"--side",
"client",
]);
assert!(client_out.join("client.rs").exists());
assert!(client_out.join("mod.rs").exists());
assert!(client_out.join("services.rs").exists());
assert!(client_out.join("types.rs").exists());
assert!(!client_out.join("activation.rs").exists());
assert!(!client_out.join("server.rs").exists());
run_tripley_rpc(&[
"generate",
"rust",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
server_out.as_str(),
"--side",
"server",
]);
assert!(server_out.join("activation.rs").exists());
assert!(server_out.join("mod.rs").exists());
assert!(server_out.join("server.rs").exists());
assert!(server_out.join("services.rs").exists());
assert!(server_out.join("types.rs").exists());
assert!(!server_out.join("client.rs").exists());
}
#[test]
fn generate_rust_accepts_notification_backend_options() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "mpsc-sdk");
run_tripley_rpc(&[
"generate",
"rust",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
out.as_str(),
"--notification-backend",
"tokio-mpsc",
"--notification-buffer",
"16",
]);
let client = std::fs::read_to_string(out.join("client.rs")).expect("read client");
assert!(client.contains("use tokio::sync::mpsc;"));
assert!(client.contains("pub struct CardReaderMediaInsertedReceiver"));
assert!(!client.contains("use tokio::sync::broadcast;"));
}
#[test]
fn generate_typescript_writes_client_sdk_files() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "typescript-sdk");
run_tripley_rpc(&[
"generate",
"typescript",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
out.as_str(),
"--runtime-package",
"@acme/xrpc-runtime",
]);
for file in ["client.ts", "index.ts", "services.ts", "types.ts"] {
assert!(out.join(file).exists(), "missing generated file {file}");
}
let client = std::fs::read_to_string(out.join("client.ts")).expect("read client");
assert!(client.contains("from '@acme/xrpc-runtime'"));
assert!(client.contains("export class CardReaderClient"));
}
#[test]
fn generate_and_doctor_can_emit_json() {
let temp = TempDir::new().expect("tempdir");
let out = temp_path(&temp, "json-sdk");
let generated = run_tripley_rpc(&[
"--format",
"json",
"generate",
"typescript",
"--in",
kiosk_idl().as_str(),
"--import-root",
kiosk_root().as_str(),
"--out",
out.as_str(),
]);
let json = stdout_json(generated);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "generate typescript");
let doctor = run_tripley_rpc(&["--format", "json", "doctor"]);
let json = stdout_json(doctor);
assert_eq!(json["ok"], true);
assert_eq!(json["command"], "doctor");
assert!(
json["version"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
assert!(
json["target"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
assert!(
json["checks"]
.as_array()
.expect("checks")
.contains(&Value::from("idl_parse"))
);
for check in [
"version",
"native_binary",
"cwd_writable",
"idl_import_root",
"output_dir_writable",
"rust_generator",
"typescript_generator",
] {
assert!(
json["checks"]
.as_array()
.expect("checks")
.contains(&Value::from(check)),
"missing doctor check {check}"
);
}
}
#[test]
fn invalid_idl_failure_can_emit_json() {
let temp = TempDir::new().expect("tempdir");
let idl = temp_path(&temp, "invalid.rpc.yaml");
std::fs::write(
&idl,
r#"
idl_version: 1
package: demo
types:
Empty:
kind: struct
fields: []
services:
Demo:
guid: not-a-guid
"#,
)
.expect("write invalid idl");
let output = run_tripley_rpc_fail(&["--format", "json", "validate", "--in", idl.as_str()]);
let json: Value = serde_json::from_slice(&output.stderr).expect("stderr json");
assert_eq!(json["ok"], false);
assert!(
json["message"]
.as_str()
.is_some_and(|value| !value.is_empty())
);
assert!(json["diagnostics"].as_array().is_some());
assert!(
json["diagnostics"]
.as_array()
.expect("diagnostics")
.iter()
.any(|diag| diag["message"]
.as_str()
.unwrap_or("")
.contains("invalid GUID"))
);
}
#[test]
fn fmt_can_rewrite_and_check_directory() {
let temp = TempDir::new().expect("tempdir");
let idl = temp_path(&temp, "sample.rpc.yaml");
std::fs::write(&idl, UNFORMATTED_IDL).expect("write idl");
run_tripley_rpc(&["fmt", "--in", idl.as_str()]);
run_tripley_rpc(&["fmt", "--in", temp_path(&temp, "").as_str(), "--check"]);
}
#[test]
fn doctor_runs_self_checks() {
let output = run_tripley_rpc(&["doctor"]);
assert!(String::from_utf8_lossy(&output.stdout).contains("doctor ok"));
}
fn run_tripley_rpc(args: &[&str]) -> std::process::Output {
let output = Command::new(env!("CARGO_BIN_EXE_tripley-rpc"))
.args(args)
.current_dir(repo_root().as_str())
.output()
.expect("run tripley-rpc");
if !output.status.success() {
panic!(
"tripley-rpc {} failed\nstdout:\n{}\nstderr:\n{}",
args.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
output
}
fn run_tripley_rpc_fail(args: &[&str]) -> std::process::Output {
let output = Command::new(env!("CARGO_BIN_EXE_tripley-rpc"))
.args(args)
.current_dir(repo_root().as_str())
.output()
.expect("run tripley-rpc");
assert!(
!output.status.success(),
"tripley-rpc {} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",
args.join(" "),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
output
}
fn stdout_json(output: std::process::Output) -> Value {
serde_json::from_slice(&output.stdout).expect("stdout json")
}
fn temp_path(temp: &TempDir, relative: &str) -> Utf8PathBuf {
Utf8PathBuf::from_path_buf(temp.path().join(relative)).expect("utf8 temp path")
}
fn kiosk_idl() -> Utf8PathBuf {
kiosk_root().join("card_reader.rpc.yaml")
}
fn kiosk_root() -> Utf8PathBuf {
repo_root().join("idl/examples/kiosk_demo")
}
fn repo_root() -> Utf8PathBuf {
Utf8Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Utf8Path::parent)
.expect("repo root")
.to_owned()
}
const UNFORMATTED_IDL: &str = r#"
idl_version: 1
package: demo.format
types:
Empty:
kind: struct
fields: []
services:
Probe:
guid: 42d85f4a-60b8-4b64-9ecb-1b6db17bd30c
methods:
ping:
id: 1
request: Empty
response: Empty
"#;