release_index_test.c (5696B)
1 /* release_index_test — public <kit/package.h> channel-index surface: 2 * 3 * - Build a KitReleaseIndex (two hosts, inserted out of target order, with 4 * both ids, a size, and a mirror list), emit it to a memory KitWriter, and 5 * assert the EXACT canonical bytes (byte-stability: hosts sorted ascending 6 * by target, fixed key order). 7 * - Parse those bytes back and assert full round-trip equality. 8 * - kit_calver_compare: numeric (not lexical) ordering and malformed input. 9 * 10 * Run by: make test-release-index 11 */ 12 13 #include <kit/core.h> 14 #include <kit/package.h> 15 #include <stddef.h> 16 #include <stdint.h> 17 #include <string.h> 18 19 #include "lib/kit_unit.h" 20 21 static KitUnit g_u; 22 #define EXPECT(c, ...) CU_EXPECT(&g_u, c, __VA_ARGS__) 23 24 /* aarch64-macos sorts before x86_64-linux-gnu, so it is emitted first even 25 * though we register it second. kpkg id = bytes 0x00..0x1f, targz = 0x20..0x3f. 26 */ 27 static const char* const GOLDEN = 28 "kit-release 1\n" 29 "channel = stable\n" 30 "version = 2026.6.0\n" 31 "hash = blake2b-256\n" 32 "\n" 33 "[host]\n" 34 "target = aarch64-macos\n" 35 "kpkg = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" 36 "targz = 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f\n" 37 "size = 31457280\n" 38 "url = https://example/a.kpkg\n" 39 "url = https://dl.example/a.kpkg\n" 40 "url = https://mirror.example/a.kpkg\n" 41 "\n" 42 "[host]\n" 43 "target = x86_64-linux-gnu\n" 44 "kpkg = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\n" 45 "url = https://example/b.kpkg\n"; 46 47 static void fill_index(KitReleaseIndex* idx) { 48 KitReleaseHost* a; 49 KitReleaseHost* b; 50 unsigned i; 51 memset(idx, 0, sizeof *idx); 52 snprintf(idx->channel, sizeof idx->channel, "%s", "stable"); 53 snprintf(idx->version, sizeof idx->version, "%s", "2026.6.0"); 54 idx->n_hosts = 2; 55 56 /* Register x86_64-linux-gnu FIRST to prove emit re-sorts ascending. */ 57 b = &idx->hosts[0]; 58 snprintf(b->target, sizeof b->target, "%s", "x86_64-linux-gnu"); 59 for (i = 0; i < KIT_CAS_HASH_LEN; ++i) b->kpkg_id[i] = (uint8_t)i; 60 b->has_kpkg = 1; 61 b->has_targz = 0; 62 b->size = 0; /* omitted */ 63 snprintf(b->urls[0], sizeof b->urls[0], "%s", "https://example/b.kpkg"); 64 b->n_urls = 1; 65 66 a = &idx->hosts[1]; 67 snprintf(a->target, sizeof a->target, "%s", "aarch64-macos"); 68 for (i = 0; i < KIT_CAS_HASH_LEN; ++i) a->kpkg_id[i] = (uint8_t)i; 69 for (i = 0; i < KIT_CAS_HASH_LEN; ++i) a->targz_id[i] = (uint8_t)(i + 32u); 70 a->has_kpkg = 1; 71 a->has_targz = 1; 72 a->size = 31457280u; 73 snprintf(a->urls[0], sizeof a->urls[0], "%s", "https://example/a.kpkg"); 74 snprintf(a->urls[1], sizeof a->urls[1], "%s", "https://dl.example/a.kpkg"); 75 snprintf(a->urls[2], sizeof a->urls[2], "%s", "https://mirror.example/a.kpkg"); 76 a->n_urls = 3; 77 } 78 79 static int host_eq(const KitReleaseHost* x, const KitReleaseHost* y) { 80 unsigned i; 81 if (strcmp(x->target, y->target) != 0) return 0; 82 if (x->has_kpkg != y->has_kpkg || x->has_targz != y->has_targz) return 0; 83 if (x->size != y->size || x->n_urls != y->n_urls) return 0; 84 if (x->has_kpkg && memcmp(x->kpkg_id, y->kpkg_id, KIT_CAS_HASH_LEN) != 0) 85 return 0; 86 if (x->has_targz && memcmp(x->targz_id, y->targz_id, KIT_CAS_HASH_LEN) != 0) 87 return 0; 88 for (i = 0; i < x->n_urls; ++i) 89 if (strcmp(x->urls[i], y->urls[i]) != 0) return 0; 90 return 1; 91 } 92 93 /* The parsed index is canonical (ascending by target), so compare it to the 94 * sorted expectation: aarch64-macos first, then x86_64-linux-gnu. */ 95 static void check_roundtrip(const uint8_t* bytes, size_t len) { 96 static KitReleaseIndex want; /* large; keep off the stack */ 97 static KitReleaseIndex got; 98 fill_index(&want); 99 EXPECT(kit_release_index_parse(&g_u.ctx, bytes, len, &got) == KIT_OK, 100 "parse golden bytes OK"); 101 EXPECT(strcmp(got.channel, want.channel) == 0, "channel round-trips"); 102 EXPECT(strcmp(got.version, want.version) == 0, "version round-trips"); 103 EXPECT(got.n_hosts == want.n_hosts, "n_hosts round-trips (%u)", got.n_hosts); 104 if (got.n_hosts == 2) { 105 EXPECT(host_eq(&got.hosts[0], &want.hosts[1]), "host[0] == aarch64-macos"); 106 EXPECT(host_eq(&got.hosts[1], &want.hosts[0]), 107 "host[1] == x86_64-linux-gnu"); 108 } 109 } 110 111 static void check_emit_parse(void) { 112 static KitReleaseIndex idx; /* large; keep off the stack */ 113 KitWriter* w = NULL; 114 const uint8_t* bytes; 115 size_t len, want_len; 116 fill_index(&idx); 117 EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK, "writer_mem OK"); 118 EXPECT(kit_release_index_emit(&g_u.ctx, &idx, w) == KIT_OK, "emit OK"); 119 bytes = kit_writer_mem_bytes(w, &len); 120 want_len = strlen(GOLDEN); 121 EXPECT(len == want_len, "emitted length %zu == golden %zu", len, want_len); 122 EXPECT(len == want_len && memcmp(bytes, GOLDEN, len) == 0, 123 "emitted bytes match golden exactly"); 124 check_roundtrip(bytes, len); 125 kit_writer_close(w); 126 } 127 128 static void check_calver(void) { 129 int cmp = 99; 130 EXPECT(kit_calver_compare("2026.6.0", "2026.6.1", &cmp) == KIT_OK && cmp < 0, 131 "2026.6.0 < 2026.6.1"); 132 EXPECT(kit_calver_compare("2026.10.0", "2026.9.0", &cmp) == KIT_OK && cmp > 0, 133 "2026.10.0 > 2026.9.0 (numeric, not lexical)"); 134 EXPECT(kit_calver_compare("2026.6.0", "2026.6.0", &cmp) == KIT_OK && cmp == 0, 135 "2026.6.0 == 2026.6.0"); 136 EXPECT(kit_calver_compare("2026.6", "2026.6.0", &cmp) == KIT_INVALID, 137 "too few fields -> KIT_INVALID"); 138 EXPECT(kit_calver_compare("2026.x.0", "2026.6.0", &cmp) == KIT_INVALID, 139 "non-numeric field -> KIT_INVALID"); 140 } 141 142 int main(void) { 143 kit_unit_init(&g_u); 144 check_emit_parse(); 145 check_calver(); 146 kit_unit_summary(&g_u, "release_index_test"); 147 return kit_unit_status(&g_u); 148 }