Files
distribution/tests/manifest_parse.sx
agra 09853f8882 P3.2: manifest model + std.json parse + validate
Define the v0 publish manifest, parse it via std.json into a typed
struct, and validate it.

- src/manifest/manifest.sx: typed Manifest { app, version, channel,
  artifacts: List(ManifestArtifact) } + ManifestArtifact { platform,
  path, filename, content_type, metadata }. parse_manifest walks the
  std.json Value tree into the struct, surfacing every malformed/
  missing/wrong-type field as a distinct typed ManifestErr (BadJson,
  WrongType, MissingField, UnknownPlatform, MissingArtifact, Io) — never
  a silent default. Reuses P2.1 parse_platform for the platform id.
  validate_manifest checks each artifact path exists on disk (fs.exists),
  resolved relative to the manifest's directory. Strings are copied into
  the caller's allocator (long-lived-container rule).
- examples/dist.json: representative valid manifest (android_apk + ios).
- examples/fixtures/: tiny stand-in artifact byte files referenced by it.
- tests/manifest_parse.sx: parses dist.json and asserts fields; asserts
  the three failure classes surface distinct typed errors.
2026-06-06 03:46:39 +03:00

92 lines
4.2 KiB
Plaintext

// Acceptance for P3.2 — the v0 publish manifest model + parse + validate.
//
// 1. Parses `examples/dist.json` into a typed `Manifest` and asserts the
// fields (app/version/channel, the artifact count, and each artifact's
// platform + path).
// 2. Asserts each failure class surfaces a DISTINCT machine-readable error
// (never a silent default):
// - a missing required field (a manifest lacking `version`) -> MissingField
// - an unknown platform ("psvita") -> UnknownPlatform
// - a non-existent artifact path -> MissingArtifact
//
// Prints a PASS/FAIL line per case and exits non-zero if any case behaves
// unexpectedly, so the test runner counts it as a failure.
#import "modules/std.sx";
#import "../src/domain/platform.sx";
#import "../src/manifest/manifest.sx";
// ── happy path: examples/dist.json -> typed Manifest ────────────────────
check_parse_valid :: (alloc: Allocator) -> bool {
m, e := load_manifest("examples/dist.json", alloc);
if e { return false; }
if m.app != "acme-app" { return false; }
if m.version != "1.2.3" { return false; }
if m.channel != "stable" { return false; }
if m.artifacts.len != 2 { return false; }
a0 := m.artifacts.items[0];
if a0.platform != .android_apk { return false; }
if a0.path != "fixtures/acme-1.2.3-android.apk" { return false; }
if a0.filename != "acme.apk" { return false; }
if a0.content_type != "application/vnd.android.package-archive" { return false; }
a1 := m.artifacts.items[1];
if a1.platform != .ios { return false; }
if a1.path != "fixtures/acme-1.2.3-ios.ipa" { return false; }
if a1.filename != "" { return false; } // optional override, absent -> ""
return true;
}
// ── failure: a required field (version) is absent -> MissingField ───────
check_missing_version :: (alloc: Allocator) -> bool {
src := "{\"app\":\"acme-app\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"ios\",\"path\":\"fixtures/acme-1.2.3-ios.ipa\"}]}";
_, e := parse_manifest(src, alloc);
return e == error.MissingField;
}
// ── failure: an artifact platform id is unknown -> UnknownPlatform ──────
check_unknown_platform :: (alloc: Allocator) -> bool {
src := "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"psvita\",\"path\":\"fixtures/acme-1.2.3-ios.ipa\"}]}";
_, e := parse_manifest(src, alloc);
return e == error.UnknownPlatform;
}
// ── failure: an artifact path does not exist on disk -> MissingArtifact ─
// Parse SUCCEEDS (parse is filesystem-free); the missing file surfaces only
// at validate time, against the manifest's base directory.
check_missing_artifact_path :: (alloc: Allocator) -> bool {
src := "{\"app\":\"acme-app\",\"version\":\"1.2.3\",\"channel\":\"stable\",\"artifacts\":[{\"platform\":\"ios\",\"path\":\"fixtures/does-not-exist.ipa\"}]}";
m, pe := parse_manifest(src, alloc);
if pe { return false; } // parse must not fail here
raised := false;
matched := false;
validate_manifest(m, "examples") catch err { raised = true; matched = (err == error.MissingArtifact); };
return raised and matched;
}
run_case :: (label: string, ok: bool) -> s32 {
if ok { print(" PASS {}\n", label); return 0; }
print(" FAIL {}\n", label);
return 1;
}
main :: () -> s32 {
gpa := GPA.init();
arena := Arena.init(xx gpa, 65536);
defer arena.deinit();
failures : s32 = 0;
failures += run_case("parse valid dist.json -> fields", check_parse_valid(xx arena));
failures += run_case("missing version -> MissingField", check_missing_version(xx arena));
failures += run_case("unknown platform 'psvita' -> UnknownPlatform", check_unknown_platform(xx arena));
failures += run_case("nonexistent path -> MissingArtifact", check_missing_artifact_path(xx arena));
print("------------------------------------------------\n");
if failures == 0 {
print("manifest_parse: ALL CASES PASS\n");
return 0;
}
print("manifest_parse: {} CASE(S) FAILED\n", failures);
return 1;
}