/* test_models.c - /models 响应体的解析 * * 只测纯解析。拉取(GET {base_url}/models)要走网络,由 e2e 第 52 段对着 * mock 覆盖,那里还顺带验了交互式选择本身(方向键、确认、取消)。 * * 为什么要容忍这么多种形状:OpenAI 兼容是个很大的家庭,/models 的返回花样 * 比文档里写的多 —— 见过裸数组、见过重复列同一模型、见过元素里没有 id。 * 解析器要么跳过,要么去重,但**不能**因此整个失败:列表里少一个模型远好过 * 一个都选不了。 */ #include "models.h" #include #include static int failures = 0; static int checks = 0; #define CHECK(cond, ...) \ do { \ checks++; \ if (!(cond)) { \ failures++; \ printf(" ✗ %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ } \ } while (0) static void test_standard(void) { puts("标准形状:{\"data\":[{\"id\":...}]}"); const char *json = "{\"object\":\"list\",\"data\":[" "{\"id\":\"alpha\",\"object\":\"model\",\"owned_by\":\"x\"}," "{\"id\":\"beta\",\"object\":\"model\"}" "]}"; size_t n = 0; char **l = models_parse(json, &n); CHECK(l && n == 2, "应解析出 2 个,实际 %zu", n); if (l && n == 2) { CHECK(strcmp(l[0], "alpha") == 0, "顺序应保持服务端给的(第一个 alpha)"); CHECK(strcmp(l[1], "beta") == 0, "第二个 beta"); } models_free(l, n); } static void test_dedupe_and_skip(void) { puts("重复项要去重,缺 id 的要跳过"); const char *json = "{\"data\":[" "{\"id\":\"a\"},{\"id\":\"a\"},{\"object\":\"model\"}," "{\"id\":\"\"},{\"id\":\"b\"},{\"id\":42}" "]}"; size_t n = 0; char **l = models_parse(json, &n); CHECK(l && n == 2, "去重 + 跳过后应剩 2 个,实际 %zu", n); if (l && n == 2) { CHECK(strcmp(l[0], "a") == 0, "第一个是 a"); CHECK(strcmp(l[1], "b") == 0, "第二个是 b"); } models_free(l, n); } static void test_bare_array(void) { puts("兼容裸数组(字符串 / 对象都认)"); size_t n = 0; char **l = models_parse("[\"alpha\",\"beta\"]", &n); CHECK(l && n == 2 && strcmp(l[1], "beta") == 0, "字符串数组应能解析"); models_free(l, n); n = 0; l = models_parse("[{\"id\":\"gamma\"}]", &n); CHECK(l && n == 1 && strcmp(l[0], "gamma") == 0, "对象数组应能解析"); models_free(l, n); } static void test_bad_input(void) { puts("拿不到模型时返回 NULL,而不是空数组或崩溃"); size_t n = 123; CHECK(models_parse(NULL, &n) == NULL && n == 0, "NULL 输入"); n = 123; CHECK(models_parse("", &n) == NULL && n == 0, "空串"); n = 123; CHECK(models_parse("not json", &n) == NULL && n == 0, "不是 JSON"); n = 123; CHECK(models_parse("{\"data\":[]}", &n) == NULL && n == 0, "空列表"); n = 123; CHECK(models_parse("{\"object\":\"list\"}", &n) == NULL && n == 0, "没有 data 字段"); n = 123; CHECK(models_parse("{\"data\":\"nope\"}", &n) == NULL && n == 0, "data 不是数组"); n = 123; CHECK(models_parse("{\"data\":[{\"object\":\"model\"}]}", &n) == NULL && n == 0, "全是坏条目"); /* out_n 也可为 NULL(调用方只关心数组) */ char **l = models_parse("{\"data\":[{\"id\":\"x\"}]}", NULL); CHECK(l != NULL, "out_n 为 NULL 也要能工作"); models_free(l, 1); puts("空列表要与\"解析不了\"分开"); CHECK(models_is_empty_list("{\"object\":\"list\",\"data\":[]}") == 1, "空的 data 应判为空列表"); CHECK(models_is_empty_list("[]") == 1, "空的裸数组也是空列表"); CHECK(models_is_empty_list("not json") == 0, "解析不了不算空列表(是两回事)"); CHECK(models_is_empty_list("{\"data\":\"nope\"}") == 0, "data 不是数组不算空列表"); CHECK(models_is_empty_list("{\"data\":[{\"id\":\"a\"}]}") == 0, "有模型就不是空列表"); CHECK(models_is_empty_list(NULL) == 0, "NULL 不算空列表"); CHECK(models_is_empty_list("") == 0, "空串不算空列表"); /* 释放 NULL 不该崩 */ models_free(NULL, 0); CHECK(1, "释放 NULL 没崩"); } int main(void) { test_standard(); test_dedupe_and_skip(); test_bare_array(); test_bad_input(); printf("\n%d 项检查,%d 项失败\n", checks, failures); return failures ? 1 : 0; }