@velox0/cerver 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +6 -4
- package/Makefile +26 -0
- package/bin/cerver.js +11 -0
- package/lib/commands/dev.js +23 -9
- package/lib/commands/new.js +273 -79
- package/lib/ir/transform.js +14 -20
- package/package.json +2 -2
- package/runtime/cerver.h +139 -129
- package/runtime/http_parser.c +153 -152
- package/runtime/http_writer.c +136 -126
- package/runtime/mime.c +49 -49
- package/runtime/router.c +99 -98
- package/runtime/server.c +594 -436
- package/runtime/static.c +174 -183
- package/runtime/tests/minunit.c +76 -0
- package/runtime/tests/minunit.h +64 -0
- package/runtime/tests/runtime_tests.c +414 -0
package/runtime/static.c
CHANGED
|
@@ -25,30 +25,30 @@
|
|
|
25
25
|
/* FNV-1a hash for fast asset lookup */
|
|
26
26
|
/* ------------------------------------------------------------------ */
|
|
27
27
|
|
|
28
|
-
static uint32_t fnv1a(const char
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
static uint32_t fnv1a(const char* str) {
|
|
29
|
+
uint32_t hash = 2166136261u;
|
|
30
|
+
while (*str) {
|
|
31
|
+
hash ^= (uint8_t)*str++;
|
|
32
|
+
hash *= 16777619u;
|
|
33
|
+
}
|
|
34
|
+
return hash;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/* ------------------------------------------------------------------ */
|
|
38
38
|
/* Path safety: prevent directory traversal */
|
|
39
39
|
/* ------------------------------------------------------------------ */
|
|
40
40
|
|
|
41
|
-
static int path_is_safe(const char
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
static int path_is_safe(const char* path) {
|
|
42
|
+
/* Reject paths with ".." */
|
|
43
|
+
if (strstr(path, "..")) return 0;
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/* Reject paths with null bytes */
|
|
46
|
+
if (memchr(path, '\0', strlen(path))) return 0;
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/* Must start with "/" */
|
|
49
|
+
if (path[0] != '/') return 0;
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
return 1;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/* ------------------------------------------------------------------ */
|
|
@@ -56,220 +56,211 @@ static int path_is_safe(const char *path) {
|
|
|
56
56
|
/* ------------------------------------------------------------------ */
|
|
57
57
|
|
|
58
58
|
typedef struct {
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
int accepts_gzip;
|
|
60
|
+
int accepts_br;
|
|
61
61
|
} encoding_prefs_t;
|
|
62
62
|
|
|
63
|
-
static encoding_prefs_t parse_accept_encoding(const cerver_request_t
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
static encoding_prefs_t parse_accept_encoding(const cerver_request_t* req) {
|
|
64
|
+
encoding_prefs_t prefs = {0, 0};
|
|
65
|
+
const char* ae = cerver_req_header(req, "Accept-Encoding");
|
|
66
|
+
if (!ae) return prefs;
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (strstr(ae, "br")) prefs.accepts_br = 1;
|
|
69
|
+
if (strstr(ae, "gzip")) prefs.accepts_gzip = 1;
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
return prefs;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/* ------------------------------------------------------------------ */
|
|
75
75
|
/* Cache header helper */
|
|
76
76
|
/* ------------------------------------------------------------------ */
|
|
77
77
|
|
|
78
|
-
static void add_cache_headers(cerver_response_t
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
78
|
+
static void add_cache_headers(cerver_response_t* res, const char* path) {
|
|
79
|
+
/* Hashed/versioned assets (in /static/) get long cache */
|
|
80
|
+
if (strstr(path, "/static/") || strstr(path, "/assets/")) {
|
|
81
|
+
cerver_res_header(res, "Cache-Control", "public, max-age=31536000, immutable");
|
|
82
|
+
} else {
|
|
83
|
+
/* HTML and other top-level files get short cache with revalidation */
|
|
84
|
+
cerver_res_header(res, "Cache-Control", "public, max-age=3600, must-revalidate");
|
|
85
|
+
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/* ------------------------------------------------------------------ */
|
|
89
89
|
/* Serve from embedded assets — hash-accelerated lookup */
|
|
90
90
|
/* ------------------------------------------------------------------ */
|
|
91
91
|
|
|
92
|
-
static int serve_embedded(cerver_server_t
|
|
93
|
-
|
|
94
|
-
if (!srv->assets || srv->asset_count == 0) return -1;
|
|
92
|
+
static int serve_embedded(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
93
|
+
if (!srv->assets || srv->asset_count == 0) return -1;
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
const char* path = req->path;
|
|
96
|
+
const cerver_asset_t* found = NULL;
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/* Try exact match first */
|
|
107
|
-
for (int i = 0; i < srv->asset_count; i++) {
|
|
108
|
-
if (fnv1a(srv->assets[i].path) == target_hash &&
|
|
109
|
-
strcmp(srv->assets[i].path, path) == 0) {
|
|
110
|
-
found = &srv->assets[i];
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
98
|
+
/*
|
|
99
|
+
* Use FNV-1a hash for O(1) average lookup instead of linear scan.
|
|
100
|
+
* For small asset counts (<64), linear scan is fine, but hash helps
|
|
101
|
+
* when there are hundreds of embedded assets.
|
|
102
|
+
*/
|
|
103
|
+
uint32_t target_hash = fnv1a(path);
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
121
|
-
} else {
|
|
122
|
-
snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
uint32_t idx_hash = fnv1a(index_path);
|
|
126
|
-
for (int i = 0; i < srv->asset_count; i++) {
|
|
127
|
-
if (fnv1a(srv->assets[i].path) == idx_hash &&
|
|
128
|
-
strcmp(srv->assets[i].path, index_path) == 0) {
|
|
129
|
-
found = &srv->assets[i];
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
105
|
+
/* Try exact match first */
|
|
106
|
+
for (int i = 0; i < srv->asset_count; i++) {
|
|
107
|
+
if (fnv1a(srv->assets[i].path) == target_hash && strcmp(srv->assets[i].path, path) == 0) {
|
|
108
|
+
found = &srv->assets[i];
|
|
109
|
+
break;
|
|
133
110
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
143
|
-
found->data_br, found->data_br_len);
|
|
144
|
-
cerver_res_header(res, "Content-Encoding", "br");
|
|
145
|
-
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
146
|
-
} else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
|
|
147
|
-
/* Serve gzip */
|
|
148
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
149
|
-
found->data_gz, found->data_gz_len);
|
|
150
|
-
cerver_res_header(res, "Content-Encoding", "gzip");
|
|
151
|
-
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Try with /index.html appended (for directory-like paths) */
|
|
114
|
+
if (!found) {
|
|
115
|
+
char index_path[CERVER_MAX_PATH];
|
|
116
|
+
size_t plen = strlen(path);
|
|
117
|
+
if (plen > 0 && path[plen - 1] == '/') {
|
|
118
|
+
snprintf(index_path, sizeof(index_path), "%sindex.html", path);
|
|
152
119
|
} else {
|
|
153
|
-
|
|
154
|
-
cerver_res_file(res, 200, found->mime_type,
|
|
155
|
-
found->data, found->data_len);
|
|
120
|
+
snprintf(index_path, sizeof(index_path), "%s/index.html", path);
|
|
156
121
|
}
|
|
157
122
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
123
|
+
uint32_t idx_hash = fnv1a(index_path);
|
|
124
|
+
for (int i = 0; i < srv->asset_count; i++) {
|
|
125
|
+
if (fnv1a(srv->assets[i].path) == idx_hash && strcmp(srv->assets[i].path, index_path) == 0) {
|
|
126
|
+
found = &srv->assets[i];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!found) return -1;
|
|
133
|
+
|
|
134
|
+
/* Check for pre-compressed variants */
|
|
135
|
+
encoding_prefs_t enc = parse_accept_encoding(req);
|
|
136
|
+
|
|
137
|
+
if (enc.accepts_br && found->data_br && found->data_br_len > 0) {
|
|
138
|
+
/* Serve brotli */
|
|
139
|
+
cerver_res_file(res, 200, found->mime_type, found->data_br, found->data_br_len);
|
|
140
|
+
cerver_res_header(res, "Content-Encoding", "br");
|
|
141
|
+
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
142
|
+
} else if (enc.accepts_gzip && found->data_gz && found->data_gz_len > 0) {
|
|
143
|
+
/* Serve gzip */
|
|
144
|
+
cerver_res_file(res, 200, found->mime_type, found->data_gz, found->data_gz_len);
|
|
145
|
+
cerver_res_header(res, "Content-Encoding", "gzip");
|
|
146
|
+
cerver_res_header(res, "Vary", "Accept-Encoding");
|
|
147
|
+
} else {
|
|
148
|
+
/* Serve uncompressed */
|
|
149
|
+
cerver_res_file(res, 200, found->mime_type, found->data, found->data_len);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
add_cache_headers(res, found->path);
|
|
153
|
+
|
|
154
|
+
return 0;
|
|
161
155
|
}
|
|
162
156
|
|
|
163
157
|
/* ------------------------------------------------------------------ */
|
|
164
158
|
/* Serve from filesystem — sendfile/mmap + stat cache */
|
|
165
159
|
/* ------------------------------------------------------------------ */
|
|
166
160
|
|
|
167
|
-
static int serve_filesystem(cerver_server_t
|
|
168
|
-
|
|
169
|
-
if (!srv->public_dir) return -1;
|
|
161
|
+
static int serve_filesystem(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
162
|
+
if (!srv->public_dir) return -1;
|
|
170
163
|
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
const char* path = req->path;
|
|
165
|
+
if (!path_is_safe(path)) return -1;
|
|
173
166
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
/* Build the full filesystem path */
|
|
168
|
+
char full_path[CERVER_MAX_PATH * 2];
|
|
169
|
+
snprintf(full_path, sizeof(full_path), "%s%s", srv->public_dir, path);
|
|
177
170
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
171
|
+
/* Check if it's a directory — try index.html */
|
|
172
|
+
struct stat st;
|
|
173
|
+
if (stat(full_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
|
174
|
+
snprintf(full_path, sizeof(full_path), "%s%s/index.html", srv->public_dir, path);
|
|
175
|
+
if (stat(full_path, &st) != 0) return -1;
|
|
176
|
+
}
|
|
185
177
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
178
|
+
/* Must be a regular file */
|
|
179
|
+
if (stat(full_path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
180
|
+
return -1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
size_t file_size = (size_t)st.st_size;
|
|
184
|
+
|
|
185
|
+
/* Store in stat cache for future lookups */
|
|
186
|
+
cerver_stat_cache_store(&srv->stat_cache, full_path, file_size, st.st_mtime);
|
|
187
|
+
|
|
188
|
+
/* Determine MIME type */
|
|
189
|
+
const char* mime = cerver_mime_from_path(full_path);
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* Use mmap for zero-copy serving instead of fopen+malloc+fread.
|
|
193
|
+
* The mmap'd region is used directly as the response body.
|
|
194
|
+
* We mark it as _body_owned=0 since munmap needs special handling,
|
|
195
|
+
* but for simplicity we'll use read() for small files and mmap for large.
|
|
196
|
+
*/
|
|
197
|
+
if (file_size > 65536) {
|
|
198
|
+
/* Large files: mmap for zero-copy */
|
|
199
|
+
int fd = open(full_path, O_RDONLY);
|
|
200
|
+
if (fd < 0) return -1;
|
|
201
|
+
|
|
202
|
+
void* mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
203
|
+
close(fd);
|
|
204
|
+
|
|
205
|
+
if (mapped == MAP_FAILED) return -1;
|
|
206
|
+
|
|
207
|
+
/* Advise the kernel we'll read sequentially */
|
|
208
|
+
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
209
|
+
|
|
210
|
+
res->status = 200;
|
|
211
|
+
res->content_type = mime;
|
|
212
|
+
res->body = (const char*)mapped;
|
|
213
|
+
res->body_len = file_size;
|
|
214
|
+
res->_body_owned = 2; /* Special flag: needs munmap, not free */
|
|
215
|
+
} else {
|
|
216
|
+
/* Small files: read into buffer (avoids mmap overhead) */
|
|
217
|
+
int fd = open(full_path, O_RDONLY);
|
|
218
|
+
if (fd < 0) return -1;
|
|
219
|
+
|
|
220
|
+
char* file_data = malloc(file_size);
|
|
221
|
+
if (!file_data) {
|
|
222
|
+
close(fd);
|
|
223
|
+
return -1;
|
|
189
224
|
}
|
|
190
225
|
|
|
191
|
-
size_t
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
/*
|
|
200
|
-
* Use mmap for zero-copy serving instead of fopen+malloc+fread.
|
|
201
|
-
* The mmap'd region is used directly as the response body.
|
|
202
|
-
* We mark it as _body_owned=0 since munmap needs special handling,
|
|
203
|
-
* but for simplicity we'll use read() for small files and mmap for large.
|
|
204
|
-
*/
|
|
205
|
-
if (file_size > 65536) {
|
|
206
|
-
/* Large files: mmap for zero-copy */
|
|
207
|
-
int fd = open(full_path, O_RDONLY);
|
|
208
|
-
if (fd < 0) return -1;
|
|
209
|
-
|
|
210
|
-
void *mapped = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
211
|
-
close(fd);
|
|
212
|
-
|
|
213
|
-
if (mapped == MAP_FAILED) return -1;
|
|
214
|
-
|
|
215
|
-
/* Advise the kernel we'll read sequentially */
|
|
216
|
-
madvise(mapped, file_size, MADV_SEQUENTIAL);
|
|
226
|
+
size_t total = 0;
|
|
227
|
+
while (total < file_size) {
|
|
228
|
+
ssize_t n = read(fd, file_data + total, file_size - total);
|
|
229
|
+
if (n <= 0) break;
|
|
230
|
+
total += (size_t)n;
|
|
231
|
+
}
|
|
232
|
+
close(fd);
|
|
217
233
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
res->body_len = file_size;
|
|
222
|
-
res->_body_owned = 2; /* Special flag: needs munmap, not free */
|
|
223
|
-
} else {
|
|
224
|
-
/* Small files: read into buffer (avoids mmap overhead) */
|
|
225
|
-
int fd = open(full_path, O_RDONLY);
|
|
226
|
-
if (fd < 0) return -1;
|
|
227
|
-
|
|
228
|
-
char *file_data = malloc(file_size);
|
|
229
|
-
if (!file_data) {
|
|
230
|
-
close(fd);
|
|
231
|
-
return -1;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
size_t total = 0;
|
|
235
|
-
while (total < file_size) {
|
|
236
|
-
ssize_t n = read(fd, file_data + total, file_size - total);
|
|
237
|
-
if (n <= 0) break;
|
|
238
|
-
total += (size_t)n;
|
|
239
|
-
}
|
|
240
|
-
close(fd);
|
|
241
|
-
|
|
242
|
-
if (total != file_size) {
|
|
243
|
-
free(file_data);
|
|
244
|
-
return -1;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
res->status = 200;
|
|
248
|
-
res->content_type = mime;
|
|
249
|
-
res->body = file_data;
|
|
250
|
-
res->body_len = file_size;
|
|
251
|
-
res->_body_owned = 1; /* malloc'd */
|
|
234
|
+
if (total != file_size) {
|
|
235
|
+
free(file_data);
|
|
236
|
+
return -1;
|
|
252
237
|
}
|
|
253
238
|
|
|
254
|
-
|
|
239
|
+
res->status = 200;
|
|
240
|
+
res->content_type = mime;
|
|
241
|
+
res->body = file_data;
|
|
242
|
+
res->body_len = file_size;
|
|
243
|
+
res->_body_owned = 1; /* malloc'd */
|
|
244
|
+
}
|
|
255
245
|
|
|
256
|
-
|
|
246
|
+
add_cache_headers(res, path);
|
|
247
|
+
|
|
248
|
+
return 0;
|
|
257
249
|
}
|
|
258
250
|
|
|
259
251
|
/* ------------------------------------------------------------------ */
|
|
260
252
|
/* Main static serving entry point */
|
|
261
253
|
/* ------------------------------------------------------------------ */
|
|
262
254
|
|
|
263
|
-
int cerver_serve_static(cerver_server_t
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (strcmp(req->method, "GET") != 0) return -1;
|
|
255
|
+
int cerver_serve_static(cerver_server_t* srv, cerver_request_t* req, cerver_response_t* res) {
|
|
256
|
+
/* Only serve GET requests for static files */
|
|
257
|
+
if (strcmp(req->method, "GET") != 0) return -1;
|
|
267
258
|
|
|
268
|
-
|
|
269
|
-
|
|
259
|
+
/* Try embedded assets first */
|
|
260
|
+
if (serve_embedded(srv, req, res) == 0) return 0;
|
|
270
261
|
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
/* Fall back to filesystem */
|
|
263
|
+
if (serve_filesystem(srv, req, res) == 0) return 0;
|
|
273
264
|
|
|
274
|
-
|
|
265
|
+
return -1;
|
|
275
266
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#include "minunit.h"
|
|
2
|
+
|
|
3
|
+
#include <stdio.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include <unistd.h>
|
|
6
|
+
|
|
7
|
+
int mu_tests_run = 0;
|
|
8
|
+
int mu_tests_failed = 0;
|
|
9
|
+
int mu_test_failed = 0;
|
|
10
|
+
|
|
11
|
+
static int mu_is_tty(FILE* f) { return isatty(fileno(f)); }
|
|
12
|
+
|
|
13
|
+
static const char* mu_color(FILE* f, const char* code) { return mu_is_tty(f) ? code : ""; }
|
|
14
|
+
|
|
15
|
+
const char* mu_snip(const char* s, char* buf, size_t cap) {
|
|
16
|
+
if (s == NULL) {
|
|
17
|
+
return "(null)";
|
|
18
|
+
}
|
|
19
|
+
if (cap == 0) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
size_t len = strlen(s);
|
|
24
|
+
if (len < cap) {
|
|
25
|
+
snprintf(buf, cap, "%s", s);
|
|
26
|
+
return buf;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (cap <= 4) {
|
|
30
|
+
size_t keep = cap - 1;
|
|
31
|
+
memcpy(buf, s, keep);
|
|
32
|
+
buf[keep] = '\0';
|
|
33
|
+
return buf;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
size_t keep = cap - 4;
|
|
37
|
+
memcpy(buf, s, keep);
|
|
38
|
+
memcpy(buf + keep, "...", 3);
|
|
39
|
+
buf[keep + 3] = '\0';
|
|
40
|
+
return buf;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void mu_fail(const char* file, int line, const char* msg) {
|
|
44
|
+
const char* red = mu_color(stderr, "\x1b[31m");
|
|
45
|
+
const char* reset = mu_color(stderr, "\x1b[0m");
|
|
46
|
+
|
|
47
|
+
mu_test_failed = 1;
|
|
48
|
+
fprintf(stderr, "%sFAIL%s %s:%d: %s\n", red, reset, file, line, msg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void mu_run(const char* name, mu_test_fn fn) {
|
|
52
|
+
mu_tests_run++;
|
|
53
|
+
mu_test_failed = 0;
|
|
54
|
+
fn();
|
|
55
|
+
if (mu_test_failed) {
|
|
56
|
+
mu_tests_failed++;
|
|
57
|
+
fprintf(stderr, " in %s\n", name);
|
|
58
|
+
} else {
|
|
59
|
+
const char* green = mu_color(stdout, "\x1b[32m");
|
|
60
|
+
const char* reset = mu_color(stdout, "\x1b[0m");
|
|
61
|
+
printf("%sok%s %s\n", green, reset, name);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
int mu_report(void) {
|
|
66
|
+
const char* reset = mu_color(stdout, "\x1b[0m");
|
|
67
|
+
if (mu_tests_failed) {
|
|
68
|
+
const char* red = mu_color(stdout, "\x1b[31m");
|
|
69
|
+
printf("%sSummary:%s %d tests, %s%d failures%s\n", red, reset, mu_tests_run, red,
|
|
70
|
+
mu_tests_failed, reset);
|
|
71
|
+
} else {
|
|
72
|
+
const char* green = mu_color(stdout, "\x1b[32m");
|
|
73
|
+
printf("%sSummary:%s %d tests, %s0 failures%s\n", green, reset, mu_tests_run, green, reset);
|
|
74
|
+
}
|
|
75
|
+
return mu_tests_failed ? 1 : 0;
|
|
76
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#ifndef CERVER_MINUNIT_H
|
|
2
|
+
#define CERVER_MINUNIT_H
|
|
3
|
+
|
|
4
|
+
#include <stddef.h>
|
|
5
|
+
#include <stdio.h>
|
|
6
|
+
#include <string.h>
|
|
7
|
+
|
|
8
|
+
typedef void (*mu_test_fn)(void);
|
|
9
|
+
|
|
10
|
+
#define MU_SNIP_MAX 80
|
|
11
|
+
|
|
12
|
+
extern int mu_tests_run;
|
|
13
|
+
extern int mu_tests_failed;
|
|
14
|
+
extern int mu_test_failed;
|
|
15
|
+
|
|
16
|
+
void mu_fail(const char* file, int line, const char* msg);
|
|
17
|
+
void mu_run(const char* name, mu_test_fn fn);
|
|
18
|
+
int mu_report(void);
|
|
19
|
+
const char* mu_snip(const char* s, char* buf, size_t cap);
|
|
20
|
+
|
|
21
|
+
#define MU_ASSERT(cond) \
|
|
22
|
+
do { \
|
|
23
|
+
if (!(cond)) { \
|
|
24
|
+
mu_fail(__FILE__, __LINE__, #cond); \
|
|
25
|
+
return; \
|
|
26
|
+
} \
|
|
27
|
+
} while (0)
|
|
28
|
+
|
|
29
|
+
#define MU_ASSERT_EQ_INT(a, b) \
|
|
30
|
+
do { \
|
|
31
|
+
if ((a) != (b)) { \
|
|
32
|
+
char _mu_buf[128]; \
|
|
33
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got %d expected %d", (int)(a), (int)(b)); \
|
|
34
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
35
|
+
return; \
|
|
36
|
+
} \
|
|
37
|
+
} while (0)
|
|
38
|
+
|
|
39
|
+
#define MU_ASSERT_EQ_SIZE(a, b) \
|
|
40
|
+
do { \
|
|
41
|
+
if ((a) != (b)) { \
|
|
42
|
+
char _mu_buf[128]; \
|
|
43
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got %zu expected %zu", (size_t)(a), (size_t)(b)); \
|
|
44
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
45
|
+
return; \
|
|
46
|
+
} \
|
|
47
|
+
} while (0)
|
|
48
|
+
|
|
49
|
+
#define MU_ASSERT_STREQ(a, b) \
|
|
50
|
+
do { \
|
|
51
|
+
if (((a) == NULL && (b) != NULL) || ((a) != NULL && (b) == NULL) || \
|
|
52
|
+
((a) != NULL && (b) != NULL && strcmp((a), (b)) != 0)) { \
|
|
53
|
+
char _mu_buf[256]; \
|
|
54
|
+
char _mu_a[MU_SNIP_MAX]; \
|
|
55
|
+
char _mu_b[MU_SNIP_MAX]; \
|
|
56
|
+
const char* _mu_as = mu_snip((a), _mu_a, sizeof(_mu_a)); \
|
|
57
|
+
const char* _mu_bs = mu_snip((b), _mu_b, sizeof(_mu_b)); \
|
|
58
|
+
snprintf(_mu_buf, sizeof(_mu_buf), "got '%s' expected '%s'", _mu_as, _mu_bs); \
|
|
59
|
+
mu_fail(__FILE__, __LINE__, _mu_buf); \
|
|
60
|
+
return; \
|
|
61
|
+
} \
|
|
62
|
+
} while (0)
|
|
63
|
+
|
|
64
|
+
#endif
|