Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 22s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 9s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 15s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 28s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 30s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 51s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m48s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 7s
WAI operation catalogue / does this change touch it (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 7s
WAI energy receipts / does this change touch it (pull_request) Successful in 6s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 7s
wai-fleet service / does this change touch it (pull_request) Successful in 7s
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Successful in 7s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been skipped
wai-key-proxy / does this change touch it (pull_request) Successful in 7s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been skipped
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been skipped
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m42s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 6s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 6s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 6s
WAI Studio / does this change touch it (pull_request) Successful in 6s
wai-web wasm32 / does this change touch it (pull_request) Successful in 6s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m13s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m31s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 6m37s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m37s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m50s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m4s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m44s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 1m19s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 8m20s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m14s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 3m56s
wai-key-proxy / proxy · linux (pull_request) Successful in 1m9s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 20s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 12m32s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m38s
wai-ledger service / service · linux (pull_request) Successful in 4m17s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 3m50s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 55s
wai-fleet service / service · linux (pull_request) Successful in 6m15s
wai-pack / wai-pack · linux (pull_request) Successful in 1m50s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 22s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 10m53s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m6s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 2m5s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 2m53s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 10m3s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 2m20s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 9m15s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 11m18s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Failing after 7m20s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 17s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 33s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m54s
wai-fleet service / service · macos (pull_request) Successful in 47s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 6m11s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 7m58s
wai-ledger service / service · macos (pull_request) Successful in 1m5s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m43s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 13m10s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 18m27s
wai-pack / wai-pack · macos (pull_request) Successful in 3m58s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m39s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 4m45s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m20s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 8m10s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 14m49s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 13m19s
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Successful in 7s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 11m24s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 6m5s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m33s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m28s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 8m11s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 24s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 25m16s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 7m8s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m57s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m27s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m8s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m3s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 35m1s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 21m57s
Takes #540's channel lineup beside the route record: the conformance workflow's feature list, test list and per-feature check name both (channel_lineup, route_record); the catalogue keeps channel.import, channel.guide, channel.verify and route.verify; strict_json is built for video_receipt or channel_lineup. ops.json and Studio's actions are regenerated from the merged table; the registry export is up to date. Co-Authored-By: Claude Opus 5.5 <[email protected]>
586 lines
34 KiB
Rust
586 lines
34 KiB
Rust
//! The operation catalogue through the `wai` binary, as an agent and a person meet it
|
|
//! (`--features quickstart --test cli_ops`):
|
|
//!
|
|
//! - `wai ops list`, `search`, `schema` and `export` read the catalogue, and the export is
|
|
//! the committed `wai/catalogue/ops.json` (a drifted copy exits 1);
|
|
//! - the quickstart installer (`wai/quickstart/install.sh`) installs this crate's version, as
|
|
//! the release job requires of a tag `wai-v<version>`;
|
|
//! - every operation answers `--help` from its catalogue entry, and its mechanical form
|
|
//! `wai <noun> <verb>` runs it as its CLI spelling does;
|
|
//! - the exit codes: 1 a check that failed, 2 usage, 3 permission, 4 confirmation required,
|
|
//! 5 a file, an operation or a build feature not found, 6 a target that did not come up;
|
|
//! - `--json`: one envelope (`envelope/1`) on stdout, success and failure, with a command's
|
|
//! own JSON or its text in `data`, and with a confirmation's proposal and command;
|
|
//! - `--dry-run`: every operation of this build whose effect is not `reads` writes nothing
|
|
//! with it, and (for those that write) writes without it, so the dry run is of a command
|
|
//! that would write;
|
|
//! - `--yes`: replacing a file the command line did not name waits for it; `--confirm` is its
|
|
//! deprecated spelling, does the same, and help and the export say so.
|
|
#![cfg(feature = "quickstart")]
|
|
|
|
use serde_json::Value;
|
|
use std::io::{BufRead, BufReader};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output, Stdio};
|
|
use wai::catalogue::{self, Confirm, Effect, OPS};
|
|
|
|
fn wai_in(dir: &Path, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).stdin(Stdio::null()).output().expect("wai runs")
|
|
}
|
|
|
|
fn text(o: &Output) -> String {
|
|
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
|
|
}
|
|
|
|
fn scratch(name: &str) -> PathBuf {
|
|
let d = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_ops").join(name);
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d
|
|
}
|
|
|
|
fn wai_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
}
|
|
|
|
/// The one JSON envelope a `--json` run prints.
|
|
fn envelope(o: &Output) -> Value {
|
|
let v: Value = serde_json::from_slice(&o.stdout).unwrap_or_else(|e| panic!("not one JSON envelope ({e}): {}", text(o)));
|
|
assert_eq!(v["wai"], catalogue::ENVELOPE, "{v}");
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn ops_list_names_every_operation_and_export_is_the_committed_file() {
|
|
let d = scratch("list");
|
|
let o = wai_in(&d, &["ops", "list", "--json"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!((v["ok"].as_bool(), v["op"].as_str()), (Some(true), Some("ops.list")));
|
|
let ids: Vec<&str> = v["data"].as_array().unwrap().iter().map(|r| r["id"].as_str().unwrap()).collect();
|
|
assert_eq!(ids, OPS.iter().map(|o| o.id).collect::<Vec<_>>());
|
|
let plain = wai_in(&d, &["ops", "list"]);
|
|
for op in OPS {
|
|
assert!(String::from_utf8_lossy(&plain.stdout).contains(op.id), "{}", op.id);
|
|
}
|
|
|
|
let committed = wai_dir().join("catalogue/ops.json");
|
|
let o = wai_in(&d, &["ops", "export", "--check", committed.to_str().unwrap()]);
|
|
assert!(o.status.success(), "wai/catalogue/ops.json is the catalogue's export: {}", text(&o));
|
|
let o = wai_in(&d, &["ops", "export"]);
|
|
assert_eq!(o.stdout, std::fs::read(&committed).unwrap(), "`wai ops export` prints the committed export");
|
|
// A drifted copy is a check that failed.
|
|
let drifted = d.join("ops.json");
|
|
std::fs::write(&drifted, String::from_utf8_lossy(&o.stdout).replacen("envelope.inspect", "envelope.inspekt", 1)).unwrap();
|
|
let o = wai_in(&d, &["ops", "export", "--check", drifted.to_str().unwrap()]);
|
|
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
|
assert!(text(&o).contains("regenerate"), "{}", text(&o));
|
|
}
|
|
|
|
/// The release job's first check (wai-release.yml) refuses a tag whose installer installs
|
|
/// another version, after the version bump has merged; this holds the bump to it.
|
|
#[test]
|
|
fn the_quickstart_installer_installs_this_version() {
|
|
let version = env!("CARGO_PKG_VERSION");
|
|
let install = std::fs::read_to_string(wai_dir().join("quickstart/install.sh")).unwrap();
|
|
let line = format!("VERSION=\"${{WAI_VERSION:-{version}}}\"");
|
|
assert!(
|
|
install.lines().any(|l| l == line),
|
|
"wai/quickstart/install.sh does not install {version}, this crate's version: it needs the line {line}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ops_search_and_schema_answer_offline() {
|
|
let d = scratch("search");
|
|
let o = wai_in(&d, &["ops", "search", "verify", "the", "receipts", "--json"]);
|
|
let v = envelope(&o);
|
|
let hits = v["data"].as_array().unwrap();
|
|
assert!(!hits.is_empty() && hits.len() <= 5, "{v}");
|
|
assert_eq!(hits[0]["id"], "receipt.verify", "{v}");
|
|
let o = wai_in(&d, &["ops", "search", "make a new project"]);
|
|
assert!(String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("").starts_with("project.create"), "{}", text(&o));
|
|
|
|
let o = wai_in(&d, &["ops", "schema", "envelope.wrap"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let s: Value = serde_json::from_slice(&o.stdout).unwrap();
|
|
assert_eq!((s["id"].as_str(), s["effect"].as_str(), s["dry_run"].as_bool()), (Some("envelope.wrap"), Some("writes-project"), Some(true)));
|
|
assert!(s["input"]["required"].as_array().unwrap().iter().any(|r| r == "capability"), "{s}");
|
|
let o = wai_in(&d, &["ops", "schema", "envelope.frob", "--json"]);
|
|
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
|
assert_eq!(envelope(&o)["error"]["code"], "operation-not-found");
|
|
}
|
|
|
|
#[test]
|
|
fn every_operation_answers_help_from_its_entry_and_its_mechanical_form() {
|
|
let d = scratch("help");
|
|
for op in OPS.iter().filter(|o| o.cli.is_some()) {
|
|
let cmd = op.command();
|
|
let mut words: Vec<&str> = cmd.split_whitespace().skip(1).collect();
|
|
words.push("--help");
|
|
let o = wai_in(&d, &words);
|
|
assert!(o.status.success(), "{cmd} --help: {}", text(&o));
|
|
let t = String::from_utf8_lossy(&o.stdout);
|
|
assert!(t.contains(op.id) && t.contains(op.summary), "{cmd} --help: {t}");
|
|
let (n, v) = op.parts();
|
|
let o = wai_in(&d, &[n, v, "--help"]);
|
|
assert!(o.status.success() && String::from_utf8_lossy(&o.stdout).contains(op.id), "wai {n} {v} --help: {}", text(&o));
|
|
}
|
|
let o = wai_in(&d, &["--help"]);
|
|
assert!(o.status.success());
|
|
assert!(String::from_utf8_lossy(&o.stdout).contains("sends nothing off your machine unless you name a remote service"));
|
|
// The mechanical form runs the operation.
|
|
let o = wai_in(&d, &["registry", "list"]);
|
|
let p = wai_in(&d, &["capabilities"]);
|
|
assert!(o.status.success() && o.stdout == p.stdout, "{}", text(&o));
|
|
}
|
|
|
|
#[test]
|
|
fn the_exit_codes_say_what_happened() {
|
|
let d = scratch("exits");
|
|
// 2: usage.
|
|
assert_eq!(wai_in(&d, &[]).status.code(), Some(2));
|
|
assert_eq!(wai_in(&d, &["inspect"]).status.code(), Some(2));
|
|
assert_eq!(wai_in(&d, &["create", "x", "--frob"]).status.code(), Some(2));
|
|
// 5: a file, an operation, a build that does not hold the operation.
|
|
assert_eq!(wai_in(&d, &["inspect", "no-such.wai"]).status.code(), Some(5));
|
|
let o = wai_in(&d, &["frobnicate", "--json"]);
|
|
assert_eq!(o.status.code(), Some(5), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("operation-not-found")), "{v}");
|
|
#[cfg(not(feature = "generate"))]
|
|
{
|
|
let o = wai_in(&d, &["generate", "--model", "m.json"]);
|
|
assert_eq!(o.status.code(), Some(5), "a build without `generate` does not hold media.generate: {}", text(&o));
|
|
assert!(text(&o).contains("needs a build with the feature generate"), "{}", text(&o));
|
|
}
|
|
// 1: a check that failed (a receipt whose object changed), with its report in data.
|
|
assert!(wai_in(&d, &["hello", "h"]).status.success());
|
|
let obj = std::fs::read_dir(d.join("h")).unwrap().flatten().map(|e| e.path()).find(|p| p.extension().is_some_and(|x| x == "wai")).unwrap();
|
|
let mut b = std::fs::read(&obj).unwrap();
|
|
let n = b.len();
|
|
b[n - 1] ^= 1;
|
|
std::fs::write(&obj, &b).unwrap();
|
|
let o = wai_in(&d, &["verify", "h", "--json"]);
|
|
assert_eq!(o.status.code(), Some(1), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("check-failed")), "{v}");
|
|
assert!(v["data"]["text"].as_str().unwrap().contains("FAIL"), "{v}");
|
|
// 3: a file this user may not read (where the user is not root, who reads anything).
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let locked = d.join("locked.wai");
|
|
std::fs::write(&locked, b"WAI1").unwrap();
|
|
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
|
|
if std::fs::read(&locked).is_err() {
|
|
assert_eq!(wai_in(&d, &["inspect", "locked.wai"]).status.code(), Some(3));
|
|
}
|
|
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o644)).unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn a_local_stack_that_does_not_come_up_is_unavailable_exit_6() {
|
|
use std::os::unix::fs::PermissionsExt;
|
|
let d = scratch("unavailable");
|
|
let fail = d.join("fails.sh");
|
|
std::fs::write(&fail, "#!/bin/sh\nexit 1\n").unwrap();
|
|
std::fs::set_permissions(&fail, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
let web = d.join("web");
|
|
std::fs::create_dir_all(web.join("room")).unwrap();
|
|
std::fs::write(web.join("room/index.html"), "<script src=a.js></script>").unwrap();
|
|
let f = fail.to_str().unwrap();
|
|
let o = wai_in(&d, &["dev", "--port", "0", "--relay", f, "--room", f, "--web", web.to_str().unwrap(), "--check", "--json"]);
|
|
assert_eq!(o.status.code(), Some(6), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!((v["error"]["code"].as_str(), v["error"]["retryable"].as_bool()), (Some("target-unavailable"), Some(true)), "{v}");
|
|
}
|
|
|
|
#[test]
|
|
fn replacing_a_file_the_command_line_did_not_name_waits_for_yes() {
|
|
let d = scratch("confirm");
|
|
std::fs::write(d.join("p.bin"), b"hello").unwrap();
|
|
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let first = std::fs::read(d.join("out.wai")).unwrap();
|
|
std::fs::write(d.join("p.bin"), b"hello, again").unwrap();
|
|
// Off a terminal, without --yes: exit 4, nothing changed, the proposal and the command.
|
|
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]);
|
|
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!(v["error"]["code"], "confirmation-required", "{v}");
|
|
assert_eq!(v["proposed"][0]["replace"], "out.wai", "{v}");
|
|
assert_eq!(v["confirm"], "wai wrap wai.text.zstd p.bin --yes", "{v}");
|
|
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed");
|
|
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]);
|
|
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
|
assert!(text(&o).contains("to go ahead: wai wrap wai.text.zstd p.bin --yes"), "{}", text(&o));
|
|
// With --yes it goes ahead; a named output never waits.
|
|
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success());
|
|
assert_ne!(std::fs::read(d.join("out.wai")).unwrap(), first);
|
|
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "-o", "out.wai"]).status.success());
|
|
// `wai hello` over an earlier run's files waits too.
|
|
assert!(wai_in(&d, &["hello", "h"]).status.success());
|
|
assert_eq!(wai_in(&d, &["hello", "h"]).status.code(), Some(4));
|
|
assert!(wai_in(&d, &["hello", "h", "--yes"]).status.success());
|
|
}
|
|
|
|
#[test]
|
|
fn confirm_is_the_deprecated_spelling_of_yes() {
|
|
let d = scratch("confirm-alias");
|
|
std::fs::write(d.join("p.bin"), b"hello").unwrap();
|
|
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin"]).status.success());
|
|
let first = std::fs::read(d.join("out.wai")).unwrap();
|
|
std::fs::write(d.join("p.bin"), b"hello, again").unwrap();
|
|
// Without either: exit 4, and the confirming command names --yes.
|
|
let o = wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--json"]);
|
|
assert_eq!(o.status.code(), Some(4), "{}", text(&o));
|
|
assert_eq!(envelope(&o)["confirm"], "wai wrap wai.text.zstd p.bin --yes");
|
|
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), first, "nothing changed");
|
|
// --confirm alone goes ahead as --yes does, wherever it is on the line.
|
|
let o = wai_in(&d, &["wrap", "--confirm", "wai.text.zstd", "p.bin"]);
|
|
assert!(o.status.success(), "--confirm is --yes: {}", text(&o));
|
|
let replaced = std::fs::read(d.join("out.wai")).unwrap();
|
|
assert_ne!(replaced, first);
|
|
assert!(wai_in(&d, &["wrap", "wai.text.zstd", "p.bin", "--yes"]).status.success());
|
|
assert_eq!(std::fs::read(d.join("out.wai")).unwrap(), replaced, "the same change");
|
|
// The help names it deprecated, on its own line and in every operation that confirms.
|
|
let h = String::from_utf8_lossy(&wai_in(&d, &["--help"]).stdout).into_owned();
|
|
assert!(h.lines().any(|l| l.trim_start().starts_with("--confirm") && l.contains("deprecated") && l.contains("--yes")), "{h}");
|
|
for op in OPS.iter().filter(|o| o.cli.is_some() && o.confirms() != Confirm::Never) {
|
|
let cmd = op.command();
|
|
let words: Vec<&str> = cmd.split_whitespace().skip(1).chain(["--help"]).collect();
|
|
let t = String::from_utf8_lossy(&wai_in(&d, &words).stdout).into_owned();
|
|
assert!(t.contains("--yes") && t.contains("--confirm, deprecated"), "{} --help: {t}", op.id);
|
|
}
|
|
// The export says so, for agents.
|
|
let e: Value = serde_json::from_slice(&wai_in(&d, &["ops", "export"]).stdout).unwrap();
|
|
let yes = e["agent_flags"].as_array().unwrap().iter().find(|f| f["flag"] == "--yes").unwrap().clone();
|
|
assert_eq!((yes["aliases"][0]["flag"].as_str(), yes["aliases"][0]["deprecated"]["use_instead"].as_str()), (Some("--confirm"), Some("--yes")), "{yes}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_text_command_carries_its_output_in_the_envelope() {
|
|
let d = scratch("envelope");
|
|
let o = wai_in(&d, &["capabilities", "--json"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let v = envelope(&o);
|
|
assert_eq!((v["op"].as_str(), v["ok"].as_bool()), (Some("registry.list"), Some(true)));
|
|
assert!(v["data"]["text"].as_str().unwrap().contains("wai.image.png"), "{v}");
|
|
let o = wai_in(&d, &["inspect", "no-such.wai", "--json"]);
|
|
let v = envelope(&o);
|
|
assert_eq!((v["ok"].as_bool(), v["error"]["code"].as_str()), (Some(false), Some("not-found")), "{v}");
|
|
assert!(v.get("data").is_none(), "{v}");
|
|
}
|
|
|
|
// ─────────────────────────────── --dry-run ───────────────────────────────
|
|
|
|
/// Every file under `d`, with its length and modification time.
|
|
fn snapshot(d: &Path) -> Vec<(String, u64, std::time::SystemTime)> {
|
|
fn walk(root: &Path, p: &Path, out: &mut Vec<(String, u64, std::time::SystemTime)>) {
|
|
for e in std::fs::read_dir(p).unwrap().flatten() {
|
|
let q = e.path();
|
|
let m = e.metadata().unwrap();
|
|
out.push((q.strip_prefix(root).unwrap().display().to_string(), m.len(), m.modified().unwrap()));
|
|
if m.is_dir() {
|
|
walk(root, &q, out);
|
|
}
|
|
}
|
|
}
|
|
let mut v = Vec::new();
|
|
walk(d, d, &mut v);
|
|
v.sort();
|
|
v
|
|
}
|
|
|
|
fn corpus_object(id: &str) -> PathBuf {
|
|
let c = wai_dir().join("http-conformance");
|
|
let listing: Value = serde_json::from_slice(&std::fs::read(c.join("objects.json")).unwrap()).unwrap();
|
|
c.join(listing[id]["path"].as_str().unwrap())
|
|
}
|
|
|
|
/// A running `wai http origin` over `root`, stopped when dropped.
|
|
struct Origin(std::process::Child, u16);
|
|
|
|
impl Origin {
|
|
fn start(root: &Path) -> Origin {
|
|
let mut child = Command::new(env!("CARGO_BIN_EXE_wai"))
|
|
.args(["http", "origin", "--root", root.to_str().unwrap(), "--port", "0"])
|
|
.stdout(Stdio::piped())
|
|
.spawn()
|
|
.unwrap();
|
|
let mut first = String::new();
|
|
BufReader::new(child.stdout.take().unwrap()).read_line(&mut first).unwrap();
|
|
let port = first.trim().rsplit(':').next().unwrap().trim_end_matches('/').parse().unwrap();
|
|
Origin(child, port)
|
|
}
|
|
}
|
|
|
|
impl Drop for Origin {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.kill();
|
|
let _ = self.0.wait();
|
|
}
|
|
}
|
|
|
|
/// A fleet service that answers one request with `body` (status 200), as `wai-fleet` would;
|
|
/// its `http://` address.
|
|
fn fleet_service(body: Value) -> String {
|
|
use std::io::{Read, Write};
|
|
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
|
let addr = format!("http://{}", l.local_addr().unwrap());
|
|
std::thread::spawn(move || {
|
|
if let Ok((mut s, _)) = l.accept() {
|
|
let mut buf = [0u8; 4096];
|
|
let _ = s.read(&mut buf);
|
|
let b = body.to_string();
|
|
let _ = s.write_all(format!("HTTP/1.1 200 X\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{b}", b.len()).as_bytes());
|
|
}
|
|
});
|
|
addr
|
|
}
|
|
|
|
/// A command line that writes, for each operation of this build whose effect is not
|
|
/// `reads`, with what it needs made in `d`: `(id, args, whether it writes without
|
|
/// --dry-run)`. An operation that runs something, or changes only a service, writes nothing
|
|
/// of the project's.
|
|
fn samples(d: &Path, origin_port: u16) -> Vec<(&'static str, Vec<String>, bool)> {
|
|
let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<_>>();
|
|
std::fs::write(d.join("in.txt"), b"a payload to carry").unwrap();
|
|
assert!(wai_in(d, &["wrap", "wai.text.zstd", "in.txt", "-o", "w.wai"]).status.success());
|
|
assert!(wai_in(d, &["encode", "zstd", "in.txt", "-o", "z.wai"]).status.success());
|
|
let reg = d.join("regroot");
|
|
std::fs::create_dir_all(®).unwrap();
|
|
std::fs::copy(wai_dir().join("REGISTRY.md"), reg.join("REGISTRY.md")).unwrap();
|
|
let objs: Vec<String> = ["O9a", "O9b", "O9c"].iter().map(|id| corpus_object(id).to_str().unwrap().to_string()).collect();
|
|
std::fs::write(d.join("classical.m3u8"), "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:0.640,\nc0.m4s\n#EXTINF:0.640,\nc1.m4s\n#EXTINF:0.640,\nc2.m4s\n#EXT-X-ENDLIST\n").unwrap();
|
|
let list = serde_json::json!({
|
|
"uri_prefix": "objects/",
|
|
"variants": [{ "uri": "wai-180.m3u8", "classical": "classical.m3u8", "resolution": [320, 180], "frame_rate_milli": 25000,
|
|
"segments": objs.iter().map(|o| serde_json::json!({ "object": o })).collect::<Vec<_>>() }],
|
|
});
|
|
std::fs::write(d.join("list.json"), serde_json::to_vec_pretty(&list).unwrap()).unwrap();
|
|
let od = corpus_object("OD");
|
|
let cid = wai::http_binding::hex(&wai::http_binding::content_hash(&std::fs::read(&od).unwrap()));
|
|
let web = d.join("web");
|
|
std::fs::create_dir_all(web.join("room")).unwrap();
|
|
std::fs::write(web.join("room/index.html"), "<script src=a.js></script>").unwrap();
|
|
let me = env!("CARGO_BIN_EXE_wai");
|
|
std::fs::create_dir_all(d.join("agentproj")).unwrap();
|
|
#[allow(unused_mut)]
|
|
let mut more: Vec<(&'static str, Vec<String>, bool)> = Vec::new();
|
|
#[cfg(feature = "video_manifest")]
|
|
{
|
|
let sd = d.join("sealdir");
|
|
std::fs::create_dir_all(&sd).unwrap();
|
|
std::fs::copy(d.join("classical.m3u8"), sd.join("classical.m3u8")).unwrap();
|
|
std::fs::copy(d.join("list.json"), sd.join("list.json")).unwrap();
|
|
let o = wai_in(&sd, &["http", "hls", "--objects", "list.json", "--out", "."]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
more.push(("playlist.lock", s(&["http", "seal-playlist", "sealdir/wai-180.m3u8", "--key", &"07".repeat(32), "--out", "sealed"]), true));
|
|
}
|
|
#[cfg(feature = "app")]
|
|
{
|
|
// A Create runtime as wai-app's runtime.mjs lays one out, and a program made with Create.
|
|
use sha2::{Digest, Sha256};
|
|
let rt = d.join("runtime");
|
|
let mut listed = Vec::new();
|
|
for (p, text) in [("create/wai-app-app.js", "export {};\n"), ("create/pkg/wai_app_bg.wasm", "\0asm"), ("room/wai-app-room-client.js", "// the room client\n")] {
|
|
let f = rt.join(p);
|
|
std::fs::create_dir_all(f.parent().unwrap()).unwrap();
|
|
std::fs::write(&f, text).unwrap();
|
|
let h: String = Sha256::digest(text.as_bytes()).iter().map(|x| format!("{x:02x}")).collect();
|
|
listed.push(serde_json::json!({"path": p, "sha256": h}));
|
|
}
|
|
std::fs::write(rt.join("create/wai-app-runtime.json"), serde_json::json!({"files": listed}).to_string()).unwrap();
|
|
std::fs::write(d.join("app.oui"), "root = App([show], \"Friday night\")\nshow = Show([player, chat], \"Live\", 2)\nplayer = Player()\nchat = Chat(\"Talk\")\n").unwrap();
|
|
more.push(("app.export", s(&["app", "export", "app.oui", "appproj", "--runtime", "runtime"]), true));
|
|
}
|
|
#[cfg(feature = "fleet")]
|
|
{
|
|
// A node's enrolment written here; a report's bundle exported from a fleet service (the
|
|
// corpus's week, which this build verifies before it writes); and a report the service
|
|
// would sign, which a dry run does not ask for.
|
|
std::fs::write(d.join("org.key"), "07".repeat(32)).unwrap();
|
|
let corpus: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("live-conformance/fleet/vectors.json")).unwrap()).unwrap();
|
|
let week = corpus["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "a_week").unwrap().clone();
|
|
let digest = week["outcome"]["digest"].as_str().unwrap().to_string();
|
|
let url = fleet_service(serde_json::json!({"report": digest, "outcome": week["outcome"], "bundle": week["bundle"]}));
|
|
more.push(("enrolment.create", s(&["enrolment", "create", &"11".repeat(32), "--role", "relay", "--key", "org.key", "--out", "node.wfe"]), true));
|
|
more.push(("report.export", s(&["report", "export", &digest, "--target", &url, "--out", "week.wai-fleet.json"]), true));
|
|
more.push(("report.create", s(&["report", "create", "--target", "http://127.0.0.1:9", "--start", "0", "--end", "1"]), false));
|
|
}
|
|
#[cfg(feature = "channel_lineup")]
|
|
{
|
|
// The sample channel of extensions/channel-lineup.md §11, imported here; and the guide of
|
|
// its committed receipt and lineup.
|
|
let c = wai_dir().join("live-conformance/channels");
|
|
let f = |n: &str| c.join(n).to_str().unwrap().to_string();
|
|
let corpus: Value = serde_json::from_slice(&std::fs::read(c.join("vectors.json")).unwrap()).unwrap();
|
|
std::fs::write(d.join("channel-operator.key"), corpus["operator"]["seed"].as_str().unwrap()).unwrap();
|
|
let l = corpus["imports"][0]["outcome"]["lineup"].as_str().unwrap();
|
|
let lineup: Vec<u8> = (0..l.len()).step_by(2).map(|i| u8::from_str_radix(&l[i..i + 2], 16).unwrap()).collect();
|
|
std::fs::write(d.join("sample.wlu"), lineup).unwrap();
|
|
let (x, a, b, p, art) = (f("sample.xmltv.xml"), f("sample-a.mrss.xml"), f("sample-b.mrss.xml"), f("sample.m3u8"), f(""));
|
|
more.push((
|
|
"channel.import",
|
|
s(&["channel", "import", "--channel", "sample-1", "--xmltv", &x, "--mrss", &a, "--mrss-owner", "owner-a", "--mrss", &b, "--mrss-owner", "owner-b", "--owner", "owner-c", "--playlist", &p, "--artwork", &art, "--key", "channel-operator.key", "--out-dir", "channel-out"]),
|
|
true,
|
|
));
|
|
more.push(("channel.guide", s(&["channel", "guide", "--receipt", &f("sample.channel.json"), "--lineup", "sample.wlu", "--out", "guide.xml"]), true));
|
|
}
|
|
#[cfg(feature = "revenue_share")]
|
|
{
|
|
// The revenue share: a contract written from its terms, an owner's countersignature added
|
|
// to an offered contract, and a statement of the share corpus's October, recomputed here
|
|
// from its runs and ledger documents.
|
|
use wai::revenue_share::{corpus, terms};
|
|
let hexs = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
|
let (operator, owner) = (corpus::key("operator"), corpus::key("owner-a"));
|
|
std::fs::write(d.join("operator.key"), hexs(&operator.to_bytes())).unwrap();
|
|
std::fs::write(d.join("owner-a.key"), hexs(&owner.to_bytes())).unwrap();
|
|
let a = hexs(&owner.verifying_key().to_bytes());
|
|
let t = serde_json::json!({
|
|
"start": 0, "end": 10, "currency": "EUR", "channel": "fast-7", "channel_signer": "22".repeat(32), "rule": "preceding", "between": 0,
|
|
"carriers": [{"room": "11".repeat(32)}], "owners": [a],
|
|
"splits": [{"operator": 500_000, "owners": {a.clone(): 500_000}}], "assets": {"a/ep-1": 0},
|
|
});
|
|
std::fs::write(d.join("terms.json"), t.to_string()).unwrap();
|
|
std::fs::write(d.join("offered.wrc"), terms(&t).unwrap().offer(&operator).to_bytes()).unwrap();
|
|
let share: Value = serde_json::from_slice(&std::fs::read(wai_dir().join("live-conformance/share/vectors.json")).unwrap()).unwrap();
|
|
let ledger = |n: &str| {
|
|
let l = &share["ledgers"][n];
|
|
let sources: Vec<Value> = l["sources"].as_array().unwrap().iter().map(|s| share["sources"][s.as_str().unwrap()].clone()).collect();
|
|
serde_json::json!({"tariff": l["tariff"], "invoice": l["invoice"], "previous": l["previous"], "sources": sources})
|
|
};
|
|
let inputs = serde_json::json!({
|
|
"contract": share["contracts"]["preceding"], "contracts": [],
|
|
"runs": [share["runs"]["oct_16"], share["runs"]["oct_25"]], "ledgers": [ledger("day_16"), ledger("day_25")],
|
|
});
|
|
std::fs::write(d.join("inputs.json"), inputs.to_string()).unwrap();
|
|
more.push(("contract.create", s(&["contract", "create", "terms.json", "--key", "operator.key", "--out", "c.wrc"]), true));
|
|
more.push(("contract.update", s(&["contract", "accept", "offered.wrc", "--key", "owner-a.key", "--out", "accepted.wrc"]), true));
|
|
more.push((
|
|
"statement.create",
|
|
s(&["statement", "create", "inputs.json", "--party", &a, "--start", "1790812800000000", "--end", "1793491200000000", "--key", "operator.key", "--out", "a.wai-share.json"]),
|
|
true,
|
|
));
|
|
}
|
|
#[cfg(feature = "opportunity_report")]
|
|
{
|
|
// The opportunity report: owner B's report of the opportunity corpus's day, recomputed
|
|
// here from its runs and room, and the check of B's reported statement beside it.
|
|
use wai::opportunity_report::corpus;
|
|
let hexs = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
|
let w = corpus::world().unwrap();
|
|
let b = &w.reports["owner_b"];
|
|
std::fs::write(d.join("owner-b.key"), hexs(&corpus::key("owner-b").to_bytes())).unwrap();
|
|
std::fs::write(d.join("opp-inputs.json"), w.pools.report_document(b, false).to_string()).unwrap();
|
|
std::fs::write(d.join("opp-report.json"), w.pools.report_document(b, true).to_string()).unwrap();
|
|
std::fs::write(d.join("reported.json"), w.pools.reported["b_programmes"]["text"].as_str().unwrap()).unwrap();
|
|
let party = hexs(&corpus::pk("owner-b"));
|
|
let (start, end) = (corpus::DAY16.to_string(), corpus::DAY17.to_string());
|
|
more.push(("opportunity.create", s(&["opportunity", "create", "opp-inputs.json", "--party", &party, "--start", &start, "--end", &end, "--key", "owner-b.key", "--out", "b.wai-opportunity.json"]), true));
|
|
more.push(("statement.check", s(&["statement", "check", "opp-report.json", "reported.json", "--key", "owner-b.key", "--out", "b.wai-check.json"]), true));
|
|
}
|
|
#[cfg(feature = "generate")]
|
|
{
|
|
let model = wai_dir().join("wai-rs/tests/vectors/generate/generate_spectral_model.json");
|
|
more.push(("media.generate", s(&["generate", "--model", model.to_str().unwrap(), "--prompt", "a calm spectral horizon", "--seed", "7", "--steps", "1", "--size", "16x16", "-o", "g.rgb"]), true));
|
|
}
|
|
#[cfg(feature = "score")]
|
|
{
|
|
let notes = wai_dir().join("score-conformance/notes/scale.json");
|
|
let notes = notes.to_str().unwrap();
|
|
assert!(wai_in(d, &["score", "build", notes, "-o", "s0.wscr"]).status.success());
|
|
more.push(("score.build", s(&["score", "build", notes, "-o", "s.wscr"]), true));
|
|
more.push(("score.decode", s(&["score", "render", "s0.wscr", "--pcm", "s.s16"]), true));
|
|
}
|
|
#[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))]
|
|
{
|
|
let frame: Vec<u8> = (0..6 * 10 * 3).map(|i| ((i * 13) % 256) as u8).collect();
|
|
std::fs::write(d.join("f.rgb"), &frame).unwrap();
|
|
for args in [
|
|
&["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b0.wai"][..],
|
|
&["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--target", "f.rgb", "--q", "1", "-o", "l0.wai"],
|
|
&["stage", "pack", "b0.wai", "l0.wai", "-o", "x0.wai"],
|
|
] {
|
|
let o = wai_in(d, args);
|
|
assert!(o.status.success(), "{args:?}: {}", text(&o));
|
|
}
|
|
more.push(("stage.build", s(&["stage", "base", "--frames", "f.rgb", "--dims", "1,6,10", "--q", "40", "-o", "b.wai"]), true));
|
|
more.push(("stage.encode", s(&["stage", "refine", "--base", "b0.wai", "--span", "0:1", "--target", "f.rgb", "--q", "1", "-o", "l.wai"]), true));
|
|
more.push(("stage.wrap", s(&["stage", "pack", "b0.wai", "l0.wai", "-o", "x.wai"]), true));
|
|
more.push(("stage.unwrap", s(&["stage", "explode", "x0.wai", "0", "-o", "e.wai"]), true));
|
|
}
|
|
let mut v = vec![
|
|
("toolchain.test", s(&["hello", "hello-out"]), true),
|
|
("project.create", s(&["create", "proj", "--template", "video-room"]), true),
|
|
("envelope.wrap", s(&["wrap", "wai.text.zstd", "in.txt", "-o", "w2.wai"]), true),
|
|
("envelope.unwrap", s(&["unwrap", "w.wai", "-o", "payload.out"]), true),
|
|
("media.encode", s(&["encode", "zstd", "in.txt", "-o", "e.wai"]), true),
|
|
("media.decode", s(&["decode", "z.wai", "-o", "decoded.txt"]), true),
|
|
("registry.export", s(&["registry", "export", "--root", reg.to_str().unwrap()]), true),
|
|
("ops.export", s(&["ops", "export", "--out", "ops-out.json"]), true),
|
|
("http.export", s(&["http", "publish", corpus_object("O3").to_str().unwrap(), "--out", "pub"]), true),
|
|
("hls.build", s(&["http", "hls", "--objects", "list.json", "--out", "."]), true),
|
|
("dash.build", s(&["http", "mpd", "--objects", "list.json", "--out", "wai.mpd"]), true),
|
|
("corpus.generate", s(&["http", "vectors", "diff", ".", "--seed", "1", "--count", "1", "--out", "diff.bin"]), true),
|
|
("http.load", s(&["http", "bench", "--url", &format!("http://127.0.0.1:{origin_port}/{cid}.wai#h=1009&n=53794"), "--runs", "1", "--advertise", "wai.video.av1", "--out", "bench.json"]), true),
|
|
("dev.start", s(&["dev", "--port", "0", "--relay", me, "--room", me, "--web", web.to_str().unwrap()]), false),
|
|
("http.start", s(&["http", "origin", "--root", ".", "--port", "0"]), false),
|
|
("conformance.run", s(&["conform", "verifier", "--suite", "jwp", "--impl", &format!("'{}' verdicts {{corpus}}", env!("CARGO_BIN_EXE_wai_jwp_vectors")), "--root", wai_dir().to_str().unwrap(), "--out", "jwp.wai-conform.json"]), true),
|
|
("agent.setup", s(&["setup-agent", "--dir", "agentproj"]), true),
|
|
("mcp.start", s(&["mcp", "--project", "."]), false),
|
|
// The packer given is this binary, which is no packer: the dry run names the package's
|
|
// directory and runs nothing, and a real run writes nothing here. A pack through this
|
|
// door with wai-pack itself runs in wai-pack.yml.
|
|
("package.build", s(&["package", "build", "--packer", me, "--policy", "p.toml", "--out", "pkg", "--key", "k.hex"]), false),
|
|
];
|
|
v.extend(more);
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn every_operation_that_is_not_read_only_writes_nothing_with_dry_run() {
|
|
let root = scratch("dry-run");
|
|
let served = root.join("served");
|
|
std::fs::create_dir_all(&served).unwrap();
|
|
assert!(wai_in(&served, &["http", "publish", corpus_object("OD").to_str().unwrap(), "--out", "."]).status.success());
|
|
let origin = Origin::start(&served);
|
|
let work = root.join("work");
|
|
std::fs::create_dir_all(&work).unwrap();
|
|
let table = samples(&work, origin.1);
|
|
// Every such operation this build holds has a sample: a new one without --dry-run
|
|
// support cannot join the catalogue unnoticed.
|
|
for op in OPS.iter().filter(|o| o.cli.is_some() && o.effect != Effect::Reads && o.available()) {
|
|
assert!(table.iter().any(|(id, _, _)| *id == op.id), "{} is not read-only and has no --dry-run sample here", op.id);
|
|
}
|
|
for (id, args, writes) in &table {
|
|
let before = snapshot(&work);
|
|
let mut dry: Vec<&str> = args.iter().map(String::as_str).collect();
|
|
dry.push("--dry-run");
|
|
let o = wai_in(&work, &dry);
|
|
assert!(o.status.success(), "{id} --dry-run: {}", text(&o));
|
|
assert_eq!(snapshot(&work), before, "{id} --dry-run wrote: {}", text(&o));
|
|
let t = text(&o);
|
|
assert!(t.contains("dry run") || t.contains("dry-run"), "{id} --dry-run says so: {t}");
|
|
// The same with --json: the envelope says it was a dry run, and lists the plan.
|
|
dry.push("--json");
|
|
let o = wai_in(&work, &dry);
|
|
assert_eq!(snapshot(&work), before, "{id} --dry-run --json wrote: {}", text(&o));
|
|
if *writes {
|
|
let v = envelope(&o);
|
|
assert_eq!((v["ok"].as_bool(), v["dry_run"].as_bool()), (Some(true), Some(true)), "{id}: {v}");
|
|
assert!(!v["plan"].as_array().unwrap().is_empty(), "{id}: the plan names what it would write: {v}");
|
|
// Without --dry-run the same command writes: the dry run was of a write.
|
|
let refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
|
let o = wai_in(&work, &refs);
|
|
assert!(o.status.success(), "{id}: {}", text(&o));
|
|
assert_ne!(snapshot(&work), before, "{id} without --dry-run writes");
|
|
}
|
|
}
|
|
drop(origin);
|
|
}
|