-
Notifications
You must be signed in to change notification settings - Fork 0
Add unit coverage for core and bare-metal HALs #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,193 @@ | ||||||||||||||
| #include "test_runner.h" | ||||||||||||||
| #include "ec_io.h" | ||||||||||||||
| #include "ec_socket.h" | ||||||||||||||
|
|
||||||||||||||
| #include <stdint.h> | ||||||||||||||
| #include <string.h> | ||||||||||||||
|
|
||||||||||||||
| typedef struct { | ||||||||||||||
| const char *input; | ||||||||||||||
| size_t input_pos; | ||||||||||||||
| char output[128]; | ||||||||||||||
| size_t output_len; | ||||||||||||||
| } fake_uart_t; | ||||||||||||||
|
|
||||||||||||||
| static int fake_uart_read(void *buf, size_t len, fake_uart_t *uart) | ||||||||||||||
| { | ||||||||||||||
| (void)len; | ||||||||||||||
| if (!uart->input[uart->input_pos]) return 0; | ||||||||||||||
| *(char *)buf = uart->input[uart->input_pos++]; | ||||||||||||||
| return 1; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int fake_uart_write(const void *buf, size_t len, fake_uart_t *uart) | ||||||||||||||
| { | ||||||||||||||
| if (uart->output_len + len >= sizeof(uart->output)) return -1; | ||||||||||||||
| memcpy(uart->output + uart->output_len, buf, len); | ||||||||||||||
| uart->output_len += len; | ||||||||||||||
| uart->output[uart->output_len] = '\0'; | ||||||||||||||
| return (int)len; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static fake_uart_t *s_uart_ctx; | ||||||||||||||
|
|
||||||||||||||
| static int uart_read_wrapper(void *buf, size_t len, uint32_t timeout_ms) | ||||||||||||||
| { | ||||||||||||||
| (void)timeout_ms; | ||||||||||||||
| return fake_uart_read(buf, len, s_uart_ctx); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int uart_write_wrapper(const void *buf, size_t len, uint32_t timeout_ms) | ||||||||||||||
| { | ||||||||||||||
| (void)timeout_ms; | ||||||||||||||
| return fake_uart_write(buf, len, s_uart_ctx); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| typedef struct { | ||||||||||||||
| int connect_calls; | ||||||||||||||
| int send_calls; | ||||||||||||||
| int recv_calls; | ||||||||||||||
| int close_calls; | ||||||||||||||
| const char *host; | ||||||||||||||
| uint16_t port; | ||||||||||||||
| int use_tls; | ||||||||||||||
| char sent[64]; | ||||||||||||||
| const char *recv_data; | ||||||||||||||
| } fake_socket_t; | ||||||||||||||
|
|
||||||||||||||
| static int fake_connect(void *ctx, const char *host, uint16_t port, int use_tls) | ||||||||||||||
| { | ||||||||||||||
| fake_socket_t *sock = (fake_socket_t *)ctx; | ||||||||||||||
| sock->connect_calls++; | ||||||||||||||
| sock->host = host; | ||||||||||||||
| sock->port = port; | ||||||||||||||
| sock->use_tls = use_tls; | ||||||||||||||
| return 0; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int fake_send(void *ctx, const void *data, size_t len) | ||||||||||||||
| { | ||||||||||||||
| fake_socket_t *sock = (fake_socket_t *)ctx; | ||||||||||||||
| sock->send_calls++; | ||||||||||||||
| if (len >= sizeof(sock->sent)) return -1; | ||||||||||||||
| memcpy(sock->sent, data, len); | ||||||||||||||
| sock->sent[len] = '\0'; | ||||||||||||||
| return (int)len; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int fake_recv(void *ctx, void *buf, size_t len, uint32_t timeout_ms) | ||||||||||||||
| { | ||||||||||||||
| fake_socket_t *sock = (fake_socket_t *)ctx; | ||||||||||||||
| (void)timeout_ms; | ||||||||||||||
| sock->recv_calls++; | ||||||||||||||
| size_t n = strlen(sock->recv_data); | ||||||||||||||
| if (n > len) n = len; | ||||||||||||||
| memcpy(buf, sock->recv_data, n); | ||||||||||||||
| return (int)n; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static void fake_close(void *ctx) | ||||||||||||||
| { | ||||||||||||||
| fake_socket_t *sock = (fake_socket_t *)ctx; | ||||||||||||||
| sock->close_calls++; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int test_baremetal_uart_reads_lines_and_writes(void) | ||||||||||||||
| { | ||||||||||||||
| fake_uart_t uart; | ||||||||||||||
| memset(&uart, 0, sizeof(uart)); | ||||||||||||||
| uart.input = "hello\r\n"; | ||||||||||||||
| s_uart_ctx = &uart; | ||||||||||||||
|
|
||||||||||||||
| ec_io_uart_hal_t hal = { | ||||||||||||||
| .read = uart_read_wrapper, | ||||||||||||||
| .write = uart_write_wrapper, | ||||||||||||||
| }; | ||||||||||||||
| ec_io_uart_set_hal(&hal); | ||||||||||||||
| ec_io_init(&ec_io_uart_ops); | ||||||||||||||
|
|
||||||||||||||
| char line[16]; | ||||||||||||||
| ASSERT_EQ(ec_io_read_line(line, sizeof(line)), 5, | ||||||||||||||
| "bare-metal UART should read a line"); | ||||||||||||||
| ASSERT_EQ(strcmp(line, "hello"), 0, "bare-metal UART should strip CRLF"); | ||||||||||||||
| ASSERT_EQ(ec_io_write("ok"), 0, "bare-metal UART should write"); | ||||||||||||||
| ASSERT_EQ(strcmp(uart.output, "ok"), 0, "write should reach HAL"); | ||||||||||||||
| return 1; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int test_baremetal_uart_truncates_long_lines(void) | ||||||||||||||
| { | ||||||||||||||
| fake_uart_t uart; | ||||||||||||||
| memset(&uart, 0, sizeof(uart)); | ||||||||||||||
| uart.input = "abcdef\n"; | ||||||||||||||
| s_uart_ctx = &uart; | ||||||||||||||
|
|
||||||||||||||
| ec_io_uart_hal_t hal = { | ||||||||||||||
| .read = uart_read_wrapper, | ||||||||||||||
| .write = uart_write_wrapper, | ||||||||||||||
| }; | ||||||||||||||
| ec_io_uart_set_hal(&hal); | ||||||||||||||
| ec_io_init(&ec_io_uart_ops); | ||||||||||||||
|
|
||||||||||||||
| char line[4]; | ||||||||||||||
| ASSERT_EQ(ec_io_read_line(line, sizeof(line)), 0, | ||||||||||||||
| "truncated line should return an empty command"); | ||||||||||||||
| ASSERT_EQ(line[0], '\0', "truncated line should clear output buffer"); | ||||||||||||||
| ASSERT_STR(uart.output, "input truncated", | ||||||||||||||
| "truncation warning should be written through HAL"); | ||||||||||||||
| return 1; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| static int test_baremetal_socket_delegates_to_hal(void) | ||||||||||||||
| { | ||||||||||||||
| fake_socket_t fake; | ||||||||||||||
| memset(&fake, 0, sizeof(fake)); | ||||||||||||||
| fake.recv_data = "HTTP/1.1 200 OK\r\n\r\n{}"; | ||||||||||||||
|
|
||||||||||||||
| ec_socket_baremetal_hal_t hal = { | ||||||||||||||
| .ctx = &fake, | ||||||||||||||
| .connect = fake_connect, | ||||||||||||||
| .send = fake_send, | ||||||||||||||
| .recv = fake_recv, | ||||||||||||||
| .close = fake_close, | ||||||||||||||
| }; | ||||||||||||||
| ec_socket_baremetal_set_hal(&hal); | ||||||||||||||
|
|
||||||||||||||
| ec_socket_t *sock = ec_socket_connect("api.example.test", 443, 1); | ||||||||||||||
| ASSERT(sock != 0, "connect should return a socket handle"); | ||||||||||||||
| ASSERT_EQ(fake.connect_calls, 1, "connect callback should run"); | ||||||||||||||
| ASSERT_EQ(strcmp(fake.host, "api.example.test"), 0, "host should be passed through"); | ||||||||||||||
| ASSERT_EQ(fake.port, 443, "port should be passed through"); | ||||||||||||||
| ASSERT_EQ(fake.use_tls, 1, "TLS flag should be passed through"); | ||||||||||||||
|
|
||||||||||||||
| ASSERT(ec_socket_connect("second.example.test", 80, 0) == 0, | ||||||||||||||
| "single static bare-metal socket should reject concurrent connect"); | ||||||||||||||
|
|
||||||||||||||
| ASSERT_EQ(ec_socket_send(sock, "ping", 4), 4, "send should delegate to HAL"); | ||||||||||||||
| ASSERT_EQ(strcmp(fake.sent, "ping"), 0, "send payload should reach HAL"); | ||||||||||||||
|
|
||||||||||||||
| char buf[32]; | ||||||||||||||
| int n = ec_socket_recv(sock, buf, sizeof(buf), 100); | ||||||||||||||
| ASSERT(n > 0, "recv should delegate to HAL"); | ||||||||||||||
| buf[n] = '\0'; | ||||||||||||||
|
Comment on lines
+170
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential buffer overflow: if
Suggested change
|
||||||||||||||
| ASSERT_STR(buf, "HTTP/1.1 200 OK", "recv payload should come from HAL"); | ||||||||||||||
|
|
||||||||||||||
| ec_socket_close(sock); | ||||||||||||||
| ASSERT_EQ(fake.close_calls, 1, "close callback should run"); | ||||||||||||||
| ASSERT(ec_socket_connect("after-close.example.test", 80, 0) != 0, | ||||||||||||||
| "connect should work again after close"); | ||||||||||||||
| ec_socket_close(sock); | ||||||||||||||
| return 1; | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| int main(void) | ||||||||||||||
| { | ||||||||||||||
| printf("=== EmbedClaw Bare-metal HAL Tests ===\n\n"); | ||||||||||||||
|
|
||||||||||||||
| RUN_TEST(test_baremetal_uart_reads_lines_and_writes); | ||||||||||||||
| RUN_TEST(test_baremetal_uart_truncates_long_lines); | ||||||||||||||
| RUN_TEST(test_baremetal_socket_delegates_to_hal); | ||||||||||||||
|
|
||||||||||||||
| PRINT_RESULTS(); | ||||||||||||||
| return (_tests_pass == _tests_run) ? 0 : 1; | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| #include "test_runner.h" | ||
| #include "ec_json.h" | ||
| #include "ec_session.h" | ||
|
|
||
| #include <string.h> | ||
|
|
||
| static int test_json_writer_escapes_and_composes(void) | ||
| { | ||
| char buf[256]; | ||
| ec_json_writer_t w; | ||
|
|
||
| ec_json_writer_init(&w, buf, sizeof(buf)); | ||
| ec_json_obj_start(&w); | ||
| ec_json_add_string(&w, "text", "quote: \" slash: \\ newline:\n"); | ||
| ec_json_add_int(&w, "count", 3); | ||
| ec_json_array_start(&w, "items"); | ||
| ec_json_array_obj_start(&w); | ||
| ec_json_add_string(&w, "name", "uart0"); | ||
| ec_json_obj_end(&w); | ||
| ec_json_array_end(&w); | ||
| ec_json_obj_end(&w); | ||
|
|
||
| ASSERT(ec_json_writer_finish(&w) > 0, "writer should finish"); | ||
| ASSERT_STR(buf, "\"text\":\"quote: \\\" slash: \\\\ newline:\\n\"", | ||
| "writer should escape special characters"); | ||
| ASSERT_STR(buf, "\"count\":3", "writer should include integer fields"); | ||
| ASSERT_STR(buf, "\"items\":[{\"name\":\"uart0\"}]", | ||
| "writer should compose arrays of objects"); | ||
| return 1; | ||
| } | ||
|
|
||
| static int test_json_writer_reports_overflow(void) | ||
| { | ||
| char buf[24]; | ||
| ec_json_writer_t w; | ||
|
|
||
| ec_json_writer_init(&w, buf, sizeof(buf)); | ||
| ec_json_obj_start(&w); | ||
| ec_json_add_string(&w, "too_long", "abcdefghijklmnopqrstuvwxyz"); | ||
| ec_json_obj_end(&w); | ||
|
|
||
| ASSERT_EQ(ec_json_writer_finish(&w), -1, "small buffer should overflow"); | ||
| return 1; | ||
| } | ||
|
|
||
| static int test_json_find_nested_strings_and_bounds(void) | ||
| { | ||
| const char json[] = | ||
| "{\"choices\":[{\"message\":{\"content\":\"hello\"," | ||
| "\"tool_calls\":[{\"function\":{\"name\":\"hw_register_read\"}}]}}]}"; | ||
| char out[32]; | ||
|
|
||
| ASSERT_EQ(ec_json_find_string(json, strlen(json), | ||
| "choices[0].message.content", | ||
| out, sizeof(out)), 5, | ||
| "nested string lookup should return length"); | ||
| ASSERT_EQ(strcmp(out, "hello"), 0, "nested string lookup should copy value"); | ||
|
|
||
| ASSERT(ec_json_find_string(json, strlen(json), | ||
| "choices[0].message.tool_calls[0].function.name", | ||
| out, sizeof(out)) > 0, | ||
| "array path lookup should succeed"); | ||
| ASSERT_EQ(strcmp(out, "hw_register_read"), 0, | ||
| "array path lookup should copy tool name"); | ||
|
|
||
| ASSERT(ec_json_find_string(json, strlen(json), | ||
| "choices[1].message.content", | ||
| out, sizeof(out)) < 0, | ||
| "out-of-range array lookup should fail"); | ||
| memset(out, 'x', sizeof(out)); | ||
| ASSERT_EQ(ec_json_find_string(json, strlen(json), | ||
| "choices[0].message.content", | ||
| out, 4), 5, | ||
| "undersized output buffer should still report full value length"); | ||
| ASSERT_EQ(strcmp(out, "hel"), 0, | ||
| "undersized output buffer should contain a safe truncated string"); | ||
| return 1; | ||
| } | ||
|
|
||
| static int test_session_message_view_and_reset(void) | ||
| { | ||
| ec_session_t session; | ||
| ec_model_tool_call_t call; | ||
| size_t count = 0; | ||
|
|
||
| memset(&call, 0, sizeof(call)); | ||
| strcpy(call.id, "call_1"); | ||
| strcpy(call.name, "hw_register_read"); | ||
| strcpy(call.arguments, "{\"address\":\"0x40000000\"}"); | ||
|
|
||
| ec_session_init(&session, "system prompt"); | ||
| ASSERT_EQ(ec_session_append(&session, "user", "hello"), 0, | ||
| "user append should succeed"); | ||
| ASSERT_EQ(ec_session_append_tool_calls(&session, &call, 1), 0, | ||
| "assistant tool-call append should succeed"); | ||
| ASSERT_EQ(ec_session_append_tool_result(&session, "call_1", | ||
| "{\"value\":\"0x00000000\"}"), 0, | ||
| "tool result append should succeed"); | ||
|
|
||
| const ec_model_message_t *messages = ec_session_messages(&session, &count); | ||
| ASSERT_EQ(count, 4, "message view should include system plus three entries"); | ||
| ASSERT_EQ(strcmp(messages[0].role, "system"), 0, "first message is system"); | ||
| ASSERT_EQ(strcmp(messages[1].role, "user"), 0, "second message is user"); | ||
| ASSERT_EQ(strcmp(messages[2].role, "assistant"), 0, | ||
| "third message is assistant tool call"); | ||
| ASSERT_EQ(messages[2].num_tool_calls, 1, "assistant view should expose tool call"); | ||
| ASSERT_EQ(strcmp(messages[2].tool_calls[0].id, "call_1"), 0, | ||
| "tool call id should be preserved"); | ||
| ASSERT_EQ(strcmp(messages[3].role, "tool"), 0, "fourth message is tool result"); | ||
| ASSERT_EQ(strcmp(messages[3].tool_call_id, "call_1"), 0, | ||
| "tool result call id should be preserved"); | ||
|
|
||
| ec_session_reset(&session); | ||
| messages = ec_session_messages(&session, &count); | ||
| ASSERT_EQ(count, 1, "reset should retain only system prompt"); | ||
| ASSERT_EQ(strcmp(messages[0].content, "system prompt"), 0, | ||
| "system prompt should survive reset"); | ||
| return 1; | ||
| } | ||
|
|
||
| static int test_session_rejects_overflow(void) | ||
| { | ||
| ec_session_t session; | ||
| ec_session_init(&session, "system prompt"); | ||
|
|
||
| for (int i = 0; i < EC_CONFIG_MAX_HISTORY; i++) { | ||
| ASSERT_EQ(ec_session_append(&session, "user", "x"), 0, | ||
| "append before max history should succeed"); | ||
| } | ||
| ASSERT_EQ(ec_session_append(&session, "user", "overflow"), -1, | ||
| "append past max history should fail"); | ||
| return 1; | ||
| } | ||
|
|
||
| int main(void) | ||
| { | ||
| printf("=== EmbedClaw Unit Tests ===\n\n"); | ||
|
|
||
| RUN_TEST(test_json_writer_escapes_and_composes); | ||
| RUN_TEST(test_json_writer_reports_overflow); | ||
| RUN_TEST(test_json_find_nested_strings_and_bounds); | ||
| RUN_TEST(test_session_message_view_and_reset); | ||
| RUN_TEST(test_session_rejects_overflow); | ||
|
|
||
| PRINT_RESULTS(); | ||
| return (_tests_pass == _tests_run) ? 0 : 1; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UART HAL and context pointers are stored in global variables (
s_uart_halands_uart_ctx) but point to local stack variables within this test. To prevent dangling pointers that could cause issues if more tests are added or reordered, these globals should be cleared before the function returns.