Commit 62a6f74ba for llama.cpp

commit 62a6f74bab748b1755f4cd8dfc0871caf865c6c8
Author: Xuan-Son Nguyen <son@huggingface.co>
Date:   Sun Oct 11 12:51:07 2026 +0200

    server: add component tests (#30312)

    * server: add component tests

    * nits

    * add codeowners

diff --git a/CODEOWNERS b/CODEOWNERS
index df6905252..b9762685b 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -97,6 +97,7 @@
 /src/models/                            @CISC
 /tests/                                 @ggerganov
 /tests/test-chat.*                      @pwilkin
+/tests/test-server-component.cpp        @ggml-org/llama-server
 /tools/batched-bench/                   @ggerganov
 /tools/cli/                             @ngxson
 /tools/completion/                      @ggerganov
diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md
index 7550085f2..9f14b4269 100644
--- a/skills/code-review/SKILL.md
+++ b/skills/code-review/SKILL.md
@@ -132,6 +132,7 @@ Public API changes carry a higher bar than internal ones (`CONTRIBUTING.md`). Re
 - Only add tests that bring meaningful results. Too-trivial tests just bloat the suite and CI.
 - No time-sensitive tests (timing thresholds, sleeps, races against wall-clock); they are flaky on CI.
 - Think twice about tests that significantly increase CI run time (expensive computation, large inputs, or long sleep/wait delays) or download large amounts of data from the internet (big models, datasets). Flag them and ask whether a smaller model/input or an existing fixture would do.
+- Server: small tests of internal components (helpers, `server_tokens`, queues, format converters, ...) belong in `tests/test-server-component.cpp`. The end-to-end pytest suite in `tools/server/tests` is costly in CI time; only add to it when the test requires end-to-end behavior with a real model.

 ## General (always)

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 5f9bd037f..a732b6d65 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -163,6 +163,9 @@ if (NOT WIN32 OR NOT BUILD_SHARED_LIBS)
     llama_build_and_test(test-chat.cpp WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
     target_include_directories(test-chat PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
     target_link_libraries(test-chat PRIVATE server-context)
+    llama_build_and_test(test-server-component.cpp)
+    target_include_directories(test-server-component PRIVATE ${PROJECT_SOURCE_DIR}/tools/server)
+    target_link_libraries(test-server-component PRIVATE server-context)
     llama_build_and_test(test-json-schema-to-grammar.cpp)

     if (NOT GGML_BACKEND_DL)
diff --git a/tests/test-server-component.cpp b/tests/test-server-component.cpp
new file mode 100644
index 000000000..d2d103a45
--- /dev/null
+++ b/tests/test-server-component.cpp
@@ -0,0 +1,393 @@
+#include "testing.h"
+
+#include "../tools/server/server-common.h"
+
+#include <cstdlib>
+#include <string>
+#include <thread>
+#include <vector>
+
+// unit tests for server internal components, no model required
+// NOTE: end-to-end tests live in tools/server/tests
+// see skills/code-review/SKILL.md for more
+
+//
+// server_tokens (text only, has_mtmd = false)
+//
+
+static void test_server_tokens_basic(testing & t) {
+    server_tokens st(llama_tokens{1, 2, 3}, false);
+    t.assert_equal("size", (size_t) 3, st.size());
+    t.assert_true("not empty", !st.empty());
+    t.assert_equal("operator[]", 2, st[1]);
+
+    st.push_back(4);
+    t.assert_equal("push_back", (size_t) 4, st.size());
+    t.assert_true("get_tokens", st.get_tokens() == llama_tokens({1, 2, 3, 4}));
+
+    st.insert({5, 6});
+    t.assert_true("insert appends", st.get_tokens() == llama_tokens({1, 2, 3, 4, 5, 6}));
+
+    st.set_token(0, 9);
+    t.assert_equal("set_token", 9, st[0]);
+
+    bool thrown = false;
+    try {
+        st.push_back(LLAMA_TOKEN_NULL);
+    } catch (const std::exception &) {
+        thrown = true;
+    }
+    t.assert_true("push_back(LLAMA_TOKEN_NULL) throws", thrown);
+    t.assert_equal("size unchanged after throw", (size_t) 6, st.size());
+
+    st.clear();
+    t.assert_true("clear", st.empty());
+}
+
+static void test_server_tokens_pos(testing & t) {
+    server_tokens st(llama_tokens{1, 2, 3, 4, 5}, false);
+    t.assert_equal("pos_next()", (llama_pos) 5, st.pos_next());
+    t.assert_equal("pos_next(0)", (llama_pos) 0, st.pos_next(0));
+    t.assert_equal("pos_next(3)", (llama_pos) 3, st.pos_next(3));
+
+    t.assert_equal("size_up_to_pos(0)", (size_t) 0, st.size_up_to_pos(0));
+    t.assert_equal("size_up_to_pos(3)", (size_t) 3, st.size_up_to_pos(3));
+    t.assert_equal("size_up_to_pos clamps", (size_t) 5, st.size_up_to_pos(100));
+}
+
+static void test_server_tokens_keep_first(testing & t) {
+    server_tokens st(llama_tokens{1, 2, 3, 4, 5}, false);
+    st.keep_first(5);
+    t.assert_equal("keep all", (size_t) 5, st.size());
+    st.keep_first(2);
+    t.assert_true("keep 2", st.get_tokens() == llama_tokens({1, 2}));
+    st.keep_first(0);
+    t.assert_true("keep 0", st.empty());
+}
+
+static void test_server_tokens_common_prefix(testing & t) {
+    server_tokens a(llama_tokens{1, 2, 3, 4}, false);
+    server_tokens empty(llama_tokens{}, false);
+
+    t.assert_equal("both empty",  (size_t) 0, empty.get_common_prefix(empty));
+    t.assert_equal("one empty",   (size_t) 0, a.get_common_prefix(empty));
+    t.assert_equal("identical",   (size_t) 4, a.get_common_prefix(a));
+    t.assert_equal("diverge at 2", (size_t) 2, a.get_common_prefix(server_tokens(llama_tokens{1, 2, 9, 4}, false)));
+    t.assert_equal("diverge at 0", (size_t) 0, a.get_common_prefix(server_tokens(llama_tokens{9, 2, 3, 4}, false)));
+    t.assert_equal("b is prefix",  (size_t) 2, a.get_common_prefix(server_tokens(llama_tokens{1, 2}, false)));
+    t.assert_equal("a is prefix",  (size_t) 4, a.get_common_prefix(server_tokens(llama_tokens{1, 2, 3, 4, 5, 6}, false)));
+}
+
+static void test_server_tokens_serialize(testing & t) {
+    // same packing as slot save/restore in server-context.cpp
+    auto pack = [](const std::vector<char> & data) {
+        llama_tokens packed(data.size() / sizeof(llama_token));
+        memcpy(packed.data(), data.data(), data.size());
+        return packed;
+    };
+
+    t.test("roundtrip", [&](testing & t) {
+        server_tokens st(llama_tokens{10, 20, 30}, false);
+        std::vector<char> data = st.serialize();
+        t.assert_equal("aligned to llama_token", (size_t) 0, data.size() % sizeof(llama_token));
+
+        server_tokens restored = server_tokens::deserialize(pack(data), false);
+        t.assert_true("tokens", restored.get_tokens() == st.get_tokens());
+    });
+
+    t.test("roundtrip empty", [&](testing & t) {
+        server_tokens st(llama_tokens{}, false);
+        server_tokens restored = server_tokens::deserialize(pack(st.serialize()), false);
+        t.assert_true("empty", restored.empty());
+    });
+
+    t.test("legacy plain token list", [&](testing & t) {
+        server_tokens restored = server_tokens::deserialize(llama_tokens{7, 8, 9}, false);
+        t.assert_true("tokens", restored.get_tokens() == llama_tokens({7, 8, 9}));
+    });
+
+    auto expect_throw = [](testing & t, const std::string & msg, const llama_tokens & packed) {
+        bool thrown = false;
+        try {
+            server_tokens::deserialize(packed, false);
+        } catch (const std::exception &) {
+            thrown = true;
+        }
+        t.assert_true(msg, thrown);
+    };
+
+    t.test("corrupted", [&](testing & t) {
+        llama_tokens good = pack(server_tokens(llama_tokens{10, 20, 30}, false).serialize());
+
+        llama_tokens bad_version = good;
+        bad_version[1] = 999;
+        expect_throw(t, "bad version", bad_version);
+
+        llama_tokens truncated = good;
+        truncated.pop_back();
+        expect_throw(t, "truncated", truncated);
+
+        llama_tokens trailing = good;
+        trailing.push_back(42);
+        expect_throw(t, "trailing data", trailing);
+
+        llama_tokens huge_count = good;
+        huge_count[2] = 0x7fffffff;
+        expect_throw(t, "huge token count", huge_count);
+
+        expect_throw(t, "marker only", llama_tokens{LLAMA_TOKEN_NULL});
+    });
+}
+
+static void test_server_tokens_clone(testing & t) {
+    server_tokens st(llama_tokens{1, 2, 3}, false);
+    server_tokens cl = st.clone();
+    cl.push_back(4);
+    t.assert_equal("original untouched", (size_t) 3, st.size());
+    t.assert_equal("clone grew", (size_t) 4, cl.size());
+    t.assert_true("get_text_tokens", cl.get_text_tokens() == llama_tokens({1, 2, 3, 4}));
+}
+
+static void test_server_tokens(testing & t) {
+    t.test("basic",         test_server_tokens_basic);
+    t.test("pos",           test_server_tokens_pos);
+    t.test("keep_first",    test_server_tokens_keep_first);
+    t.test("common_prefix", test_server_tokens_common_prefix);
+    t.test("serialize",     test_server_tokens_serialize);
+    t.test("clone",         test_server_tokens_clone);
+}
+
+//
+// string / json helpers
+//
+
+static void test_utf8(testing & t) {
+    t.test("validate_utf8", [](testing & t) {
+        t.assert_equal("empty",            (size_t) 0, validate_utf8(""));
+        t.assert_equal("ascii",            (size_t) 3, validate_utf8("abc"));
+        t.assert_equal("full 2-byte",      (size_t) 3, validate_utf8("a\xC3\xA9"));
+        t.assert_equal("full 3-byte",      (size_t) 4, validate_utf8("a\xE2\x82\xAC"));
+        t.assert_equal("full 4-byte",      (size_t) 5, validate_utf8("a\xF0\x9F\x98\x80"));
+        t.assert_equal("cut 2-byte (1/2)", (size_t) 1, validate_utf8("a\xC3"));
+        t.assert_equal("cut 3-byte (1/3)", (size_t) 1, validate_utf8("a\xE2"));
+        t.assert_equal("cut 3-byte (2/3)", (size_t) 1, validate_utf8("a\xE2\x82"));
+        t.assert_equal("cut 4-byte (1/4)", (size_t) 1, validate_utf8("a\xF0"));
+        t.assert_equal("cut 4-byte (2/4)", (size_t) 1, validate_utf8("a\xF0\x9F"));
+        t.assert_equal("cut 4-byte (3/4)", (size_t) 1, validate_utf8("a\xF0\x9F\x98"));
+        t.assert_equal("cut only",         (size_t) 0, validate_utf8("\xF0\x9F\x98"));
+        t.assert_equal("full then cut",    (size_t) 4, validate_utf8("\xF0\x9F\x98\x80\xC3"));
+    });
+
+    t.test("is_valid_utf8", [](testing & t) {
+        t.assert_true("empty",          is_valid_utf8(""));
+        t.assert_true("ascii",          is_valid_utf8("hello"));
+        t.assert_true("multi-byte",     is_valid_utf8("\xC3\xA9\xE2\x82\xAC\xF0\x9F\x98\x80"));
+        t.assert_true("truncated",      !is_valid_utf8("\xE2\x82"));
+        t.assert_true("bad cont. byte", !is_valid_utf8("\xC3\x41"));
+        t.assert_true("lone cont. byte", !is_valid_utf8("\x80"));
+        t.assert_true("invalid lead",   !is_valid_utf8("\xFF"));
+    });
+}
+
+static void test_sse(testing & t) {
+    t.test("oai", [](testing & t) {
+        t.assert_equal("single", std::string("data: {\"a\":1}\n\n"), format_oai_sse(json{{"a", 1}}));
+        t.assert_equal("array",  std::string("data: {\"a\":1}\n\ndata: {\"b\":2}\n\n"),
+                format_oai_sse(json::array({json{{"a", 1}}, json{{"b", 2}}})));
+        t.assert_equal("invalid utf8 replaced", std::string("data: \"x\xEF\xBF\xBD\"\n\n"), format_oai_sse(json("x\xC3")));
+    });
+
+    t.test("oai_resp", [](testing & t) {
+        json ev = {{"event", "response.created"}, {"data", {{"id", "r1"}}}};
+        t.assert_equal("single", std::string("event: response.created\ndata: {\"id\":\"r1\"}\n\n"), format_oai_resp_sse(ev));
+        t.assert_equal("array",  std::string("event: response.created\ndata: {\"id\":\"r1\"}\n\nevent: response.created\ndata: {\"id\":\"r1\"}\n\n"),
+                format_oai_resp_sse(json::array({ev, ev})));
+    });
+
+    t.test("anthropic", [](testing & t) {
+        json ev = {{"event", "message_start"}, {"data", {{"type", "message_start"}}}};
+        t.assert_equal("with event", std::string("event: message_start\ndata: {\"type\":\"message_start\"}\n\n"), format_anthropic_sse(ev));
+        t.assert_equal("data only",  std::string("data: {\"type\":\"ping\"}\n\n"), format_anthropic_sse(json{{"type", "ping"}}));
+    });
+}
+
+static void test_json_helpers(testing & t) {
+    t.test("json_is_array_of_numbers", [](testing & t) {
+        t.assert_true("ints",         json_is_array_of_numbers(json::array({1, 2, 3})));
+        t.assert_true("empty array",  json_is_array_of_numbers(json::array()));
+        t.assert_true("float",        !json_is_array_of_numbers(json::array({1, 2.5})));
+        t.assert_true("string",       !json_is_array_of_numbers(json::array({1, "a"})));
+        t.assert_true("not an array", !json_is_array_of_numbers(json(1)));
+    });
+
+    t.test("json_is_array_of_mixed_numbers_strings", [](testing & t) {
+        t.assert_true("mixed",        json_is_array_of_mixed_numbers_strings(json::array({"a", 1})));
+        t.assert_true("numbers only", !json_is_array_of_mixed_numbers_strings(json::array({1, 2})));
+        t.assert_true("strings only", !json_is_array_of_mixed_numbers_strings(json::array({"a", "b"})));
+        t.assert_true("empty",        !json_is_array_of_mixed_numbers_strings(json::array()));
+    });
+
+    t.test("json_is_array_and_contains_numbers", [](testing & t) {
+        t.assert_true("contains",     json_is_array_and_contains_numbers(json::array({"a", 1})));
+        t.assert_true("strings only", !json_is_array_and_contains_numbers(json::array({"a"})));
+        t.assert_true("not an array", !json_is_array_and_contains_numbers(json(1)));
+    });
+
+    t.test("json_get_nested_values", [](testing & t) {
+        json js = {{"a", {{"b", {{"c", 1}}}}}, {"x", 2}};
+        json res = json_get_nested_values({"a/b/c", "x", "a/b", "a/missing", "x/y"}, js);
+        t.assert_equal("nested",  1, res.at("a/b/c").get<int>());
+        t.assert_equal("top",     2, res.at("x").get<int>());
+        t.assert_true("object",   res.at("a/b") == json{{"c", 1}});
+        t.assert_true("missing",  !res.contains("a/missing"));
+        t.assert_true("not obj",  !res.contains("x/y"));
+        t.assert_equal("count", (size_t) 3, res.size());
+    });
+}
+
+static void test_lora_helpers(testing & t) {
+    // only pointer identity is used, never dereferenced
+    auto * p0 = reinterpret_cast<llama_adapter_lora *>(0x10);
+    auto * p1 = reinterpret_cast<llama_adapter_lora *>(0x20);
+
+    auto make = [](llama_adapter_lora * ptr, float scale) {
+        common_adapter_lora_info info;
+        info.scale = scale;
+        info.ptr   = ptr;
+        return info;
+    };
+
+    t.test("parse_lora_request", [](testing & t) {
+        auto res = parse_lora_request(json::array({
+            json{{"id", 0}, {"scale", 0.5}},
+            json{{"id", 2}, {"scale", 1.0}},
+            json{{"id", 0}, {"scale", 0.25}}, // last one wins
+        }));
+        t.assert_equal("count", (size_t) 2, res.size());
+        t.assert_equal("id 0", 0.25f, res.at(0));
+        t.assert_equal("id 2", 1.0f,  res.at(2));
+
+        auto defaults = parse_lora_request(json::array({json::object()}));
+        t.assert_equal("default id/scale", 0.0f, defaults.at(-1));
+    });
+
+    t.test("are_lora_equal", [&](testing & t) {
+        t.assert_true("both empty",   are_lora_equal({}, {}));
+        t.assert_true("same",         are_lora_equal({make(p0, 1.0f)}, {make(p0, 1.0f)}));
+        t.assert_true("diff scale",   !are_lora_equal({make(p0, 1.0f)}, {make(p0, 0.5f)}));
+        t.assert_true("diff ptr",     !are_lora_equal({make(p0, 1.0f)}, {make(p1, 1.0f)}));
+        t.assert_true("diff size",    !are_lora_equal({make(p0, 1.0f)}, {}));
+    });
+
+    t.test("lora_get_enabled_ids", [&](testing & t) {
+        auto ids = lora_get_enabled_ids({make(p0, 0.0f), make(p1, 0.5f), make(p0, -1.0f), make(p1, 2.0f)});
+        t.assert_true("ids", ids == std::vector<size_t>({1, 3}));
+    });
+}
+
+//
+// server_pipe
+//
+
+static void test_server_pipe(testing & t) {
+    auto stop_now = []() { return true; };
+
+    t.test("drain then eof", [](testing & t) {
+        server_pipe<int> p;
+        t.assert_true("write 1", p.write(1));
+        t.assert_true("write 2", p.write(2));
+        p.close_write();
+
+        int v = 0;
+        t.assert_true("read 1", p.read(v, nullptr) && v == 1);
+        t.assert_true("read 2", p.read(v, nullptr) && v == 2);
+        t.assert_true("eof", !p.read(v, nullptr));
+    });
+
+    t.test("broken pipe", [](testing & t) {
+        server_pipe<int> p;
+        p.close_read();
+        t.assert_true("write fails", !p.write(1));
+    });
+
+    t.test("max_size drops oldest", [](testing & t) {
+        server_pipe<int> p;
+        p.max_size = 2;
+        p.write(1);
+        p.write(2);
+        p.write(3);
+        p.close_write();
+
+        int v = 0;
+        t.assert_true("read 2", p.read(v, nullptr) && v == 2);
+        t.assert_true("read 3", p.read(v, nullptr) && v == 3);
+        t.assert_true("eof", !p.read(v, nullptr));
+    });
+
+    t.test("queued data wins over should_stop", [&](testing & t) {
+        server_pipe<int> p;
+        p.write(1);
+        int v = 0;
+        t.assert_true("read", p.read(v, stop_now) && v == 1);
+    });
+
+    t.test("should_stop closes the pipe", [&](testing & t) {
+        server_pipe<int> p;
+        int v = 0;
+        t.assert_true("read stopped", !p.read(v, stop_now));
+        t.assert_true("write fails", !p.write(1));
+    });
+
+    t.test("should_stop as deadline keeps the pipe open", [&](testing & t) {
+        server_pipe<int> p;
+        int v = 0;
+        t.assert_true("read stopped", !p.read(v, stop_now, /* close_on_stop */ false));
+        t.assert_true("write ok", p.write(1));
+        t.assert_true("read again", p.read(v, nullptr) && v == 1);
+    });
+
+    t.test("cross thread", [](testing & t) {
+        constexpr int n = 1000;
+        server_pipe<int> p;
+        std::thread producer([&]() {
+            for (int i = 0; i < n; i++) {
+                p.write(int(i));
+            }
+            p.close_write();
+        });
+
+        int v = 0;
+        int count = 0;
+        bool in_order = true;
+        while (p.read(v, nullptr)) {
+            in_order &= v == count;
+            count++;
+        }
+        producer.join();
+
+        t.assert_equal("count", n, count);
+        t.assert_true("in order", in_order);
+    });
+}
+
+int main(int argc, char ** argv) {
+    testing t;
+
+    const char * verbose = getenv("LLAMA_TEST_VERBOSE");
+    if (verbose) {
+        t.verbose = std::string(verbose) == "1";
+    }
+
+    if (argc > 1) {
+        t.set_filter(argv[1]);
+    }
+
+    t.test("server_tokens", test_server_tokens);
+    t.test("utf8",          test_utf8);
+    t.test("sse",           test_sse);
+    t.test("json_helpers",  test_json_helpers);
+    t.test("lora_helpers",  test_lora_helpers);
+    t.test("server_pipe",   test_server_pipe);
+
+    return t.summary();
+}