1 /*
2 * Copyright 2021 The ChromiumOS Authors
3 * Use of this source code is governed by a BSD-style license that can be
4 * found in the LICENSE file.
5 */
6
7 #define _GNU_SOURCE
8 #include <assert.h>
9 #include <errno.h>
10 #include <ftw.h>
11 #include <stdbool.h>
12 #include <stdio.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <unistd.h>
16
17 #include <rutabaga_gfx/rutabaga_gfx_ffi.h>
18
19 #include "virtgpu_cross_domain_protocol.h"
20
21 #define CHECK_RESULT(result) \
22 do { \
23 if (result) { \
24 printf("CHECK_RESULT failed in %s() %s:%d\n", __func__, __FILE__, __LINE__); \
25 return result; \
26 } \
27 } while (0)
28
29 #define CHECK(cond) \
30 do { \
31 if (!(cond)) { \
32 printf("CHECK failed in %s() %s:%d\n", __func__, __FILE__, __LINE__); \
33 return -EINVAL; \
34 } \
35 } while (0)
36
37 #define DEFAULT_BUFFER_SIZE 4096
38 #define WIDTH 512
39 #define HEIGHT 512
40 #define NUM_ITERATIONS 4
41
42 #define GBM_BO_USE_LINEAR (1 << 4)
43 #define GBM_BO_USE_SCANOUT (1 << 5)
44 #define fourcc_code(a, b, c, d) \
45 ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
46 #define DRM_FORMAT_XRGB8888 fourcc_code('X', 'R', '2', '4');
47
48 #define PIPE_TEXTURE_2D 2
49 #define PIPE_BIND_RENDER_TARGET 2
50 #define VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM 1
51
52 static int s_resource_id = 1;
53 static int s_fence_id = 1;
54
55 #if defined(__linux__)
56 static char *s_wayland_path = "/run/user/1000/wayland-0";
57 #elif defined(__Fuchsia__)
58 #endif
59
60 struct rutabaga_test {
61 struct rutabaga *rutabaga;
62 uint32_t ctx_id;
63 uint64_t value;
64 uint32_t query_ring_id;
65 uint32_t channel_ring_id;
66 struct iovec *query_iovecs;
67 struct iovec *channel_iovecs;
68 };
69
rutabaga_test_write_fence(uint64_t user_data,const struct rutabaga_fence * fence)70 static void rutabaga_test_write_fence(uint64_t user_data, const struct rutabaga_fence *fence)
71 {
72 struct rutabaga_test *test = (void *)(uintptr_t)user_data;
73 test->value = fence->fence_id;
74 }
75
rutabaga_test_debug_cb(uint64_t user_data,const struct rutabaga_debug * debug)76 static void rutabaga_test_debug_cb(uint64_t user_data, const struct rutabaga_debug *debug)
77 {
78 if (debug->message) {
79 printf("The debug message is %s\n", debug->message);
80 }
81 }
82
test_capset_mask_calculation(void)83 static int test_capset_mask_calculation(void)
84 {
85 int result;
86 uint64_t capset_mask;
87
88 result = rutabaga_calculate_capset_mask("cross-domain:gfxstream-vulkan", &capset_mask);
89 CHECK_RESULT(result);
90 CHECK(capset_mask ==
91 ((1 << RUTABAGA_CAPSET_CROSS_DOMAIN) | (1 << RUTABAGA_CAPSET_GFXSTREAM_VULKAN)));
92
93 result = rutabaga_calculate_capset_mask(":gfxstream-vulkan", &capset_mask);
94 CHECK_RESULT(result);
95 CHECK(capset_mask == (1 << RUTABAGA_CAPSET_GFXSTREAM_VULKAN));
96
97 result = rutabaga_calculate_capset_mask("cross-domain:", &capset_mask);
98 CHECK_RESULT(result);
99 CHECK(capset_mask == (1 << RUTABAGA_CAPSET_CROSS_DOMAIN));
100
101 result = rutabaga_calculate_capset_mask("cross-domain", &capset_mask);
102 CHECK_RESULT(result);
103 CHECK(capset_mask == (1 << RUTABAGA_CAPSET_CROSS_DOMAIN));
104
105 result = rutabaga_calculate_capset_mask(":", &capset_mask);
106 CHECK_RESULT(result);
107 CHECK(capset_mask == 0);
108
109 result = rutabaga_calculate_capset_mask("fake", &capset_mask);
110 CHECK_RESULT(result);
111 CHECK(capset_mask == 0);
112
113 result = rutabaga_calculate_capset_mask("", &capset_mask);
114 CHECK_RESULT(result);
115 CHECK(capset_mask == 0);
116
117 result = rutabaga_calculate_capset_mask(NULL, &capset_mask);
118 CHECK(result != 0);
119
120 return 0;
121 }
122
test_rutabaga_init(struct rutabaga_test * test,uint64_t capset_mask)123 static int test_rutabaga_init(struct rutabaga_test *test, uint64_t capset_mask)
124 {
125 int result;
126 struct rutabaga_builder builder = { 0 };
127 struct rutabaga_channels channels = { 0 };
128
129 builder.fence_cb = rutabaga_test_write_fence;
130 builder.debug_cb = rutabaga_test_debug_cb;
131 builder.capset_mask = capset_mask;
132 builder.wsi = RUTABAGA_WSI_SURFACELESS;
133 if (capset_mask & (1 << RUTABAGA_CAPSET_CROSS_DOMAIN)) {
134 builder.user_data = (uint64_t)(uintptr_t *)(void *)test;
135 channels.channels = (struct rutabaga_channel *)calloc(1, sizeof(struct rutabaga_channel));
136 channels.num_channels = 1;
137
138 channels.channels[0].channel_name = s_wayland_path;
139 channels.channels[0].channel_type = RUTABAGA_CHANNEL_TYPE_WAYLAND;
140
141 builder.channels = &channels;
142 }
143
144 result = rutabaga_init(&builder, &test->rutabaga);
145
146 if (capset_mask & (1 << RUTABAGA_CAPSET_CROSS_DOMAIN))
147 free(channels.channels);
148
149 CHECK_RESULT(result);
150 return 0;
151 }
152
test_create_context(struct rutabaga_test * test,const char * context_name)153 static int test_create_context(struct rutabaga_test *test, const char *context_name)
154 {
155 int result;
156 uint32_t num_capsets;
157 uint32_t capset_id, capset_version, capset_size;
158 bool found_cross_domain = false;
159 struct CrossDomainCapabilities *capset;
160
161 result = rutabaga_get_num_capsets(test->rutabaga, &num_capsets);
162 CHECK_RESULT(result);
163 CHECK(num_capsets == 1);
164
165 for (uint32_t i = 0; i < num_capsets; i++) {
166 result =
167 rutabaga_get_capset_info(test->rutabaga, i, &capset_id, &capset_version, &capset_size);
168 CHECK_RESULT(result);
169 if (capset_id == RUTABAGA_CAPSET_CROSS_DOMAIN) {
170 found_cross_domain = true;
171 CHECK(capset_size == (uint32_t)sizeof(struct CrossDomainCapabilities));
172 }
173 }
174
175 CHECK(found_cross_domain);
176 CHECK_RESULT(result);
177
178 capset = (struct CrossDomainCapabilities *)calloc(1, capset_size);
179 result = rutabaga_get_capset(test->rutabaga, RUTABAGA_CAPSET_CROSS_DOMAIN, 0, (uint8_t *)capset,
180 capset_size);
181 CHECK_RESULT(result);
182
183 CHECK(capset->version == 1);
184 free(capset);
185
186 size_t context_name_len = 0;
187 if (context_name)
188 context_name_len = strlen(context_name);
189
190 result = rutabaga_context_create(test->rutabaga, test->ctx_id, RUTABAGA_CAPSET_CROSS_DOMAIN,
191 context_name, context_name_len);
192 CHECK_RESULT(result);
193
194 return 0;
195 }
196
test_init_context(struct rutabaga_test * test)197 static int test_init_context(struct rutabaga_test *test)
198 {
199 int result;
200 struct rutabaga_create_blob rc_blob = { 0 };
201 struct rutabaga_iovecs vecs = { 0 };
202 struct rutabaga_command cmd = { 0 };
203 struct CrossDomainInit cmd_init = { { 0 } };
204
205 struct iovec *query_iovecs = (struct iovec *)calloc(1, sizeof(struct iovec));
206 query_iovecs[0].iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
207 query_iovecs[0].iov_len = DEFAULT_BUFFER_SIZE;
208
209 test->query_iovecs = query_iovecs;
210 rc_blob.blob_mem = RUTABAGA_BLOB_MEM_GUEST;
211 rc_blob.blob_flags = RUTABAGA_BLOB_FLAG_USE_MAPPABLE;
212 rc_blob.size = DEFAULT_BUFFER_SIZE;
213
214 vecs.iovecs = query_iovecs;
215 vecs.num_iovecs = 1;
216
217 result = rutabaga_resource_create_blob(test->rutabaga, 0, test->query_ring_id, &rc_blob, &vecs,
218 NULL);
219 CHECK_RESULT(result);
220
221 result = rutabaga_context_attach_resource(test->rutabaga, test->ctx_id, test->query_ring_id);
222 CHECK_RESULT(result);
223
224 struct iovec *channel_iovecs = (struct iovec *)calloc(1, sizeof(struct iovec));
225 channel_iovecs[0].iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
226 channel_iovecs[0].iov_len = DEFAULT_BUFFER_SIZE;
227
228 test->channel_iovecs = channel_iovecs;
229 rc_blob.blob_mem = RUTABAGA_BLOB_MEM_GUEST;
230 rc_blob.blob_flags = RUTABAGA_BLOB_FLAG_USE_MAPPABLE;
231 rc_blob.size = DEFAULT_BUFFER_SIZE;
232
233 vecs.iovecs = channel_iovecs;
234 vecs.num_iovecs = 1;
235
236 result = rutabaga_resource_create_blob(test->rutabaga, 0, test->channel_ring_id, &rc_blob,
237 &vecs, NULL);
238 CHECK_RESULT(result);
239
240 result = rutabaga_context_attach_resource(test->rutabaga, test->ctx_id, test->channel_ring_id);
241 CHECK_RESULT(result);
242
243 cmd_init.hdr.cmd = CROSS_DOMAIN_CMD_INIT;
244 cmd_init.hdr.cmd_size = sizeof(struct CrossDomainInit);
245 cmd_init.query_ring_id = test->query_ring_id;
246 cmd_init.channel_ring_id = test->channel_ring_id;
247 cmd_init.channel_type = CROSS_DOMAIN_CHANNEL_TYPE_WAYLAND;
248
249 cmd.ctx_id = test->ctx_id;
250 cmd.cmd = (uint8_t *)&cmd_init;
251 cmd.cmd_size = cmd_init.hdr.cmd_size;
252
253 result = rutabaga_submit_command(test->rutabaga, &cmd);
254 CHECK_RESULT(result);
255 return 0;
256 }
257
test_command_submission(struct rutabaga_test * test)258 static int test_command_submission(struct rutabaga_test *test)
259 {
260 int result;
261 struct CrossDomainGetImageRequirements cmd_get_reqs = { 0 };
262 struct CrossDomainImageRequirements *image_reqs = (void *)test->query_iovecs[0].iov_base;
263 struct rutabaga_create_blob rc_blob = { 0 };
264 struct rutabaga_fence fence;
265 struct rutabaga_handle handle = { 0 };
266 struct rutabaga_command cmd = { 0 };
267 uint32_t map_info;
268
269 fence.flags = RUTABAGA_FLAG_FENCE | RUTABAGA_FLAG_INFO_RING_IDX;
270 fence.ctx_id = test->ctx_id;
271 fence.ring_idx = 0;
272
273 cmd_get_reqs.hdr.cmd = CROSS_DOMAIN_CMD_GET_IMAGE_REQUIREMENTS;
274 cmd_get_reqs.hdr.cmd_size = sizeof(struct CrossDomainGetImageRequirements);
275
276 for (uint32_t i = 0; i < NUM_ITERATIONS; i++) {
277 for (uint32_t j = 0; j < NUM_ITERATIONS; j++) {
278 fence.fence_id = s_fence_id;
279 map_info = 0;
280
281 cmd_get_reqs.width = WIDTH * i;
282 cmd_get_reqs.height = HEIGHT * j;
283 cmd_get_reqs.drm_format = DRM_FORMAT_XRGB8888;
284
285 cmd_get_reqs.flags = GBM_BO_USE_LINEAR | GBM_BO_USE_SCANOUT;
286
287 cmd.ctx_id = test->ctx_id;
288 cmd.cmd = (uint8_t *)&cmd_get_reqs;
289 cmd.cmd_size = cmd_get_reqs.hdr.cmd_size;
290
291 result = rutabaga_submit_command(test->rutabaga, &cmd);
292
293 CHECK(test->value < fence.fence_id);
294 result = rutabaga_create_fence(test->rutabaga, &fence);
295
296 CHECK_RESULT(result);
297 for (;;) {
298 if (fence.fence_id == test->value)
299 break;
300 }
301
302 CHECK(image_reqs->strides[0] >= cmd_get_reqs.width * 4);
303 CHECK(image_reqs->size >= (cmd_get_reqs.width * 4) * cmd_get_reqs.height);
304
305 rc_blob.blob_mem = RUTABAGA_BLOB_MEM_HOST3D;
306 rc_blob.blob_flags = RUTABAGA_BLOB_FLAG_USE_MAPPABLE | RUTABAGA_BLOB_FLAG_USE_SHAREABLE;
307 rc_blob.blob_id = image_reqs->blob_id;
308 rc_blob.size = image_reqs->size;
309
310 result = rutabaga_resource_create_blob(test->rutabaga, test->ctx_id, s_resource_id,
311 &rc_blob, NULL, NULL);
312 CHECK_RESULT(result);
313
314 result = rutabaga_context_attach_resource(test->rutabaga, test->ctx_id, s_resource_id);
315 CHECK_RESULT(result);
316
317 result = rutabaga_resource_map_info(test->rutabaga, s_resource_id, &map_info);
318 CHECK_RESULT(result);
319 CHECK(map_info > 0);
320
321 result = rutabaga_resource_export_blob(test->rutabaga, s_resource_id, &handle);
322 CHECK_RESULT(result);
323 CHECK(handle.os_handle >= 0);
324
325 result = close(handle.os_handle);
326 CHECK_RESULT(result);
327
328 result = rutabaga_context_detach_resource(test->rutabaga, test->ctx_id, s_resource_id);
329 CHECK_RESULT(result);
330
331 result = rutabaga_resource_unref(test->rutabaga, s_resource_id);
332 CHECK_RESULT(result);
333
334 s_resource_id++;
335 s_fence_id++;
336 }
337 }
338
339 return 0;
340 }
341
test_context_finish(struct rutabaga_test * test)342 static int test_context_finish(struct rutabaga_test *test)
343 {
344 int result;
345
346 result = rutabaga_context_detach_resource(test->rutabaga, test->ctx_id, test->query_ring_id);
347 CHECK_RESULT(result);
348
349 result = rutabaga_resource_unref(test->rutabaga, test->query_ring_id);
350 CHECK_RESULT(result);
351
352 free(test->query_iovecs[0].iov_base);
353
354 result = rutabaga_context_detach_resource(test->rutabaga, test->ctx_id, test->channel_ring_id);
355 CHECK_RESULT(result);
356
357 result = rutabaga_resource_unref(test->rutabaga, test->channel_ring_id);
358 CHECK_RESULT(result);
359
360 free(test->channel_iovecs[0].iov_base);
361
362 result = rutabaga_context_destroy(test->rutabaga, test->ctx_id);
363 CHECK_RESULT(result);
364
365 return 0;
366 }
367
test_rutabaga_2d(struct rutabaga_test * test)368 static int test_rutabaga_2d(struct rutabaga_test *test)
369 {
370 struct rutabaga_create_3d rc_3d = { 0 };
371 struct rutabaga_transfer transfer = { 0 };
372 int result;
373 uint32_t resource_id = s_resource_id++;
374
375 struct rutabaga_iovecs vecs = { 0 };
376 struct iovec *iovecs = (struct iovec *)calloc(1, sizeof(struct iovec));
377 uint8_t *test_data;
378 struct iovec result_iovec;
379
380 iovecs[0].iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
381 iovecs[0].iov_len = DEFAULT_BUFFER_SIZE;
382 result_iovec.iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
383 result_iovec.iov_len = DEFAULT_BUFFER_SIZE;
384 test_data = (uint8_t *)result_iovec.iov_base;
385
386 vecs.iovecs = iovecs;
387 vecs.num_iovecs = 1;
388
389 rc_3d.target = PIPE_TEXTURE_2D;
390 rc_3d.bind = PIPE_BIND_RENDER_TARGET;
391 rc_3d.format = VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM;
392 rc_3d.width = DEFAULT_BUFFER_SIZE / 16;
393 rc_3d.height = 4;
394
395 transfer.w = DEFAULT_BUFFER_SIZE / 16;
396 transfer.h = 4;
397 transfer.d = 1;
398
399 result = rutabaga_resource_create_3d(test->rutabaga, resource_id, &rc_3d);
400 CHECK_RESULT(result);
401
402 result = rutabaga_resource_attach_backing(test->rutabaga, resource_id, &vecs);
403 CHECK_RESULT(result);
404
405 memset(iovecs[0].iov_base, 8, DEFAULT_BUFFER_SIZE);
406
407 result =
408 rutabaga_resource_transfer_read(test->rutabaga, 0, resource_id, &transfer, &result_iovec);
409 CHECK_RESULT(result);
410
411 CHECK(test_data[0] == 0);
412
413 result = rutabaga_resource_transfer_write(test->rutabaga, 0, resource_id, &transfer);
414 CHECK_RESULT(result);
415
416 result =
417 rutabaga_resource_transfer_read(test->rutabaga, 0, resource_id, &transfer, &result_iovec);
418 CHECK_RESULT(result);
419
420 CHECK(test_data[0] == 8);
421
422 result = rutabaga_resource_detach_backing(test->rutabaga, resource_id);
423 CHECK_RESULT(result);
424
425 result = rutabaga_resource_unref(test->rutabaga, resource_id);
426 CHECK_RESULT(result);
427
428 free(iovecs[0].iov_base);
429 free(iovecs);
430 free(test_data);
431 return 0;
432 }
433
434 struct cb_data {
435 uint8_t buf[2000];
436 size_t len;
437 };
438
snapshot_cb(uint64_t user_data,const uint8_t * data,size_t len)439 static void snapshot_cb(uint64_t user_data, const uint8_t* data, size_t len) {
440 struct cb_data* cb_data = (struct cb_data*)user_data;
441 if (cb_data->len + len > 2000) {
442 // Silently truncate. Checked for by the test.
443 len = 2000 - cb_data->len;
444 }
445 memcpy(cb_data->buf + cb_data->len, data, len);
446 cb_data->len += len;
447 }
448
test_rutabaga_finish(struct rutabaga_test * test)449 static int test_rutabaga_finish(struct rutabaga_test *test)
450 {
451 int result;
452
453 result = rutabaga_finish(&test->rutabaga);
454 CHECK_RESULT(result);
455 CHECK(test->rutabaga == NULL);
456 return 0;
457 }
458
test_rutabaga_2d_snapshot(struct rutabaga_test * test,const char * dir)459 static int test_rutabaga_2d_snapshot(struct rutabaga_test *test, const char* dir)
460 {
461 struct rutabaga_create_3d rc_3d = { 0 };
462 struct rutabaga_transfer transfer = { 0 };
463 int result;
464 uint32_t resource_id = s_resource_id++;
465
466 result = test_rutabaga_init(test, 0);
467 CHECK_RESULT(result);
468
469 struct rutabaga_iovecs vecs = { 0 };
470 struct iovec *iovecs = (struct iovec *)calloc(1, sizeof(struct iovec));
471 uint8_t *test_data;
472 struct iovec result_iovec;
473
474 iovecs[0].iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
475 iovecs[0].iov_len = DEFAULT_BUFFER_SIZE;
476 result_iovec.iov_base = calloc(1, DEFAULT_BUFFER_SIZE);
477 result_iovec.iov_len = DEFAULT_BUFFER_SIZE;
478 test_data = (uint8_t *)result_iovec.iov_base;
479
480 vecs.iovecs = iovecs;
481 vecs.num_iovecs = 1;
482
483 rc_3d.target = PIPE_TEXTURE_2D;
484 rc_3d.bind = PIPE_BIND_RENDER_TARGET;
485 rc_3d.format = VIRTIO_GPU_FORMAT_B8G8R8A8_UNORM;
486 rc_3d.width = DEFAULT_BUFFER_SIZE / 16;
487 rc_3d.height = 4;
488
489 transfer.w = DEFAULT_BUFFER_SIZE / 16;
490 transfer.h = 4;
491 transfer.d = 1;
492
493 result = rutabaga_resource_create_3d(test->rutabaga, resource_id, &rc_3d);
494 CHECK_RESULT(result);
495
496 result = rutabaga_resource_attach_backing(test->rutabaga, resource_id, &vecs);
497 CHECK_RESULT(result);
498
499 struct cb_data cb_data;
500 memset(&cb_data, 0, sizeof(struct cb_data));
501
502 result = rutabaga_snapshot(test->rutabaga, dir);
503 CHECK_RESULT(result);
504 // If the buffer is filled, assume it overflow. If that happens, just make
505 // the test's buffer bigger (or make the snapshot smaller).
506 CHECK(cb_data.len < 2000);
507
508 result = rutabaga_resource_unref(test->rutabaga, resource_id);
509 CHECK_RESULT(result);
510
511 free(iovecs[0].iov_base);
512 free(iovecs);
513 free(test_data);
514
515 // Teardown and re-init. Restore is only supported from a fresh init.
516 result = test_rutabaga_finish(test);
517 CHECK_RESULT(result);
518
519 result = test_rutabaga_init(test, 0);
520 CHECK_RESULT(result);
521
522 result = rutabaga_restore(test->rutabaga, dir);
523 CHECK_RESULT(result);
524
525 result = test_rutabaga_finish(test);
526 CHECK_RESULT(result);
527
528 return 0;
529 }
530
ftw_cb(const char * fpath,const struct stat *,int,struct FTW *)531 int ftw_cb(const char *fpath, const struct stat *, int, struct FTW *) {
532 return remove(fpath);
533 }
534
recursive_rm(const char * dir)535 int recursive_rm(const char* dir) {
536 CHECK(nftw(dir, ftw_cb, 64, FTW_DEPTH | FTW_PHYS) == 0);
537 }
538
main(int argc,char * argv[])539 int main(int argc, char *argv[])
540 {
541 struct rutabaga_test test = { 0 };
542 test.ctx_id = 1;
543 test.query_ring_id = s_resource_id++;
544 test.channel_ring_id = s_resource_id++;
545
546 int result;
547
548 const char *context_names[] = {
549 NULL,
550 "test_context",
551 };
552 const uint32_t num_context_names = 2;
553
554 for (uint32_t i = 0; i < num_context_names; i++) {
555 continue;
556 const char *context_name = context_names[i];
557 for (uint32_t j = 0; j < NUM_ITERATIONS; j++) {
558 result = test_capset_mask_calculation();
559 CHECK_RESULT(result);
560
561 result = test_rutabaga_init(&test, 1 << RUTABAGA_CAPSET_CROSS_DOMAIN);
562 CHECK_RESULT(result);
563
564 result |= test_create_context(&test, context_name);
565 CHECK_RESULT(result);
566
567 result |= test_init_context(&test);
568 CHECK_RESULT(result);
569
570 result |= test_command_submission(&test);
571 CHECK_RESULT(result);
572
573 result |= test_context_finish(&test);
574 CHECK_RESULT(result);
575
576 result |= test_rutabaga_finish(&test);
577 CHECK_RESULT(result);
578 }
579 }
580
581 for (uint32_t i = 0; i < NUM_ITERATIONS; i++) {
582 result = test_rutabaga_init(&test, 0);
583 CHECK_RESULT(result);
584
585 result |= test_rutabaga_2d(&test);
586 CHECK_RESULT(result);
587
588 result |= test_rutabaga_finish(&test);
589 CHECK_RESULT(result);
590 }
591
592 for (uint32_t i = 0; i < NUM_ITERATIONS; i++) {
593 char template[] = "/tmp/rutabaga_test_snapshot.XXXXXX";
594 const char *dir = mkdtemp(template);
595 CHECK(dir);
596 result = test_rutabaga_2d_snapshot(&test, dir);
597 CHECK(recursive_rm(dir) == 0);
598 CHECK_RESULT(result);
599 break;
600 }
601
602 printf("[ PASSED ] rutabaga_test success\n");
603 return 0;
604 }
605