The amalgamation and the bindings now ship with sx itself (sx library/vendors/sqlite/ — bindings + c/ amalgamation); every import flips from ../src/db/sqlite.sx to vendors/sqlite/sqlite.sx, resolved through the compiler's stdlib search paths. vendor/ and src/db/ leave this repo entirely. make test 22/22 — the object cache keys on content, not path, so the relocated source still hits the existing cache entries.
189 lines
8.3 KiB
Plaintext
189 lines
8.3 KiB
Plaintext
// Acceptance for P3.4a — the local `dist ci publish` SUCCESS pipeline.
|
|
//
|
|
// Drives the BUILT `build/dist` binary (via `process.run`, like
|
|
// cli_dispatch.sx) on `examples/dist.json` into a FRESH `.sx-tmp/` store and
|
|
// asserts the end-to-end publish:
|
|
//
|
|
// 1. exit 0; stdout in `--json` mode is a SINGLE valid JSON object
|
|
// (parsed via std.json, no trailing junk).
|
|
// 2. the emitted release id / artifact ids / sha256 / local URLs MATCH the
|
|
// store: each `<store>/objects/<sha256>` exists and re-hashes (std.hash)
|
|
// to its own key, and each url is `file://<abs-store>/objects/<sha256>`.
|
|
// 3. `<store>/dist.db` (queried through the SQLite bindings) records the
|
|
// release, both artifacts (storage_key == sha256, validation_status
|
|
// valid), the channel pointer (current_release_id == the release), and
|
|
// an audit event per upload/publish/promotion.
|
|
//
|
|
// This FAILS against the pre-P3.4a stub (which rejects --manifest /
|
|
// --local-store as unknown flags, exiting 64, and writes no store) and
|
|
// PASSES against the real pipeline. Fresh store per run.
|
|
#import "modules/std.sx";
|
|
#import "modules/std/json.sx";
|
|
process :: #import "modules/std/process.sx";
|
|
fs :: #import "modules/std/fs.sx";
|
|
hash :: #import "modules/std/hash.sx";
|
|
sq :: #import "vendors/sqlite/sqlite.sx";
|
|
|
|
cstd :: #library "c";
|
|
c_getcwd :: (buf: [*]u8, size: usize) -> *u8 #foreign cstd "getcwd";
|
|
|
|
STORE_REL :: ".sx-tmp/publish_happy";
|
|
|
|
// Process cwd, so the absolute store path / download URLs can be rebuilt
|
|
// exactly as the publish does.
|
|
cwd :: () -> string {
|
|
buf : [4096]u8 = ---;
|
|
r := c_getcwd(@buf[0], 4096);
|
|
process.assert(cast(i64) r != 0, "getcwd must succeed");
|
|
n := 0;
|
|
while buf[n] != 0 { n += 1; }
|
|
return substr(string.{ ptr = @buf[0], len = n }, 0, n);
|
|
}
|
|
|
|
// Fetch a member value by key, asserting presence (the publish output is a
|
|
// fixed shape, so an absent key is a hard failure).
|
|
get :: (o: Object, key: string) -> Value {
|
|
i := 0;
|
|
while i < o.len {
|
|
if o.items[i].key == key { return o.items[i].val; }
|
|
i += 1;
|
|
}
|
|
process.assert(false, concat("missing json key: ", key));
|
|
dummy : Value = .null_;
|
|
return dummy;
|
|
}
|
|
get_str :: (o: Object, key: string) -> string { return get(o, key).str; }
|
|
get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; }
|
|
get_arr :: (o: Object, key: string) -> Array { return get(o, key).array; }
|
|
|
|
// True iff the object at `path` re-hashes (std.hash) to `want` (its key).
|
|
rehashes_to :: (path: string, want: string) -> bool {
|
|
maybe := hash.sha256_file(path);
|
|
if maybe == null { return false; }
|
|
d := maybe!;
|
|
view := string.{ ptr = @d[0], len = 64 };
|
|
return view == want;
|
|
}
|
|
|
|
// Open the store database read-only (asserts it exists and opens).
|
|
db_open_ro :: () -> sq.Sqlite {
|
|
c, oe := sq.Sqlite.open_v2(path_join(STORE_REL, "dist.db"), sq.SQLITE_OPEN_READONLY);
|
|
process.assert(!oe, "dist.db must open as a SQLite database");
|
|
c.busy_timeout(2000);
|
|
return c;
|
|
}
|
|
|
|
// One-row TEXT scalar with one optional text binding ("" = unbound).
|
|
q_text :: (sql: string, p1: string) -> string {
|
|
c := db_open_ro();
|
|
st, pe := c.prepare(sql);
|
|
process.assert(!pe, concat("prepare must succeed: ", sql));
|
|
if p1.len > 0 { st.bind_text(1, p1) catch { process.assert(false, "bind 1 failed"); }; }
|
|
rc, se := st.step();
|
|
process.assert(!se, concat("step must succeed: ", sql));
|
|
process.assert(rc == sq.SQLITE_ROW, concat("query must return a row: ", sql));
|
|
out := st.column_text(0);
|
|
st.finalize();
|
|
c.close();
|
|
return out;
|
|
}
|
|
|
|
// One-row INTEGER scalar with one optional text binding ("" = unbound).
|
|
q_int :: (sql: string, p1: string) -> i64 {
|
|
c := db_open_ro();
|
|
st, pe := c.prepare(sql);
|
|
process.assert(!pe, concat("prepare must succeed: ", sql));
|
|
if p1.len > 0 { st.bind_text(1, p1) catch { process.assert(false, "bind 1 failed"); }; }
|
|
rc, se := st.step();
|
|
process.assert(!se, concat("step must succeed: ", sql));
|
|
process.assert(rc == sq.SQLITE_ROW, concat("query must return a row: ", sql));
|
|
out := st.column_int64(0);
|
|
st.finalize();
|
|
c.close();
|
|
return out;
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
gpa := GPA.init();
|
|
arena := Arena.init(xx gpa, 1 << 20);
|
|
defer arena.deinit();
|
|
|
|
base := cwd();
|
|
store_abs := path_join(base, STORE_REL);
|
|
|
|
// Fresh store, even after a crashed prior run.
|
|
process.run(concat("rm -rf ", STORE_REL));
|
|
|
|
// ── 1. Run the real publish; stdout must be one JSON object, exit 0 ──
|
|
r := process.run("build/dist ci publish --manifest examples/dist.json --local-store .sx-tmp/publish_happy --json 2>/dev/null");
|
|
process.assert(r != null, "spawn build/dist ci publish failed");
|
|
res := r!;
|
|
process.assert(res.exit_code == 0, "publish must exit 0 (EX_OK)");
|
|
|
|
v, e := parse(res.stdout, xx arena);
|
|
if e { process.assert(false, "stdout must be a single valid JSON object (parse failed / trailing junk)"); return 1; }
|
|
root := v.object;
|
|
|
|
process.assert(get_str(root, "status") == "published", "status must be published");
|
|
|
|
rel := get_obj(root, "release");
|
|
rel_id := get_str(rel, "id");
|
|
process.assert(rel_id == "rel-acme-app-1.2.3", "release id derived from slug+version");
|
|
process.assert(get_str(rel, "version") == "1.2.3", "release version");
|
|
process.assert(get_str(rel, "channel") == "stable", "release channel");
|
|
print(" release {} published\n", rel_id);
|
|
|
|
// ── 2. Each artifact: object exists at objects/<sha256>, re-hashes to
|
|
// its key, and its url is file://<abs-store>/objects/<sha256> ──
|
|
arts := get_arr(root, "artifacts");
|
|
process.assert(arts.len == 2, "two artifacts published");
|
|
i := 0;
|
|
while i < arts.len {
|
|
ao := arts.items[i].object;
|
|
sha := get_str(ao, "sha256");
|
|
url := get_str(ao, "url");
|
|
process.assert(sha.len == 64, "sha256 is a 64-char digest");
|
|
|
|
obj_path := path_join(STORE_REL, concat("objects/", sha));
|
|
process.assert(fs.exists(obj_path), "object exists at objects/<sha256>");
|
|
process.assert(rehashes_to(obj_path, sha), "stored object re-hashes to its key");
|
|
|
|
want_url := concat(concat(concat("file://", store_abs), "/objects/"), sha);
|
|
process.assert(url == want_url, "url is file://<abs-store>/objects/<sha256>");
|
|
|
|
process.assert(get_str(ao, "id").len > 0, "artifact id present");
|
|
i += 1;
|
|
}
|
|
print(" {} artifacts stored + re-hash to their keys\n", arts.len);
|
|
|
|
// ── 3. dist.db records the published aggregate ──────────────────────
|
|
process.assert(q_int("SELECT COUNT(*) FROM releases", "") == 1, "db: one release");
|
|
process.assert(q_text("SELECT id FROM releases", "") == rel_id, "db: release id matches");
|
|
|
|
process.assert(q_int("SELECT COUNT(*) FROM artifacts", "") == 2, "db: two artifacts");
|
|
process.assert(q_int("SELECT COUNT(*) FROM artifacts WHERE storage_key = sha256", "") == 2,
|
|
"db: storage_key == sha256 (content-addressed)");
|
|
process.assert(q_int("SELECT COUNT(*) FROM artifacts WHERE validation_status = 'valid'", "") == 2,
|
|
"db: artifact validation passed");
|
|
process.assert(q_int("SELECT COUNT(*) FROM artifacts WHERE release_id = ?1", rel_id) == 2,
|
|
"db: artifacts belong to the release");
|
|
|
|
process.assert(q_int("SELECT COUNT(*) FROM channels", "") == 1, "db: one channel");
|
|
process.assert(q_text("SELECT name FROM channels", "") == "stable", "db: channel name");
|
|
process.assert(q_text("SELECT current_release_id FROM channels", "") == rel_id,
|
|
"db: channel points at the release");
|
|
|
|
process.assert(q_int("SELECT COUNT(*) FROM audit_events WHERE action = ?1", "artifact.upload") == 2,
|
|
"db: one upload event per artifact");
|
|
process.assert(q_int("SELECT COUNT(*) FROM audit_events WHERE action = ?1", "release.publish") == 1,
|
|
"db: one publish event");
|
|
process.assert(q_int("SELECT COUNT(*) FROM audit_events WHERE action = ?1", "channel.promote") == 1,
|
|
"db: one promotion event");
|
|
print(" dist.db records release/artifacts/channel + {} audit events\n",
|
|
q_int("SELECT COUNT(*) FROM audit_events", ""));
|
|
|
|
process.run(concat("rm -rf ", STORE_REL));
|
|
print("publish_happy: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|