P3.1: wire dist CLI into build + test (0100 unblocked)
The sx 0100 fix (cli.parse / json.parse name collision) is merged on sx master, so `dist.sx` — co-importing std.cli (via dist) and std.json (via json_out) — now lowers and builds. Finish the step: - dist.sx: fix two real frontend errors the old IR-lowering crash had masked — `main` returns `!` (noreturn exit tails), and the post-parse dispatch is guarded by `if !perr` so the failable `p` is used only with its error proven absent. Drop the stale BLOCKED narration. - Makefile: `make build` now also compiles src/dist.sx -> build/dist; `make test` depends on `build` so the acceptance test finds the binary. - tests/cli_dispatch.sx: drives the BUILT build/dist via process.run and asserts the std.cli exit-code + --json purity contract: no-args and unknown-command -> human text on stderr + EX_USAGE (64); `ci publish --json` -> stdout is a single valid JSON object (std.json.parse, no trailing junk) with the human ack on stderr; `--help` lists ci/release. Handlers stay honest stubs (real ci publish is P3.4). Gate green: make build (build/dist), make test (7/7).
This commit is contained in:
15
Makefile
15
Makefile
@@ -6,19 +6,24 @@ SX ?= /Users/agra/projects/sx/zig-out/bin/sx
|
|||||||
|
|
||||||
BUILD_DIR := build
|
BUILD_DIR := build
|
||||||
|
|
||||||
# Programs compiled by `make build`. Currently just the smoke program;
|
# Programs compiled by `make build`: the smoke program and the `dist`
|
||||||
# product entry points under src/ get added here as they land (P1.2+).
|
# product entry point. Further entry points under src/ get added here as
|
||||||
|
# they land.
|
||||||
SMOKE := tests/smoke.sx
|
SMOKE := tests/smoke.sx
|
||||||
|
DIST := src/dist.sx
|
||||||
|
|
||||||
.PHONY: build test publish-example clean
|
.PHONY: build test publish-example clean
|
||||||
|
|
||||||
# Compile the smoke program (and, later, product sources) without running.
|
# Compile the product sources (and the smoke program) without running.
|
||||||
build:
|
build:
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(SX) build -o $(BUILD_DIR)/smoke $(SMOKE)
|
$(SX) build -o $(BUILD_DIR)/smoke $(SMOKE)
|
||||||
|
$(SX) build -o $(BUILD_DIR)/dist $(DIST)
|
||||||
|
|
||||||
# Run the test runner over every tests/**/*.sx. Exits non-zero on any failure.
|
# Run the test runner over every tests/**/*.sx. Exits non-zero on any
|
||||||
test:
|
# failure. Depends on `build` so the CLI acceptance test (tests/cli_*.sx)
|
||||||
|
# finds a fresh `build/dist` to drive.
|
||||||
|
test: build
|
||||||
@SX="$(SX)" ./tests/run.sh
|
@SX="$(SX)" ./tests/run.sh
|
||||||
|
|
||||||
# Placeholder for the end-to-end publish flow — becomes real in P3.4.
|
# Placeholder for the end-to-end publish flow — becomes real in P3.4.
|
||||||
|
|||||||
14
src/dist.sx
14
src/dist.sx
@@ -20,14 +20,6 @@
|
|||||||
// carries ONLY the machine-readable JSON object (emitted via `std.json`,
|
// carries ONLY the machine-readable JSON object (emitted via `std.json`,
|
||||||
// isolated in `json_out.sx`); ALL human-readable text (help, progress,
|
// isolated in `json_out.sx`); ALL human-readable text (help, progress,
|
||||||
// errors) goes to stderr. In non-json mode human text on stdout is fine.
|
// errors) goes to stderr. In non-json mode human text on stdout is fine.
|
||||||
//
|
|
||||||
// NOTE (BLOCKED — see P3.1 worker report): `std.cli` and `std.json` both
|
|
||||||
// export a top-level `parse`; with both in the compilation closure the sx
|
|
||||||
// compiler crashes while lowering the `cli.parse` call. `json_out.sx` keeps
|
|
||||||
// the `std.json` import out of this file, but `sx build` lowers the whole
|
|
||||||
// transitive closure, so the collision (and crash) persists. This program
|
|
||||||
// therefore does NOT yet build; it is the intended wiring pending the sx
|
|
||||||
// fix.
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
@@ -116,7 +108,7 @@ dispatch :: (p: *Parsed, json_mode: bool) {
|
|||||||
exit_usage();
|
exit_usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
main :: () -> s32 {
|
main :: () -> ! {
|
||||||
// Real process argv -> logical args: drop argv[0] (the program path).
|
// Real process argv -> logical args: drop argv[0] (the program path).
|
||||||
storage : [64]string = ---;
|
storage : [64]string = ---;
|
||||||
argbuf : []string = ---;
|
argbuf : []string = ---;
|
||||||
@@ -158,6 +150,8 @@ main :: () -> s32 {
|
|||||||
exit_usage();
|
exit_usage();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(@p, json_mode);
|
if !perr {
|
||||||
|
dispatch(@p, json_mode);
|
||||||
|
}
|
||||||
exit_ok();
|
exit_ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
// json_out.sx — `--json` mode output for `dist`, built with `std.json`.
|
// json_out.sx — `--json` mode output for `dist`, built with `std.json`.
|
||||||
//
|
//
|
||||||
// Isolated in its own module so the `std.json` writer (which the
|
// The machine-readable JSON object a `--json` run writes to stdout is
|
||||||
// `--json` contract requires) is not imported into the same file as the
|
// built here, keeping the `std.json` writer in one place, separate from
|
||||||
// `std.cli` dispatcher. `std.cli` and `std.json` BOTH export a top-level
|
// the `std.cli` dispatch in dist.sx.
|
||||||
// `parse`; co-importing them and calling `cli.parse` currently crashes the
|
|
||||||
// sx compiler's IR lowering (see this step's blocked report). Splitting the
|
|
||||||
// emission out keeps each file individually well-formed sx.
|
|
||||||
// =====================================================================
|
// =====================================================================
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|||||||
102
tests/cli_dispatch.sx
Normal file
102
tests/cli_dispatch.sx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// cli_dispatch.sx — acceptance for the `dist` CLI entrypoint (P3.1).
|
||||||
|
//
|
||||||
|
// Drives the BUILT `build/dist` binary through `process.run` (the binary,
|
||||||
|
// not `sx run src/dist.sx` — only a real executable sees its own argv;
|
||||||
|
// under `sx run` the process argv is the interpreter's). Asserts the
|
||||||
|
// std.cli exit-code contract and the `--json` stdout-purity contract:
|
||||||
|
//
|
||||||
|
// 1. no args → human help/usage on STDERR + EX_USAGE (64).
|
||||||
|
// 2. unknown command → human error on STDERR + EX_USAGE (64).
|
||||||
|
// 3. `ci publish --json` → STDOUT is a SINGLE valid JSON object (parses
|
||||||
|
// via std.json with no trailing junk); the human acknowledgement is
|
||||||
|
// on STDERR, never stdout.
|
||||||
|
// 4. `--help` → lists the `ci` / `release` groups, exits 0.
|
||||||
|
//
|
||||||
|
// `make test` depends on `build`, so `build/dist` exists before this runs;
|
||||||
|
// the relative path resolves from the repo root (the `make test` cwd).
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
proc :: #import "modules/process.sx";
|
||||||
|
json :: #import "modules/std/json.sx";
|
||||||
|
|
||||||
|
// True iff `needle` occurs in `hay`. Plain scan — the captured streams are
|
||||||
|
// small, and the test only needs presence, not position.
|
||||||
|
contains :: (hay: string, needle: string) -> bool {
|
||||||
|
if needle.len == 0 { return true; }
|
||||||
|
if needle.len > hay.len { return false; }
|
||||||
|
i := 0;
|
||||||
|
while i + needle.len <= hay.len {
|
||||||
|
j := 0;
|
||||||
|
ok := true;
|
||||||
|
while j < needle.len {
|
||||||
|
if hay[i + j] != needle[j] { ok = false; break; }
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if ok { return true; }
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
gpa := GPA.init();
|
||||||
|
|
||||||
|
// ── 1. No command → readable usage on stderr, EX_USAGE (64) ───────
|
||||||
|
// `2>&1 1>/dev/null` routes the command's stderr into the captured
|
||||||
|
// pipe and discards its stdout, so `r.stdout` here IS the stderr text.
|
||||||
|
if r := proc.run("build/dist 2>&1 1>/dev/null") {
|
||||||
|
proc.assert(r.exit_code == 64, "no-args must exit EX_USAGE (64)");
|
||||||
|
proc.assert(r.stdout.len > 0, "no-args must print human help/usage to stderr");
|
||||||
|
proc.assert(contains(r.stdout, "Usage"), "no-args help must carry a usage line");
|
||||||
|
} else {
|
||||||
|
proc.assert(false, "spawn build/dist (no args) failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Unknown command → readable error on stderr, EX_USAGE (64) ──
|
||||||
|
if r := proc.run("build/dist bogus 2>&1 1>/dev/null") {
|
||||||
|
proc.assert(r.exit_code == 64, "unknown command must exit EX_USAGE (64)");
|
||||||
|
proc.assert(contains(r.stdout, "unknown"), "unknown command must name the failure on stderr");
|
||||||
|
} else {
|
||||||
|
proc.assert(false, "spawn build/dist bogus failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3a. `--json` stdout purity: a single valid JSON object, nothing
|
||||||
|
// else. `2>/dev/null` drops the human note so the pipe carries
|
||||||
|
// ONLY stdout; std.json.parse rejects trailing junk. ─────────
|
||||||
|
if r := proc.run("build/dist ci publish --json 2>/dev/null") {
|
||||||
|
proc.assert(r.exit_code == 0, "stub --json command must succeed (EX_OK)");
|
||||||
|
v, e := json.parse(r.stdout, xx gpa);
|
||||||
|
proc.assert(!e, "stdout in --json mode must be a single valid JSON object (parse failed / trailing junk)");
|
||||||
|
if !e {
|
||||||
|
o := v.object;
|
||||||
|
proc.assert(o.len == 3, "stub json object carries command/status/stub");
|
||||||
|
proc.assert(o.items[0].key == "command" and o.items[0].val.str == "ci publish",
|
||||||
|
"stub json names the dispatched command");
|
||||||
|
proc.assert(o.items[1].key == "status" and o.items[1].val.str == "ok",
|
||||||
|
"stub json reports status ok");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proc.assert(false, "spawn build/dist ci publish --json failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3b. `--json` mode keeps human text on STDERR (not stdout) ──────
|
||||||
|
if r := proc.run("build/dist ci publish --json 2>&1 1>/dev/null") {
|
||||||
|
proc.assert(r.stdout.len > 0, "--json mode must still emit human text to stderr");
|
||||||
|
} else {
|
||||||
|
proc.assert(false, "spawn build/dist ci publish --json (stderr) failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. `--help` lists the ci / release groups, exits 0 ────────────
|
||||||
|
if r := proc.run("build/dist --help 2>/dev/null") {
|
||||||
|
proc.assert(r.exit_code == 0, "--help exits 0");
|
||||||
|
proc.assert(contains(r.stdout, "ci"), "--help lists the ci group");
|
||||||
|
proc.assert(contains(r.stdout, "release"), "--help lists the release group");
|
||||||
|
} else {
|
||||||
|
proc.assert(false, "spawn build/dist --help failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
print("cli_dispatch: ok\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user