use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::Value as JsonValue;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
use variable_core::ast::Value;
use variable_wire::decode_snapshot;
fn cmd() -> Command {
assert_cmd::cargo::cargo_bin_cmd!("variable")
}
fn workspace_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap()
}
fn write_var_file(dir: &TempDir, name: &str, content: &str) -> std::path::PathBuf {
let path = dir.path().join(name);
fs::write(&path, content).unwrap();
path
}
#[test]
fn generate_valid_var_file() {
let input_dir = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&input_dir,
"features.var",
r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
3: header_text String = "Complete your purchase"
}"#,
);
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg(&var_file)
.assert()
.success()
.stdout(predicate::str::contains("Generated"));
let output_file = out_dir.path().join("features.generated.ts");
assert!(output_file.exists());
let content = fs::read_to_string(&output_file).unwrap();
assert!(content.contains("CheckoutVariables"));
assert!(content.contains("import { VariableClient"));
}
#[test]
fn generate_invalid_var_file() {
let input_dir = TempDir::new().unwrap();
let out_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&input_dir,
"bad.var",
r#"1: Feature Checkout = {
1: enabled = true
}"#,
);
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg(&var_file)
.assert()
.failure()
.stderr(predicate::str::contains("error").or(predicate::str::contains("expected")));
}
#[test]
fn generate_missing_file() {
let out_dir = TempDir::new().unwrap();
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg("nonexistent.var")
.assert()
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn generate_missing_out_flag() {
cmd()
.arg("generate")
.arg("test.var")
.assert()
.failure()
.stderr(predicate::str::contains("--out"));
}
#[test]
fn e2e_generate_fixture_and_verify_output() {
let out_dir = TempDir::new().unwrap();
let fixture = workspace_root().join("fixtures/example.var");
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg(&fixture)
.assert()
.success();
let output_file = out_dir.path().join("example.generated.ts");
assert!(output_file.exists(), "generated file should exist");
let content = fs::read_to_string(&output_file).unwrap();
assert!(content.contains("// This file is generated by Variable. Do not edit."));
assert!(content.contains("import { VariableClient"));
assert!(content.contains("export interface CheckoutVariables"));
assert!(content.contains("enabled: boolean"));
assert!(content.contains("max_items: number"));
assert!(content.contains("header_text: string"));
assert!(content.contains("discount_rate: number"));
assert!(content.contains("getCheckoutVariables"));
assert!(content.contains("export interface SearchVariables"));
assert!(content.contains("boost_factor: number"));
assert!(content.contains("getSearchVariables"));
}
#[test]
fn e2e_generated_typescript_compiles() {
let out_dir = TempDir::new().unwrap();
let fixture = workspace_root().join("fixtures/example.var");
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg(&fixture)
.assert()
.success();
let output_file = out_dir.path().join("example.generated.ts");
assert!(output_file.exists());
let runtime_path = workspace_root().join("runtime/ts");
let tsconfig_content = format!(
r#"{{
"compilerOptions": {{
"target": "ES2020",
"module": "ES2020",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {{
"@variable/runtime": ["{}/src/index.ts"]
}}
}},
"include": ["*.ts"]
}}"#,
runtime_path.display()
);
fs::write(out_dir.path().join("tsconfig.json"), tsconfig_content).unwrap();
let tsc_bin = runtime_path.join("node_modules/.bin/tsc");
let tsc_result = std::process::Command::new(&tsc_bin)
.arg("--noEmit")
.current_dir(out_dir.path())
.output()
.expect("failed to run tsc");
let stdout = String::from_utf8_lossy(&tsc_result.stdout);
let stderr = String::from_utf8_lossy(&tsc_result.stderr);
assert!(
tsc_result.status.success(),
"tsc --noEmit failed:\nstdout: {}\nstderr: {}",
stdout,
stderr
);
}
#[test]
fn encode_valid_var_file_outputs_binary_snapshot() {
let input_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&input_dir,
"features.var",
r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
3: header_text String = "Complete your purchase"
}"#,
);
let assert = cmd()
.arg("encode")
.arg("--schema-revision")
.arg("5")
.arg("--manifest-revision")
.arg("9")
.arg("--generated-at-unix-ms")
.arg("123")
.arg("--source")
.arg("cli-test")
.arg(&var_file)
.assert()
.success();
let output = assert.get_output();
assert!(
output.stderr.is_empty(),
"unexpected stderr: {:?}",
output.stderr
);
assert!(output.stdout.starts_with(b"VARB"));
let report = decode_snapshot(&output.stdout);
assert!(
report.diagnostics.is_empty(),
"unexpected diagnostics: {:?}",
report.diagnostics
);
let snapshot = report.snapshot.expect("expected decoded snapshot");
assert_eq!(snapshot.metadata.schema_revision, 5);
assert_eq!(snapshot.metadata.manifest_revision, 9);
assert_eq!(snapshot.metadata.generated_at_unix_ms, 123);
assert_eq!(snapshot.metadata.source.as_deref(), Some("cli-test"));
assert_eq!(snapshot.features.len(), 1);
assert_eq!(snapshot.features[0].feature_id, 1);
assert_eq!(snapshot.features[0].variables.len(), 3);
assert_eq!(
snapshot.features[0].variables[0].value,
Value::Boolean(true)
);
assert_eq!(snapshot.features[0].variables[1].value, Value::Integer(50));
}
#[test]
fn encode_invalid_var_file_fails() {
let input_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&input_dir,
"bad.var",
r#"Feature Checkout = {
1: enabled Boolean = true
}"#,
);
cmd()
.arg("encode")
.arg(&var_file)
.assert()
.failure()
.stderr(predicate::str::contains("expected").or(predicate::str::contains("error")));
}
#[test]
fn decode_binary_snapshot_outputs_json() {
let temp_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&temp_dir,
"features.var",
r#"1: Feature Checkout = {
1: enabled Boolean = true
2: max_items Integer = 50
}"#,
);
let binary_file = temp_dir.path().join("snapshot.bin");
let encode_output = cmd()
.arg("encode")
.arg("--schema-revision")
.arg("3")
.arg("--manifest-revision")
.arg("8")
.arg("--generated-at-unix-ms")
.arg("1234")
.arg("--source")
.arg("decode-test")
.arg(&var_file)
.assert()
.success()
.get_output()
.stdout
.clone();
fs::write(&binary_file, encode_output).unwrap();
let output = cmd()
.arg("decode")
.arg(&binary_file)
.assert()
.success()
.get_output()
.stdout
.clone();
let decoded: JsonValue = serde_json::from_slice(&output).unwrap();
assert_eq!(decoded["snapshot"]["metadata"]["schema_revision"], 3);
assert_eq!(decoded["snapshot"]["metadata"]["manifest_revision"], 8);
assert_eq!(
decoded["snapshot"]["metadata"]["generated_at_unix_ms"],
1234
);
assert_eq!(decoded["snapshot"]["metadata"]["source"], "decode-test");
assert_eq!(decoded["snapshot"]["features"][0]["feature_id"], 1);
assert_eq!(
decoded["snapshot"]["features"][0]["variables"][0]["value"]["value"],
true
);
assert!(decoded["diagnostics"].as_array().unwrap().is_empty());
}
#[test]
fn decode_fail_on_error_returns_non_zero() {
let temp_dir = TempDir::new().unwrap();
let binary_file = temp_dir.path().join("bad.bin");
fs::write(&binary_file, b"NOPE").unwrap();
cmd()
.arg("decode")
.arg("--fail-on-error")
.arg(&binary_file)
.assert()
.failure()
.stdout(predicate::str::contains("\"snapshot\":null"))
.stderr(predicate::str::contains(
"decode reported error diagnostics",
));
}
#[test]
fn generate_struct_fixture() {
let out_dir = TempDir::new().unwrap();
let fixture = workspace_root().join("fixtures/structs.var");
cmd()
.arg("generate")
.arg("--out")
.arg(out_dir.path())
.arg(&fixture)
.assert()
.success();
let output_file = out_dir.path().join("structs.generated.ts");
assert!(output_file.exists(), "generated file should exist");
let content = fs::read_to_string(&output_file).unwrap();
assert!(content.contains("export interface Theme"));
assert!(content.contains("dark_mode: boolean"));
assert!(content.contains("font_size: number"));
assert!(content.contains("primary_color: string"));
assert!(content.contains("export interface ShippingConfig"));
assert!(content.contains("express_enabled: boolean"));
assert!(content.contains("max_weight_kg: number"));
assert!(content.contains("default_carrier: string"));
assert!(content.contains("theme: Theme"));
assert!(content.contains("shipping: ShippingConfig"));
}
#[test]
fn encode_decode_roundtrip_with_structs() {
let temp_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&temp_dir,
"structs.var",
r#"1: Struct Config = {
1: retries Integer = 3
2: verbose Boolean = false
}
1: Feature App = {
1: enabled Boolean = true
2: config Config = Config { retries = 5 }
}"#,
);
let encode_output = cmd()
.arg("encode")
.arg("--schema-revision")
.arg("1")
.arg("--manifest-revision")
.arg("1")
.arg("--generated-at-unix-ms")
.arg("100")
.arg(&var_file)
.assert()
.success()
.get_output()
.stdout
.clone();
assert!(encode_output.starts_with(b"VARB"));
let binary_file = temp_dir.path().join("snapshot.bin");
fs::write(&binary_file, &encode_output).unwrap();
let output = cmd()
.arg("decode")
.arg("--pretty")
.arg(&binary_file)
.assert()
.success()
.get_output()
.stdout
.clone();
let decoded: JsonValue = serde_json::from_slice(&output).unwrap();
let features = decoded["snapshot"]["features"].as_array().unwrap();
assert_eq!(features.len(), 1);
let variables = features[0]["variables"].as_array().unwrap();
assert_eq!(variables.len(), 2);
let config_var = &variables[1];
assert_eq!(config_var["value"]["type"], "struct");
let fields = &config_var["value"]["fields"];
assert_eq!(fields["retries"]["value"], 5);
assert_eq!(fields["verbose"]["value"], false);
}
#[test]
fn encode_struct_with_empty_literal() {
let temp_dir = TempDir::new().unwrap();
let var_file = write_var_file(
&temp_dir,
"test.var",
r#"1: Struct Settings = {
1: enabled Boolean = true
2: count Integer = 10
}
1: Feature App = {
1: settings Settings = Settings {}
}"#,
);
let encode_output = cmd()
.arg("encode")
.arg("--generated-at-unix-ms")
.arg("0")
.arg(&var_file)
.assert()
.success()
.get_output()
.stdout
.clone();
let report = decode_snapshot(&encode_output);
assert!(report.diagnostics.is_empty());
let snapshot = report.snapshot.unwrap();
let var = &snapshot.features[0].variables[0];
match &var.value {
Value::Struct { fields, .. } => {
assert_eq!(fields.get("count"), Some(&Value::Integer(10)));
assert_eq!(fields.get("enabled"), Some(&Value::Boolean(true)));
}
_ => panic!("expected struct value"),
}
}