Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
source_manager_tests.h
Go to the documentation of this file.
1/* -*- C -*-
2 * Serene programming language
3 * Copyright (C) 2019-2026 Sameer Rahmani <lxsameer@lxsameer.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19#pragma once
20
21#include <fcntl.h>
22#include <limits.h>
23#include <string.h>
24#include <unistd.h>
25
26#include <serene/rt/context.h>
28
29#include "base.h"
30
31// elisp: (eglot-inlay-hints-mode -1)
32
33#define SM_TESTS(X) \
34 X("source_manager::init", test_sm_init), \
35 X("source_manager::buffer_new_file", test_sm_buffer_new_file), \
36 X("source_manager::buffer_terminated", test_sm_buffer_terminated), \
37 X("source_manager::buffer_same_file_no_reread", test_sm_buffer_same_file_no_reread), \
38 X("source_manager::buffer_empty_file", test_sm_buffer_empty_file), \
39 X("source_manager::buffer_nonexistent_file", test_sm_buffer_nonexistent_file), \
40 X("source_manager::buffer_reread", test_sm_buffer_reread), \
41 X("source_manager::get_buffer", test_sm_get_buffer), \
42 X("source_manager::id_exhaustion_releases_lock", test_sm_id_exhaustion_releases_lock), \
43 X("source_manager::reread_failure_keeps_buffer", test_sm_reread_failure_keeps_buffer), \
44 X("source_manager::filename_is_owned", test_sm_filename_is_owned), \
45 X("source_manager::shutdown", test_sm_shutdown)
46
47// Create a temp file containing `content` (may be empty). Writes the path into
48// `path_out` (must be PATH_MAX bytes). Returns 0 on success, -1 on failure.
49static int sm_make_temp_file(char *path_out, const char *content) {
50 (void)snprintf(path_out, PATH_MAX, "/tmp/srn_sm_test_XXXXXX");
51 int fd = mkstemp(path_out);
52
53 if (fd < 0) {
54 return -1;
55 }
56
57 size_t len = strlen(content);
58 if (len > 0 && write(fd, content, len) != (ssize_t)len) {
59 close(fd);
60 unlink(path_out);
61 return -1;
62 }
63
64 close(fd);
65 return 0;
66}
67
68// srn_sm_init creates a source manager with no buffers.
69static void test_sm_init() {
70 MAKE_ENGINE(mm, engine);
71 MAKE_CONTEXT(engine, ctx);
72
73 srn_sm_t *sm = srn_sm_init(ctx);
75 TEST_CHECK(sm->buffer_counts == 0);
76
78 TEST_CHECK(sm->buffer_counts == 0);
79
80 RELEASE_CONTEXT(ctx);
81 SHUTDOWN_ENGINE(mm, engine);
82}
83
84TEST_IN_FIBER(sm_buffer_new_file, {
85 char path[PATH_MAX];
86 TEST_ASSERT(sm_make_temp_file(path, "hello") == 0);
87
88 srn_sm_t *sm = srn_sm_init(ctx);
89 auto result = srn_sm_buffer_file(sm, path, false);
90
92 ASSERT_NOT_NULL(result.buffer);
93
94 TEST_CHECK(sm->buffer_counts == 1);
95 TEST_CHECK(srn_sm_buffer_len(result.buffer) == 6); // including the null terminator
96 TEST_CHECK(memcmp(srn_sm_buffer_data(result.buffer), "hello", 5) == 0);
97
99 unlink(path);
100})
101
102// The byte after the file contents is a NUL, which is what the reader stops
103// on. The content is longer than the 16 bytes glibc scribbles on a freed
104// chunk, so the poisoned chunk primed below shows a missing terminator
105// instead of a stray zero from the allocator's own bookkeeping.
106// This bug is spotted by an LLM.
107TEST_IN_FIBER(sm_buffer_terminated, {
108 static const char content[] = "0123456789abcdef0123456789abcdef";
109 const size_t len = sizeof(content) - 1;
110
111 char path[PATH_MAX];
113
115
116 // Prime the allocator with poisoned chunks of the size the source manager
117 // is about to request, otherwise a fresh page hides a missing terminator.
118 unsigned char *poison[8];
119 for (size_t i = 0; i < 8; i++) {
120 poison[i] = srn_mm_malloc(ctx->engine->mm, len + 1);
121 memset(poison[i], 0xFF, len + 1);
122 }
123 for (size_t i = 0; i < 8; i++) {
124 srn_mm_free(ctx->engine->mm, poison[i]);
125 }
126
128
132 TEST_CHECK(memcmp(srn_sm_buffer_data(result.buffer), content, len) == 0);
134
137})
138
139// Buffering the same path twice without reread returns the same buffer pointer
140// and does not allocate a second buffer slot.
141TEST_IN_FIBER(sm_buffer_same_file_no_reread, {
142 char path[PATH_MAX];
143 TEST_ASSERT(sm_make_temp_file(path, "data") == 0);
144
145 srn_sm_t *sm = srn_sm_init(ctx);
146
147 auto first = srn_sm_buffer_file(sm, path, false);
148 auto second = srn_sm_buffer_file(sm, path, false);
149
152
153 TEST_CHECK(first.buffer == second.buffer);
154 TEST_CHECK(sm->buffer_counts == 1);
155
157 unlink(path);
158})
159
160// Buffering an empty file succeeds with `buffer->map == nullptr` and buffer_counts is still
161// incremented.
162TEST_IN_FIBER(sm_buffer_empty_file, {
163 char path[PATH_MAX];
165
166 srn_sm_t *sm = srn_sm_init(ctx);
167 auto result = srn_sm_buffer_file(sm, path, false);
168
171 TEST_CHECK(sm->buffer_counts == 1);
172
174 unlink(path);
175})
176
177// Buffering a path that does not exist returns a NOT_FOUND error and leaves buffer_counts
178// unchanged.
179TEST_IN_FIBER(sm_buffer_nonexistent_file, {
180 srn_sm_t *sm = srn_sm_init(ctx);
181 auto result = srn_sm_buffer_file(sm, "/tmp/srn_does_not_exist_xyz123abc", false);
182
183 ASSERT_NOT_NULL(result.maybe_error);
184 TEST_CHECK(result.maybe_error->tag == NOT_FOUND);
185 TEST_CHECK(sm->buffer_counts == 0);
186
188})
189
190// Buffering with reread == true replaces the head of the chain while keeping the old version
191// accessible via next.
192TEST_IN_FIBER(sm_buffer_reread, {
193 char path[PATH_MAX];
195
196 srn_sm_t *sm = srn_sm_init(ctx);
197
201
202 int fd = open(path, O_WRONLY | O_TRUNC);
204 TEST_ASSERT(write(fd, "version2", 8) == 8);
206
208
211 TEST_CHECK(sm->buffer_counts == 1);
212
213 // Head reflects the new content.
215 TEST_CHECK(memcmp(srn_sm_buffer_data(second.buffer), "version2", 8) == 0);
216
218
219 // Old version is still reachable via next.
220 ASSERT_NOT_NULL(second.buffer->next);
222 TEST_CHECK(memcmp(srn_sm_buffer_data(second.buffer->next), "version1", 8) == 0);
223
225 unlink(path);
226})
227
228// srn_sm_get_buffer returns the buffer for a valid id, with correct data and
229// filename. Uses srn_sm_read_buffer_file to obtain the id.
230TEST_IN_FIBER(sm_get_buffer, {
231 char path[PATH_MAX];
232 TEST_ASSERT(sm_make_temp_file(path, "test content") == 0);
233
234 srn_sm_t *sm = srn_sm_init(ctx);
235 auto result = srn_sm_read_buffer_file(sm, path, false);
237
239 ASSERT_NOT_NULL(buf);
240 TEST_CHECK(srn_sm_buffer_len(buf) == 13);
241 TEST_CHECK(memcmp(srn_sm_buffer_data(buf), "test content", 12) == 0);
242 TEST_CHECK(strcmp(buf->filename, path) == 0);
243
245 unlink(path);
246})
247
248// A reread that cannot read the file reports the error and leaves the
249// buffer as it was, instead of installing an empty head over the good one.
250TEST_IN_FIBER(sm_reread_failure_keeps_buffer, {
251 char path[PATH_MAX];
252 TEST_ASSERT(sm_make_temp_file(path, "version1") == 0);
253
254 srn_sm_t *sm = srn_sm_init(ctx);
255 auto first = srn_sm_buffer_file(sm, path, false);
257
258 unlink(path);
261 TEST_CHECK(failed.maybe_error->tag == NOT_FOUND);
262
265 TEST_CHECK(again.buffer == first.buffer);
267 TEST_CHECK(memcmp(srn_sm_buffer_data(again.buffer), "version1", 8) == 0);
268
270})
271
272// The buffer keeps its own copy of the path, since buffers outlive the
273// caller's string.
274TEST_IN_FIBER(sm_filename_is_owned, {
275 char path[PATH_MAX];
276 TEST_ASSERT(sm_make_temp_file(path, "data") == 0);
277
278 char scratch[PATH_MAX];
279 (void)snprintf(scratch, sizeof(scratch), "%s", path);
280
281 srn_sm_t *sm = srn_sm_init(ctx);
282 auto result = srn_sm_buffer_file(sm, scratch, false);
284
285 memset(scratch, 'x', strlen(scratch));
286 TEST_CHECK(strcmp(result.buffer->filename, path) == 0);
287
289 unlink(path);
290})
291
292// A load refused for lack of buffer ids must leave the write lock free,
293// otherwise the next new file load parks forever on it. The fiber sets `done`
294// as its last step and the check runs after the scheduler returns, so a
295// fiber that never comes back is a failure rather than a test with no checks.
296static srn_fiber_result_t sm_id_exhaustion_fiber(srn_context_t *ctx, void *arg) {
297 bool *done = arg;
298 char path1[PATH_MAX];
299 char path2[PATH_MAX];
302
303 srn_sm_t *sm = srn_sm_init(ctx);
304
305 sm->buffer_counts = UINT16_MAX;
309
310 sm->buffer_counts = 0;
313 TEST_CHECK(sm->buffer_counts == 1);
314
318 *done = true;
319 return nullptr;
320}
321
323 MAKE_ENGINE(mm, engine);
324 MAKE_CONTEXT(engine, ctx);
325
326 bool done = false;
327 srn_fiber_spawn(ctx, sm_id_exhaustion_fiber, &done);
328 srn_sched_run(engine->scheduler, 1);
330
331 RELEASE_CONTEXT(ctx);
332 SHUTDOWN_ENGINE(mm, engine);
333}
334
335// srn_sm_shutdown releases all buffer mappings and resets buffer_counts to 0.
336TEST_IN_FIBER(sm_shutdown, {
337 char path1[PATH_MAX];
338 char path2[PATH_MAX];
339 TEST_ASSERT(sm_make_temp_file(path1, "file one content") == 0);
340 TEST_ASSERT(sm_make_temp_file(path2, "file two content") == 0);
341
342 srn_sm_t *sm = srn_sm_init(ctx);
345 TEST_CHECK(sm->buffer_counts == 2);
346
348 TEST_CHECK(sm->buffer_counts == 0);
349
350 unlink(path1);
351 unlink(path2);
352})
#define TEST_CHECK(cond)
Definition acutest.h:95
#define TEST_ASSERT(cond)
Definition acutest.h:117
#define RELEASE_CONTEXT(x)
Definition base.h:48
#define ASSERT_NOT_NULL(x)
Definition base.h:30
#define SHUTDOWN_ENGINE(mm, engine)
Definition base.h:40
#define TEST_IN_FIBER(name, body)
Definition base.h:67
#define TEST_NO_ERROR(x)
Definition base.h:32
#define MAKE_ENGINE(mm, engine)
Definition base.h:34
#define ASSERT_NULL(x)
Definition base.h:31
#define MAKE_CONTEXT(engine, x)
Definition base.h:44
void srn_mm_free(srn_mm_t *mm, void *ptr)
Release a pointer previously returned by srn_mm_malloc or srn_mm_reallocate.
Definition default.c:168
void * srn_mm_malloc(srn_mm_t *mm, size_t size)
Generic allocations that do not participate in the block based pools.
Definition default.c:155
@ SM_BUFFER_ID_EXHAUSTED
Definition errors.h:97
@ NOT_FOUND
Definition errors.h:133
srn_fiber_t * srn_fiber_spawn(srn_context_t *ctx, srn_fiber_entry_t entry, void *arg)
Make and schedule a fiber with every default, the engine's scheduler, the configured stack size,...
Definition fiber.c:250
void * srn_fiber_result_t
What a fiber's entry produces, type-erased.
Definition fiber.h:171
void srn_sched_run(srn_scheduler_t *sched, size_t nworkers)
Run the scheduler with nworkers os threads draining it, returning once the pool goes quiescent (every...
Definition scheduler.c:875
srn_sm_maybe_buffer_t srn_sm_buffer_file(srn_sm_t *sm, const char *filepath, bool reread)
Read and buffer the given filepath into the source manager and return a srn_sm_maybe_buffer_t which e...
srn_sm_t * srn_sm_init(srn_context_t *ctx)
Initialize the source manager, as an immortal data structure.
srn_sm_buffer_t * srn_sm_get_buffer(srn_sm_t *sm, srn_sm_buffer_id_t id)
Return the buffer pointer of the given id, nullptr if the id does not exists.
srn_sm_maybe_id_t srn_sm_read_buffer_file(srn_sm_t *sm, const char *filepath, bool reread)
Read and buffer the given filepath into the source manager and return the buffer id.
Source manager is what that read the source files and own the buffer that contains the data.
static size_t srn_sm_buffer_len(srn_sm_buffer_t *buf)
static const unsigned char * srn_sm_buffer_data(srn_sm_buffer_t *buf)
const size_t len
auto accepted
auto second
srn_sm_t * sm
auto refused
char path2[PATH_MAX]
static int sm_make_temp_file(char *path_out, const char *content)
unlink(path)
char path1[PATH_MAX]
srn_sm_shutdown(sm)
auto failed
static void test_sm_init()
void * arg
auto first
auto again
unsigned char * poison[8]
close(fd)
char path[PATH_MAX]
auto result
static void test_sm_id_exhaustion_releases_lock()
const char * filename