Skip to content

Commit 786ab51

Browse files
committed
test: add C unit tests and update Makefile for testing
Introduces a new C unit test runner at tests/c/unittest.c with basic tests for the SQLite AI extension. The Makefile is updated to build and run these tests, including logic to download a test model if needed and link against the appropriate SQLite libraries.
1 parent 7c2324f commit 786ab51

File tree

2 files changed

+282
-2
lines changed

2 files changed

+282
-2
lines changed

Makefile

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ SRC_DIR = src
2929
DIST_DIR = dist
3030
VPATH = $(SRC_DIR)
3131
BUILD_DIR = build
32+
CTEST_BIN = $(BUILD_DIR)/tests/sqlite_ai_tests
33+
TEST_MODEL_DIR = tests/models/unsloth/gemma-3-270m-it-GGUF
34+
TEST_MODEL_FILE = $(TEST_MODEL_DIR)/gemma-3-270m-it-UD-IQ2_M.gguf
35+
TEST_MODEL_URL = https://huggingface.co/unsloth/gemma-3-270m-it-GGUF/resolve/main/gemma-3-270m-it-UD-IQ2_M.gguf
3236
LLAMA_DIR = modules/llama.cpp
3337
WHISPER_DIR = modules/whisper.cpp
3438
MINIAUDIO_DIR = modules/miniaudio
@@ -55,6 +59,16 @@ LLAMA_LDFLAGS = -L./$(BUILD_LLAMA)/common -L./$(BUILD_GGML)/lib -L./$(BUILD_LLAM
5559
WHISPER_LDFLAGS = -L./$(BUILD_WHISPER)/src -lwhisper
5660
MINIAUDIO_LDFLAGS = -L./$(BUILD_MINIAUDIO) -lminiaudio -lminiaudio_channel_combiner_node -lminiaudio_channel_separator_node -lminiaudio_ltrim_node -lminiaudio_reverb_node -lminiaudio_vocoder_node
5761
LDFLAGS = $(LLAMA_LDFLAGS) $(WHISPER_LDFLAGS) $(MINIAUDIO_LDFLAGS)
62+
SQLITE_TEST_LIBS = -lpthread -lm
63+
ifneq ($(PLATFORM),macos)
64+
SQLITE_TEST_LIBS += -ldl
65+
endif
66+
ifneq ($(SQLITE3),sqlite3)
67+
SQLITE3_BINDIR := $(dir $(SQLITE3))
68+
SQLITE3_PREFIX := $(abspath $(SQLITE3_BINDIR)/..)
69+
SQLITE_TEST_LIBS += -L$(SQLITE3_PREFIX)/lib
70+
endif
71+
SQLITE_TEST_LIBS += -lsqlite3
5872

5973
# Files
6074
SRC_FILES = $(wildcard $(SRC_DIR)/*.c)
@@ -210,8 +224,17 @@ endif
210224
$(BUILD_DIR)/%.o: %.c $(BUILD_DIR)/llama.cpp.stamp
211225
$(CC) $(CFLAGS) -O3 -fPIC -c $< -o $@
212226

213-
test: $(TARGET)
214-
$(SQLITE3) ":memory:" -cmd ".bail on" ".load ./dist/ai" "SELECT ai_version();"
227+
$(CTEST_BIN): tests/c/unittest.c
228+
@mkdir -p $(dir $@)
229+
$(CC) -std=c11 -Wall -Wextra -I$(SRC_DIR) tests/c/unittest.c -o $@ $(SQLITE_TEST_LIBS)
230+
231+
$(TEST_MODEL_FILE):
232+
@mkdir -p $(TEST_MODEL_DIR)
233+
curl -L --fail --retry 3 -o $@ $(TEST_MODEL_URL)
234+
235+
test: $(TARGET) $(CTEST_BIN) $(TEST_MODEL_FILE)
236+
$(SQLITE3) ":memory:" -cmd ".bail on" ".load ./dist/ai" "SELECT ai_version();"
237+
$(CTEST_BIN) --extension "$(TARGET)" --model "$(TEST_MODEL_FILE)"
215238

216239
# Build submodules
217240
ifeq ($(PLATFORM),windows)

tests/c/unittest.c

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#ifndef SQLITE_ENABLE_LOAD_EXTENSION
2+
#define SQLITE_ENABLE_LOAD_EXTENSION 1
3+
#endif
4+
#include "sqlite3.h"
5+
6+
#include <stdbool.h>
7+
#include <stdio.h>
8+
#include <stdlib.h>
9+
#include <string.h>
10+
11+
#ifdef SQLITEAI_LOAD_FROM_SOURCES
12+
#include "sqlite-ai.h"
13+
#endif
14+
15+
#define DEFAULT_MODEL_PATH "tests/models/unsloth/gemma-3-270m-it-GGUF/gemma-3-270m-it-UD-IQ2_M.gguf"
16+
17+
typedef struct {
18+
const char *extension_path;
19+
const char *model_path;
20+
bool verbose;
21+
} test_env;
22+
23+
typedef int (*test_fn)(const test_env *env);
24+
25+
typedef struct {
26+
const char *name;
27+
test_fn fn;
28+
} test_case;
29+
30+
static void usage(const char *prog) {
31+
fprintf(stderr, "Usage: %s [--extension /path/to/ai] [--model /path/to/model] [--verbose]\n", prog);
32+
}
33+
34+
static int expect_error_contains(const char *err_msg, const char *needle) {
35+
if (!err_msg) {
36+
fprintf(stderr, "Expected SQLite error message but got NULL\n");
37+
return 1;
38+
}
39+
if (!strstr(err_msg, needle)) {
40+
fprintf(stderr, "Expected error to contain \"%s\", got: %s\n", needle, err_msg);
41+
return 1;
42+
}
43+
return 0;
44+
}
45+
46+
static int open_db_and_load(const test_env *env, sqlite3 **out_db) {
47+
sqlite3 *db = NULL;
48+
int rc = sqlite3_open(":memory:", &db);
49+
if (rc != SQLITE_OK) {
50+
fprintf(stderr, "sqlite3_open failed: %s\n", db ? sqlite3_errmsg(db) : "unknown error");
51+
if (db) sqlite3_close(db);
52+
return rc;
53+
}
54+
sqlite3_enable_load_extension(db, 1);
55+
char *errmsg = NULL;
56+
#ifdef SQLITEAI_LOAD_FROM_SOURCES
57+
rc = sqlite3_ai_init(db, NULL, NULL);
58+
#else
59+
rc = sqlite3_load_extension(db, env->extension_path, NULL, &errmsg);
60+
#endif
61+
if (rc != SQLITE_OK) {
62+
fprintf(stderr, "sqlite3_load_extension failed: %s\n", errmsg ? errmsg : sqlite3_errmsg(db));
63+
if (errmsg) sqlite3_free(errmsg);
64+
sqlite3_close(db);
65+
return rc;
66+
}
67+
if (errmsg) sqlite3_free(errmsg);
68+
*out_db = db;
69+
return SQLITE_OK;
70+
}
71+
72+
typedef struct {
73+
const test_env *env;
74+
} exec_userdata;
75+
76+
static int verbose_callback(void *udata, int columns, char **values, char **names) {
77+
exec_userdata *ud = (exec_userdata *)udata;
78+
if (!ud || !ud->env || !ud->env->verbose) {
79+
return 0;
80+
}
81+
printf("[SQL] row:\n");
82+
for (int i = 0; i < columns; ++i) {
83+
printf(" %s = %s\n", names[i] ? names[i] : "(null)", values[i] ? values[i] : "NULL");
84+
}
85+
return 0;
86+
}
87+
88+
static int exec_expect_error(const test_env *env, sqlite3 *db, const char *sql, const char *needle) {
89+
if (env->verbose) {
90+
printf("[SQL] %s\n", sql);
91+
}
92+
char *errmsg = NULL;
93+
exec_userdata udata = {.env = env};
94+
int rc = sqlite3_exec(db, sql, env->verbose ? verbose_callback : NULL, env->verbose ? &udata : NULL, &errmsg);
95+
if (rc == SQLITE_OK) {
96+
fprintf(stderr, "Expected failure executing SQL: %s\n", sql);
97+
return 1;
98+
}
99+
if (env->verbose && errmsg) {
100+
printf("[SQL][ERROR] %s\n", errmsg);
101+
}
102+
int status = expect_error_contains(errmsg, needle);
103+
sqlite3_free(errmsg);
104+
return status;
105+
}
106+
107+
static int exec_expect_ok(const test_env *env, sqlite3 *db, const char *sql) {
108+
if (env->verbose) {
109+
printf("[SQL] %s\n", sql);
110+
}
111+
char *errmsg = NULL;
112+
exec_userdata udata = {.env = env};
113+
int rc = sqlite3_exec(db, sql, env->verbose ? verbose_callback : NULL, env->verbose ? &udata : NULL, &errmsg);
114+
if (rc != SQLITE_OK) {
115+
fprintf(stderr, "SQL execution failed (%d): %s\n", rc, errmsg ? errmsg : sqlite3_errmsg(db));
116+
if (errmsg) sqlite3_free(errmsg);
117+
return 1;
118+
}
119+
if (errmsg) sqlite3_free(errmsg);
120+
return 0;
121+
}
122+
123+
static int test_issue15_chat_without_context(const test_env *env) {
124+
sqlite3 *db = NULL;
125+
if (open_db_and_load(env, &db) != SQLITE_OK) {
126+
return 1;
127+
}
128+
129+
int rc = exec_expect_error(env, db, "SELECT llm_chat_create();", "Please call llm_context_create()");
130+
sqlite3_close(db);
131+
return rc;
132+
}
133+
134+
static int test_llm_chat_respond_repeated(const test_env *env) {
135+
sqlite3 *db = NULL;
136+
if (open_db_and_load(env, &db) != SQLITE_OK) {
137+
return 1;
138+
}
139+
140+
char sqlbuf[512];
141+
const char *model = env->model_path ? env->model_path : DEFAULT_MODEL_PATH;
142+
snprintf(sqlbuf, sizeof(sqlbuf), "SELECT llm_model_load('%s');", model);
143+
if (exec_expect_ok(env, db, sqlbuf) != 0) {
144+
sqlite3_close(db);
145+
return 1;
146+
}
147+
if (exec_expect_ok(env, db, "SELECT llm_context_create('context_size=32000');") != 0) {
148+
sqlite3_close(db);
149+
return 1;
150+
}
151+
if (exec_expect_ok(env, db, "SELECT llm_chat_create();") != 0) {
152+
sqlite3_close(db);
153+
return 1;
154+
}
155+
156+
const int iterations = 3;
157+
for (int i = 0; i < iterations; ++i) {
158+
if (exec_expect_ok(env, db, "SELECT llm_chat_respond('Hello world');") != 0) {
159+
sqlite3_close(db);
160+
return 1;
161+
}
162+
163+
if (exec_expect_ok(env, db, "SELECT llm_context_usage();") != 0) {
164+
sqlite3_close(db);
165+
return 1;
166+
}
167+
}
168+
169+
if (exec_expect_ok(env, db, "SELECT llm_chat_free();") != 0) {
170+
sqlite3_close(db);
171+
return 1;
172+
}
173+
if (exec_expect_ok(env, db, "SELECT llm_context_free();") != 0) {
174+
sqlite3_close(db);
175+
return 1;
176+
}
177+
if (exec_expect_ok(env, db, "SELECT llm_model_free();") != 0) {
178+
sqlite3_close(db);
179+
return 1;
180+
}
181+
182+
sqlite3_close(db);
183+
184+
sqlite3_int64 current = 0;
185+
sqlite3_int64 highwater = 0;
186+
if (sqlite3_status64(SQLITE_STATUS_MEMORY_USED, &current, &highwater, 0) != SQLITE_OK) {
187+
fprintf(stderr, "[chat_respond_repeated] sqlite3_status64 failed\n");
188+
return 1;
189+
}
190+
if (env->verbose) {
191+
printf("[STATUS] memory current=%lld highwater=%lld\n", (long long)current, (long long)highwater);
192+
}
193+
if (current > 0 || highwater <= 0) {
194+
fprintf(stderr, "[chat_respond_repeated] invalid memory stats: current=%lld highwater=%lld\n",
195+
(long long)current, (long long)highwater);
196+
return 1;
197+
}
198+
199+
return 0;
200+
}
201+
202+
static const test_case TESTS[] = {
203+
{"issue15_llm_chat_without_context", test_issue15_chat_without_context},
204+
{"llm_chat_respond_repeated", test_llm_chat_respond_repeated},
205+
};
206+
207+
int main(int argc, char **argv) {
208+
test_env env = {
209+
.extension_path = "./dist/ai",
210+
.model_path = NULL,
211+
.verbose = false,
212+
};
213+
214+
for (int i = 1; i < argc; ++i) {
215+
if (strcmp(argv[i], "--extension") == 0) {
216+
if (++i >= argc) {
217+
usage(argv[0]);
218+
return EXIT_FAILURE;
219+
}
220+
env.extension_path = argv[i];
221+
} else if (strcmp(argv[i], "--model") == 0) {
222+
if (++i >= argc) {
223+
usage(argv[0]);
224+
return EXIT_FAILURE;
225+
}
226+
env.model_path = argv[i];
227+
} else if (strcmp(argv[i], "--verbose") == 0) {
228+
env.verbose = true;
229+
} else if (strcmp(argv[i], "--help") == 0) {
230+
usage(argv[0]);
231+
return EXIT_SUCCESS;
232+
} else {
233+
fprintf(stderr, "Unknown argument: %s\n", argv[i]);
234+
usage(argv[0]);
235+
return EXIT_FAILURE;
236+
}
237+
}
238+
239+
size_t total = sizeof(TESTS) / sizeof(TESTS[0]);
240+
int failures = 0;
241+
242+
printf("Running %zu C test(s)\n\n", total);
243+
for (size_t i = 0; i < total; ++i) {
244+
const test_case *tc = &TESTS[i];
245+
int rc = tc->fn(&env);
246+
printf("- %s ... %s\n", tc->name, rc == 0 ? "PASS" : "FAIL");
247+
if (rc != 0) failures += 1;
248+
}
249+
250+
if (failures) {
251+
fprintf(stderr, "\n%d C test(s) failed.\n", failures);
252+
return EXIT_FAILURE;
253+
}
254+
255+
printf("\nAll C tests passed.\n");
256+
return EXIT_SUCCESS;
257+
}

0 commit comments

Comments
 (0)