| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
use serde_json::Value; |
| 5 |
use std::path::Path; |
| 6 |
use std::process::Command; |
| 7 |
|
| 8 |
fn oxy(dir: &Path, home: &Path, args: &[&str]) -> (i32, Vec<u8>, String) { |
| 9 |
let out = Command::new(env!("CARGO_BIN_EXE_oxy")) |
| 10 |
.arg("--plain") |
| 11 |
.args(args) |
| 12 |
.current_dir(dir) |
| 13 |
.env("OXY_HOME", home) |
| 14 |
.env("NO_COLOR", "1") |
| 15 |
.output() |
| 16 |
.unwrap(); |
| 17 |
( |
| 18 |
out.status.code().unwrap_or(-1), |
| 19 |
out.stdout, |
| 20 |
String::from_utf8_lossy(&out.stderr).to_string(), |
| 21 |
) |
| 22 |
} |
| 23 |
|
| 24 |
fn json(dir: &Path, home: &Path, args: &[&str]) -> Value { |
| 25 |
let (code, out, err) = oxy(dir, home, args); |
| 26 |
assert!( |
| 27 |
code == 0 || code == 2 || code == 1, |
| 28 |
"oxy {args:?} exited {code}: {err}" |
| 29 |
); |
| 30 |
serde_json::from_slice(&out).unwrap_or_else(|e| { |
| 31 |
panic!( |
| 32 |
"oxy {args:?} is not JSON: {e}\n{}", |
| 33 |
String::from_utf8_lossy(&out) |
| 34 |
) |
| 35 |
}) |
| 36 |
} |
| 37 |
|
| 38 |
fn write(dir: &Path, rel: &str, content: &str) { |
| 39 |
let p = dir.join(rel); |
| 40 |
std::fs::create_dir_all(p.parent().unwrap()).unwrap(); |
| 41 |
std::fs::write(p, content).unwrap(); |
| 42 |
} |
| 43 |
|
| 44 |
#[test] |
| 45 |
fn machine_interface_round_trips() { |
| 46 |
let tmp = tempfile::tempdir().unwrap(); |
| 47 |
let home = tmp.path().join("home"); |
| 48 |
std::fs::create_dir_all(&home).unwrap(); |
| 49 |
let origin = tmp.path().join("origin"); |
| 50 |
assert_eq!( |
| 51 |
oxy(tmp.path(), &home, &["init", origin.to_str().unwrap()]).0, |
| 52 |
0 |
| 53 |
); |
| 54 |
let repo = tmp.path().join("repo"); |
| 55 |
assert_eq!( |
| 56 |
oxy( |
| 57 |
tmp.path(), |
| 58 |
&home, |
| 59 |
&["clone", origin.to_str().unwrap(), repo.to_str().unwrap()] |
| 60 |
) |
| 61 |
.0, |
| 62 |
0 |
| 63 |
); |
| 64 |
let r = repo.as_path(); |
| 65 |
assert_eq!( |
| 66 |
oxy( |
| 67 |
r, |
| 68 |
&home, |
| 69 |
&[ |
| 70 |
"identity", |
| 71 |
"create", |
| 72 |
"--name", |
| 73 |
"Ada", |
| 74 |
"--email", |
| 75 |
"[email protected]" |
| 76 |
] |
| 77 |
) |
| 78 |
.0, |
| 79 |
0 |
| 80 |
); |
| 81 |
assert_eq!( |
| 82 |
oxy(r, &home, &["config", "--user", "core.sign", "change"]).0, |
| 83 |
0 |
| 84 |
); |
| 85 |
|
| 86 |
let v = json(r, &home, &["version", "--json"]); |
| 87 |
assert_eq!(v["protocol"], 1); |
| 88 |
let root = json(r, &home, &["root", "--json"]); |
| 89 |
assert_eq!(root["bookmark"], "main"); |
| 90 |
assert!(root["root"].as_str().unwrap().ends_with("repo")); |
| 91 |
|
| 92 |
write(r, "a.txt", "1\n2\n3\n"); |
| 93 |
write(r, "b.txt", "b\n"); |
| 94 |
let st = json(r, &home, &["status", "--json"]); |
| 95 |
assert_eq!(st["paths"].as_array().unwrap().len(), 2); |
| 96 |
assert_eq!(oxy(r, &home, &["commit", "-m", "base"]).0, 0); |
| 97 |
write(r, "a.txt", "0\n1\n2\n3\n"); |
| 98 |
write(r, "b.txt", "b\nbb\n"); |
| 99 |
write(r, "c.txt", "c\n"); |
| 100 |
|
| 101 |
|
| 102 |
let d = json(r, &home, &["diff", "--json"]); |
| 103 |
let files = d["files"].as_array().unwrap(); |
| 104 |
assert_eq!(files.len(), 3, "{d}"); |
| 105 |
let a = files.iter().find(|f| f["path"] == "a.txt").unwrap(); |
| 106 |
assert_eq!(a["kind"], "modified"); |
| 107 |
assert_eq!(a["hunks"][0]["base_line"], 0); |
| 108 |
assert_eq!(a["hunks"][0]["added"][0], "0\n"); |
| 109 |
let c = files.iter().find(|f| f["path"] == "c.txt").unwrap(); |
| 110 |
assert_eq!(c["kind"], "added"); |
| 111 |
let hunks = json(r, &home, &["hunks", "--json"]); |
| 112 |
let hunks = hunks.as_array().unwrap(); |
| 113 |
assert!(hunks.len() >= 3, "{hunks:?}"); |
| 114 |
let b_hunk = hunks.iter().find(|h| h["path"] == "b.txt").unwrap()["index"] |
| 115 |
.as_u64() |
| 116 |
.unwrap(); |
| 117 |
|
| 118 |
assert_eq!( |
| 119 |
oxy( |
| 120 |
r, |
| 121 |
&home, |
| 122 |
&["commit", "-m", "b only", "--hunks", &b_hunk.to_string()] |
| 123 |
) |
| 124 |
.0, |
| 125 |
0 |
| 126 |
); |
| 127 |
assert_eq!(oxy(r, &home, &["discard", "c.txt"]).0, 0); |
| 128 |
assert!(!r.join("c.txt").exists()); |
| 129 |
let hunks = json(r, &home, &["hunks", "--json"]); |
| 130 |
let a_hunk = hunks |
| 131 |
.as_array() |
| 132 |
.unwrap() |
| 133 |
.iter() |
| 134 |
.find(|h| h["path"] == "a.txt") |
| 135 |
.unwrap()["index"] |
| 136 |
.as_u64() |
| 137 |
.unwrap(); |
| 138 |
assert_eq!( |
| 139 |
oxy(r, &home, &["discard", "--hunks", &a_hunk.to_string()]).0, |
| 140 |
0 |
| 141 |
); |
| 142 |
assert_eq!( |
| 143 |
std::fs::read_to_string(r.join("a.txt")).unwrap(), |
| 144 |
"1\n2\n3\n" |
| 145 |
); |
| 146 |
let st = json(r, &home, &["status", "--json"]); |
| 147 |
assert_eq!(st["clean"], true, "{st}"); |
| 148 |
|
| 149 |
|
| 150 |
let (code, out, _) = oxy(r, &home, &["cat", "b.txt"]); |
| 151 |
assert_eq!((code, out.as_slice()), (0, b"b\nbb\n".as_slice())); |
| 152 |
let (code, out, _) = oxy(r, &home, &["cat", "b.txt", "-r", "main"]); |
| 153 |
assert_eq!((code, out.as_slice()), (0, b"b\nbb\n".as_slice())); |
| 154 |
|
| 155 |
|
| 156 |
let log = json(r, &home, &["log", "--json"]); |
| 157 |
let changes = log["changes"].as_array().unwrap(); |
| 158 |
assert_eq!(changes.len(), 3, "working copy plus two changes: {log}"); |
| 159 |
let head = changes.iter().find(|c| c["message"] == "b only").unwrap(); |
| 160 |
assert_eq!(head["signature"]["status"], "signed"); |
| 161 |
assert_eq!(head["author"], "Ada"); |
| 162 |
let hash = head["hash"].as_str().unwrap().to_string(); |
| 163 |
let by_path = json(r, &home, &["log", "--json", "--path", "b.txt"]); |
| 164 |
assert_eq!(by_path["changes"].as_array().unwrap().len(), 2, "{by_path}"); |
| 165 |
let show = json(r, &home, &["show", "--json", &format!("#{}", &hash[..12])]); |
| 166 |
assert_eq!(show["files"][0]["path"], "b.txt"); |
| 167 |
let verify = json(r, &home, &["verify", "--json"]); |
| 168 |
assert!(verify |
| 169 |
.as_array() |
| 170 |
.unwrap() |
| 171 |
.iter() |
| 172 |
.all(|v| v["status"] == "signed")); |
| 173 |
let ops = json(r, &home, &["op", "log", "--json"]); |
| 174 |
assert!(ops[0]["current"] == true && ops.as_array().unwrap().len() > 3); |
| 175 |
assert_eq!(oxy(r, &home, &["bookmark", "create", "feature"]).0, 0); |
| 176 |
let bms = json(r, &home, &["bookmark", "list", "--json"]); |
| 177 |
assert_eq!( |
| 178 |
bms.as_array() |
| 179 |
.unwrap() |
| 180 |
.iter() |
| 181 |
.map(|b| b["name"].as_str().unwrap()) |
| 182 |
.collect::<Vec<_>>(), |
| 183 |
vec!["feature", "main"] |
| 184 |
); |
| 185 |
let remotes = json(r, &home, &["remote", "list", "--json"]); |
| 186 |
assert_eq!(remotes[0]["name"], "origin"); |
| 187 |
let push = json(r, &home, &["push", "--json"]); |
| 188 |
assert_eq!(push[0]["ok"], true, "{push}"); |
| 189 |
let fetch = json(r, &home, &["fetch", "--json"]); |
| 190 |
assert_eq!(fetch["remote"], "origin"); |
| 191 |
let pull = json(r, &home, &["pull", "--json"]); |
| 192 |
assert_eq!(pull["changed"], false); |
| 193 |
assert_eq!(json(r, &home, &["resolve", "--json"])["conflicted"], false); |
| 194 |
assert!(json(r, &home, &["submodule", "status", "--json"]) |
| 195 |
.as_array() |
| 196 |
.unwrap() |
| 197 |
.is_empty()); |
| 198 |
assert!(json(r, &home, &["bisect", "status", "--json"]).is_null()); |
| 199 |
assert!(json(r, &home, &["policy", "show", "--json"]).is_null()); |
| 200 |
assert_eq!(oxy(r, &home, &["policy", "init"]).0, 0); |
| 201 |
let policy = json(r, &home, &["policy", "show", "--json"]); |
| 202 |
assert_eq!(policy["roots"][0]["name"], "Ada"); |
| 203 |
assert_eq!( |
| 204 |
oxy(r, &home, &["tag", "create", "v1", "-m", "first tag"]).0, |
| 205 |
0 |
| 206 |
); |
| 207 |
let tags = json(r, &home, &["tag", "list", "--json"]); |
| 208 |
assert_eq!( |
| 209 |
(tags[0]["name"].as_str(), tags[0]["message"].as_str()), |
| 210 |
(Some("v1"), Some("first tag")) |
| 211 |
); |
| 212 |
let ws = json(r, &home, &["workspace", "list", "--json"]); |
| 213 |
assert_eq!( |
| 214 |
(ws.as_array().unwrap().len(), ws[0]["current"].as_bool()), |
| 215 |
(1, Some(true)) |
| 216 |
); |
| 217 |
} |
| 218 |
|
| 219 |
|
| 220 |
|
| 221 |
#[test] |
| 222 |
fn commit_from_a_file_and_describe_for_builds() { |
| 223 |
let tmp = tempfile::tempdir().unwrap(); |
| 224 |
let home = tmp.path().join("home"); |
| 225 |
std::fs::create_dir_all(&home).unwrap(); |
| 226 |
let dir = tmp.path().join("r"); |
| 227 |
std::fs::create_dir_all(&dir).unwrap(); |
| 228 |
assert_eq!(oxy(&dir, &home, &["init"]).0, 0); |
| 229 |
write(&dir, "a.txt", "a\n"); |
| 230 |
write( |
| 231 |
tmp.path(), |
| 232 |
"msg.txt", |
| 233 |
"Subject from a file\n\nA body\nof two lines.\n", |
| 234 |
); |
| 235 |
let msg = tmp.path().join("msg.txt"); |
| 236 |
let (code, _, err) = oxy(&dir, &home, &["commit", "-F", msg.to_str().unwrap()]); |
| 237 |
assert_eq!(code, 0, "{err}"); |
| 238 |
let log = json(&dir, &home, &["log", "--json", "-n", "2"]); |
| 239 |
assert_eq!( |
| 240 |
log["changes"][1]["message"], |
| 241 |
"Subject from a file\n\nA body\nof two lines." |
| 242 |
); |
| 243 |
assert_ne!(oxy(&dir, &home, &["commit"]).0, 0, "a message is required"); |
| 244 |
|
| 245 |
|
| 246 |
let d = json(&dir, &home, &["describe", "--json"]); |
| 247 |
assert_eq!( |
| 248 |
(d["tag"].clone(), d["dirty"].clone()), |
| 249 |
(Value::Null, Value::Bool(false)) |
| 250 |
); |
| 251 |
let change = d["change"].as_str().unwrap().to_string(); |
| 252 |
assert_eq!(d["describe"], format!("main-{}", &change[..8])); |
| 253 |
assert_eq!( |
| 254 |
oxy(&dir, &home, &["tag", "create", "v0.1.0", "-m", "first"]).0, |
| 255 |
0 |
| 256 |
); |
| 257 |
let (_, out, _) = oxy(&dir, &home, &["describe"]); |
| 258 |
assert_eq!(String::from_utf8_lossy(&out).trim(), "v0.1.0"); |
| 259 |
let (code, out, err) = oxy(&dir, &home, &["show", "v0.1.0"]); |
| 260 |
assert_eq!(code, 0, "a tag names its change: {err}"); |
| 261 |
assert!(String::from_utf8_lossy(&out).contains("Subject from a file")); |
| 262 |
write(&dir, "b.txt", "b\n"); |
| 263 |
let d = json(&dir, &home, &["describe", "--json"]); |
| 264 |
assert_eq!(d["describe"], "v0.1.0-dirty"); |
| 265 |
assert_eq!(oxy(&dir, &home, &["commit", "-m", "b"]).0, 0); |
| 266 |
write(&dir, "c.txt", "c\n"); |
| 267 |
assert_eq!(oxy(&dir, &home, &["commit", "-m", "c"]).0, 0); |
| 268 |
let d = json(&dir, &home, &["describe", "--json"]); |
| 269 |
let change = d["change"].as_str().unwrap(); |
| 270 |
assert_eq!(d["distance"], 2); |
| 271 |
assert_eq!(d["describe"], format!("v0.1.0-2-{}", &change[..8])); |
| 272 |
} |
| 273 |
|
| 274 |
|
| 275 |
#[test] |
| 276 |
fn the_history_of_a_function() { |
| 277 |
let tmp = tempfile::tempdir().unwrap(); |
| 278 |
let home = tmp.path().join("home"); |
| 279 |
std::fs::create_dir_all(&home).unwrap(); |
| 280 |
let dir = tmp.path().join("r"); |
| 281 |
std::fs::create_dir_all(&dir).unwrap(); |
| 282 |
assert_eq!(oxy(&dir, &home, &["init"]).0, 0); |
| 283 |
write(&dir, "f.rs", "use x;\nfn a() {\n one();\n}\n"); |
| 284 |
assert_eq!(oxy(&dir, &home, &["commit", "-m", "add f"]).0, 0); |
| 285 |
write(&dir, "f.rs", "use y;\nfn a() {\n one();\n}\n"); |
| 286 |
assert_eq!(oxy(&dir, &home, &["commit", "-m", "import"]).0, 0); |
| 287 |
write(&dir, "f.rs", "use y;\nfn a() {\n two();\n}\n"); |
| 288 |
assert_eq!(oxy(&dir, &home, &["commit", "-m", "body"]).0, 0); |
| 289 |
let v = json(&dir, &home, &["log", "--json", "-L", "2,4:f.rs"]); |
| 290 |
let messages: Vec<&str> = v["changes"] |
| 291 |
.as_array() |
| 292 |
.unwrap() |
| 293 |
.iter() |
| 294 |
.map(|c| c["message"].as_str().unwrap()) |
| 295 |
.collect(); |
| 296 |
assert_eq!(messages, ["body", "add f"], "not the import"); |
| 297 |
let (code, _, err) = oxy(&dir, &home, &["log", "-L", "2:f.rs"]); |
| 298 |
assert_ne!(code, 0); |
| 299 |
assert!(err.contains("START,END:PATH"), "{err}"); |
| 300 |
} |
| 301 |
|
| 302 |
#[test] |
| 303 |
fn receipts_verify_import_list_show_and_export() { |
| 304 |
use oxy_core::hash::{ObjectRef, ObjectType}; |
| 305 |
use oxy_core::identity::SecretKey; |
| 306 |
use oxy_core::objects::StateObject; |
| 307 |
use oxy_core::receipt::{Receipt, SignedReceipt}; |
| 308 |
let tmp = tempfile::tempdir().unwrap(); |
| 309 |
let home = tmp.path().join("home"); |
| 310 |
std::fs::create_dir_all(&home).unwrap(); |
| 311 |
let r = tmp.path().join("repo"); |
| 312 |
assert_eq!(oxy(tmp.path(), &home, &["init", r.to_str().unwrap()]).0, 0); |
| 313 |
write(&r, "a.txt", "a\n"); |
| 314 |
assert_eq!(oxy(&r, &home, &["commit", "-m", "A"]).0, 0); |
| 315 |
|
| 316 |
let repo = oxy_core::repo::Repo::open(&r).unwrap(); |
| 317 |
let heads = repo.bookmark_heads().unwrap(); |
| 318 |
let state = oxy_core::hash::hash_object(ObjectType::State, &StateObject::new(heads).encode()); |
| 319 |
let receipt = Receipt { |
| 320 |
job_name: "test".into(), |
| 321 |
pipeline_hash: ObjectRef([1; 32]), |
| 322 |
scope_tree_hash: ObjectRef([2; 32]), |
| 323 |
inputs: vec![], |
| 324 |
environment: ObjectRef([3; 32]), |
| 325 |
outputs: vec![], |
| 326 |
exit_code: 0, |
| 327 |
duration_ms: 2500, |
| 328 |
}; |
| 329 |
let sk = SecretKey::generate(); |
| 330 |
let runner = sk.identity("earth-1", "runner@earth", 1); |
| 331 |
let sr = SignedReceipt::sign(receipt, repo.id().unwrap(), state, "demo#7", &runner, &sk); |
| 332 |
let file = tmp.path().join("r.receipt"); |
| 333 |
std::fs::write(&file, sr.encode()).unwrap(); |
| 334 |
let f = file.to_str().unwrap(); |
| 335 |
|
| 336 |
let v = json(&r, &home, &["receipt", "verify", f, "--json"]); |
| 337 |
assert_eq!( |
| 338 |
(v["valid"].as_bool(), v["job"].as_str(), v["run"].as_str()), |
| 339 |
(Some(true), Some("test"), Some("demo#7")) |
| 340 |
); |
| 341 |
assert!(v["attest"].is_null()); |
| 342 |
|
| 343 |
assert!(json(&r, &home, &["receipt", "list", "--json"]) |
| 344 |
.as_array() |
| 345 |
.unwrap() |
| 346 |
.is_empty()); |
| 347 |
assert_eq!(oxy(&r, &home, &["receipt", "import", f]).0, 0); |
| 348 |
let list = json(&r, &home, &["receipt", "list", "--json"]); |
| 349 |
let list = list.as_array().unwrap(); |
| 350 |
assert_eq!(list.len(), 1); |
| 351 |
assert_eq!(list[0]["runner_name"], "earth-1"); |
| 352 |
assert_eq!(list[0]["state"], state.to_hex()); |
| 353 |
let attest = list[0]["attest"].as_str().unwrap().to_string(); |
| 354 |
let shown = json(&r, &home, &["receipt", "show", &attest[..10], "--json"]); |
| 355 |
assert_eq!(shown["duration_ms"], 2500); |
| 356 |
|
| 357 |
let out = tmp.path().join("out.receipt"); |
| 358 |
assert_eq!( |
| 359 |
oxy( |
| 360 |
&r, |
| 361 |
&home, |
| 362 |
&["receipt", "export", &attest[..10], out.to_str().unwrap()] |
| 363 |
) |
| 364 |
.0, |
| 365 |
0 |
| 366 |
); |
| 367 |
assert_eq!(std::fs::read(&out).unwrap(), sr.encode()); |
| 368 |
|
| 369 |
|
| 370 |
|
| 371 |
let prov = tmp.path().join("prov.json"); |
| 372 |
let p = prov.to_str().unwrap(); |
| 373 |
let slsa = ["receipt", "export", &attest[..10], p, "--format", "slsa"]; |
| 374 |
let (code, _, err) = oxy(&r, &home, &slsa); |
| 375 |
assert_ne!(code, 0); |
| 376 |
assert!(err.contains("oxy identity create"), "{err}"); |
| 377 |
let ident = [ |
| 378 |
"identity", |
| 379 |
"create", |
| 380 |
"--name", |
| 381 |
"Ada", |
| 382 |
"--email", |
| 383 |
"[email protected]", |
| 384 |
]; |
| 385 |
assert_eq!(oxy(&r, &home, &ident).0, 0); |
| 386 |
assert_eq!(oxy(&r, &home, &slsa).0, 0); |
| 387 |
let env: serde_json::Value = serde_json::from_slice(&std::fs::read(&prov).unwrap()).unwrap(); |
| 388 |
assert_eq!(env["payloadType"], oxy_core::dsse::IN_TOTO); |
| 389 |
let payload = oxy_core::dsse::base64_decode(env["payload"].as_str().unwrap()).unwrap(); |
| 390 |
let st: serde_json::Value = serde_json::from_slice(&payload).unwrap(); |
| 391 |
assert_eq!(st["predicateType"], "https://slsa.dev/provenance/v1"); |
| 392 |
assert_eq!(st["subject"][0]["digest"]["oxygen-state"], state.to_hex()); |
| 393 |
let run = &st["predicate"]["runDetails"]; |
| 394 |
assert_eq!(run["metadata"]["invocationId"], "demo#7"); |
| 395 |
let kept = oxy_core::dsse::base64_decode(run["byproducts"][0]["content"].as_str().unwrap()); |
| 396 |
assert_eq!( |
| 397 |
kept.unwrap(), |
| 398 |
sr.encode(), |
| 399 |
"the runner's receipt rides along" |
| 400 |
); |
| 401 |
|
| 402 |
let repo = oxy_core::repo::Repo::open(&r) |
| 403 |
.unwrap() |
| 404 |
.with_home(home.clone()); |
| 405 |
let key = repo.signing_key().unwrap(); |
| 406 |
assert_eq!(env["signatures"][0]["keyid"], key.to_hex()); |
| 407 |
let me = repo.identity(&key).unwrap(); |
| 408 |
let sig = oxy_core::dsse::base64_decode(env["signatures"][0]["sig"].as_str().unwrap()).unwrap(); |
| 409 |
assert!(oxy_core::dsse::verify(&me, oxy_core::dsse::IN_TOTO, &payload, &sig).is_ok()); |
| 410 |
assert!(oxy_core::dsse::verify(&runner, oxy_core::dsse::IN_TOTO, &payload, &sig).is_err()); |
| 411 |
let pem = tmp.path().join("key.pem"); |
| 412 |
let pem_args = ["identity", "export", "--pem", pem.to_str().unwrap()]; |
| 413 |
assert_eq!(oxy(&r, &home, &pem_args).0, 0); |
| 414 |
assert_eq!( |
| 415 |
std::fs::read_to_string(&pem).unwrap(), |
| 416 |
oxy_core::dsse::public_pem(&me) |
| 417 |
); |
| 418 |
|
| 419 |
|
| 420 |
let mut forged = sr.clone(); |
| 421 |
forged.receipt.exit_code = 3; |
| 422 |
std::fs::write(&file, forged.encode()).unwrap(); |
| 423 |
let (code, out, _) = oxy(&r, &home, &["receipt", "verify", f]); |
| 424 |
assert_eq!(code, 1); |
| 425 |
assert!(String::from_utf8_lossy(&out).contains("different receipt")); |
| 426 |
let (code, _, err) = oxy(&r, &home, &["receipt", "import", f]); |
| 427 |
assert_ne!(code, 0); |
| 428 |
assert!(err.contains("does not verify"), "{err}"); |
| 429 |
} |