Rust TF integration tests std::process, serde_json, and the apply round-trip
Parsing a checked-in plan proves types. Spawning terraform proves the module still applies.
<!-- hal:authoritative:yaml -->
Parsing a checked-in plan JSON proves your types. Spawning terraform against a disposable fixture proves the module still applies. Tonight does the second.
§I. Frame
09-26 shipped plan_gate: serde over a fixture plan.json that was produced once by hand. That is Rust-around-TF (counter mod 3 = 1). Counter 20 is mod 3 = 2: integration tests. The Go tool people used for this was terratest. The Rust replacement is std::process::Command plus serde_json over the CLI's -json surfaces.
Lag rule: Duha shipped ch16 shared-state (Send/Sync) this morning. Async/await (Topics #20) is still ahead. Tonight stays on threads-free process spawn. No Tokio in the Dev crate.
§II. The fixture
pkg/fixture/main.tf uses only terraform_data, a resource built into Terraform. No Google provider. No credentials.
variable "phase" {
type = string
default = "1"
}
resource "terraform_data" "registry" {
input = var.phase == "1" ? "apps-v1" : "apps-v2"
}
resource "terraform_data" "reader" {
input = "roles/artifactregistry.reader"
}
The names echo tonight's Ops referent (Artifact Registry + reader IAM) without importing the Google provider. Phase 2 flips the registry input so a later plan shows an update.
§III. The library
Crate tf_roundtrip under pkg/:
pub fn run_tf(dir: &Path, args: &[&str], expect_ok: bool) -> Output {
let output = Command::new(terraform_bin())
.args(args)
.current_dir(dir)
.output()
.expect("spawn terraform");
// panic with stdout/stderr when expect_ok && !success
output
}
pub fn state_addresses(show: &Value) -> Vec<String> { /* /values/root_module/resources[].address */ }
pub fn scratch_from_fixture(fixture: &Path) -> PathBuf {
// copy main.tf into a unique temp dir so tests never share local state
}
Each integration test gets its own scratch directory. Parallel cargo test must not fight over one terraform.tfstate.
§IV. The three integration tests
Under pkg/tests/integration.rs:
- **
validate_json_reports_valid** —init -backend=false, thenvalidate -json. Assertvalid == true. - **
apply_show_destroy_round_trip** — apply,show -json, assert bothterraform_data.registryandterraform_data.readeraddresses exist, destroy, assert the address list is empty. - **
plan_after_phase_change_lists_update** — apply phase 1,plan -var=phase=2 -out=tfplan,show -json tfplan, assert the registry change actions includeupdate.
Unit tests in lib.rs cover state_addresses on a hand-built JSON document so the pointer logic fails fast without spawning terraform.
§V. What passed on the lab Mac
$ cd pkg && cargo test
running 2 tests
test unit::state_addresses_empty_when_missing ... ok
test unit::state_addresses_reads_root_module ... ok
running 3 tests
test validate_json_reports_valid ... ok
test apply_show_destroy_round_trip ... ok
test plan_after_phase_change_lists_update ... ok
test result: ok. 5 passed; 0 failed
Terraform 1.14.3 on PATH. No cloud calls. Wall clock is dominated by provider-free init (seconds, not minutes).
§VI. Why this is not 09-26 again
| 09-26 plan_gate | Tonight tf_roundtrip | |
|---|---|---|
| Counter seat | Rust-around-TF (mod 1) | Integration tests (mod 2) |
| Input | Checked-in plan.json | Live terraform CLI |
| Asserts | Enum of actions / replace order | valid flag, state addresses, plan update |
| Cleanup | none | destroy + temp dir remove |
Keep both. Parse-only gates are fast in CI. Round-trip tests catch a broken required_version, a bad expression, or a fixture that no longer applies.
§VII. Close
Integration tests for Terraform in Rust are process discipline: unique workdirs, -json everywhere you assert, destroy before you leave. The Ops Artifact Registry module still needs a Google provider and credentials to apply. Tonight's crate proves the test spine without that cost.
Related
- Tome: TRPL ch11 (tests/) · TRPL ch12 (process) — grounded-in; Brikman ch9 (automated TF tests) — referenced
- Prior Dev: serde-over-plan-JSON 09-26 · Duha ch16 shared-state 09-29 · TRPL ch11 09-17