#define _POSIX_C_SOURCE 200809L #include "movie_output.h" #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_PNG #include #endif static int failures = 0; static void check(int condition, const char *message) { if (!condition) { fprintf(stderr, "FAIL: %s\n", message); ++failures; } } /* Scratch data lives in the pre-approved OpenCode scratch directory rather * than directly under /tmp. */ #define TEST_TMP_ROOT "/tmp/opencode" #ifdef ENABLE_PNG #define TEST_IMAGE_EXT "png" #else #define TEST_IMAGE_EXT "ppm" #endif #define MAX_JOBS 64 #define JOB_WIDTH 16 #define JOB_HEIGHT 16 #define JOB_BYTES ((size_t)JOB_WIDTH * JOB_HEIGHT * 3) typedef struct { int fail_at; int delay_ms; size_t count; size_t order[MAX_JOBS]; unsigned char pixels[MAX_JOBS][JOB_BYTES]; int mesh_draw_flags[MAX_JOBS]; size_t mesh_pixel_counts[MAX_JOBS]; unsigned char layers[MAX_JOBS][JOB_WIDTH * JOB_HEIGHT * 4]; } MockWriter; static int mock_write(void *context, const MovieOutputJob *job, const PngWriteSettings *settings) { (void)settings; MockWriter *mock = context; if (mock->count < MAX_JOBS) { mock->order[mock->count] = job->frame_id; memcpy(mock->pixels[mock->count], job->clean_rgb8, JOB_BYTES); mock->mesh_draw_flags[mock->count] = job->draw_mesh; mock->mesh_pixel_counts[mock->count] = job->mesh_layer.rgba == NULL ? 0 : (size_t)job->mesh_layer.width * job->mesh_layer.height; if (job->mesh_layer.rgba != NULL) memcpy(mock->layers[mock->count], job->mesh_layer.rgba, sizeof mock->layers[mock->count]); } ++mock->count; if (mock->delay_ms > 0) { const struct timespec delay = {.tv_sec = mock->delay_ms / 1000, .tv_nsec = (long)(mock->delay_ms % 1000) * 1000000L}; nanosleep(&delay, NULL); } return mock->fail_at >= 0 && (int)job->frame_id == mock->fail_at ? -1 : 0; } static unsigned char *make_rgb8(size_t frame_id) { unsigned char *rgb8 = malloc(JOB_BYTES); if (rgb8 == NULL) return NULL; for (size_t i = 0; i < JOB_BYTES; ++i) rgb8[i] = (unsigned char)((frame_id * 31 + i) & 0xff); return rgb8; } static MovieOutputJob make_job(size_t frame_id) { MovieOutputJob job; memset(&job, 0, sizeof job); job.frame_id = frame_id; job.width = JOB_WIDTH; job.height = JOB_HEIGHT; job.images = frame_id + 1; job.catalog_stars = 7; job.clean_rgb8 = make_rgb8(frame_id); return job; } /* An independent premultiplied RGBA8 fixture with sparse nonzero pixels. */ static int make_layer(MeshOverlayLayer *layer, size_t count) { *layer = (MeshOverlayLayer){0}; layer->rgba = calloc(JOB_WIDTH * JOB_HEIGHT, 4); if (layer->rgba == NULL) return -1; layer->width = JOB_WIDTH; layer->height = JOB_HEIGHT; for (size_t i = 0; i < count; ++i) { for (int channel = 0; channel < 3; ++channel) layer->rgba[4 * i + channel] = (unsigned char)((i * 19 + channel * 3) % 129); layer->rgba[4 * i + 3] = 128; } return 0; } /* Order and pixel fidelity for capacity 1 and 2. */ static void test_order_and_pixels(size_t capacity) { MovieOutputQueue queue; MockWriter mock; memset(&mock, 0, sizeof mock); mock.fail_at = -1; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, capacity, &settings)) { check(0, "order: queue init"); return; } movie_output_queue_set_writer(&queue, mock_write, &mock); const size_t frames = 5; int all_ok = 1; for (size_t f = 0; f < frames; ++f) { MovieOutputJob job = make_job(f); if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) { all_ok = 0; free(job.clean_rgb8); break; } } const int finish = movie_output_queue_finish(&queue); check(all_ok && finish == 0, "order: submit/finish"); check(mock.count == frames, "order: writer saw every job"); for (size_t f = 0; f < frames && f < MAX_JOBS; ++f) { check(mock.order[f] == f, "order: frames written in submit order"); unsigned char *expected = make_rgb8(f); if (expected != NULL) { check(memcmp(mock.pixels[f], expected, JOB_BYTES) == 0, "order: pixel payload preserved"); free(expected); } } movie_output_queue_destroy(&queue); } /* A slow writer must push back on the producer at capacity 1. */ static void test_backpressure(void) { MovieOutputQueue queue; MockWriter mock; memset(&mock, 0, sizeof mock); mock.fail_at = -1; mock.delay_ms = 40; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "backpressure: queue init"); return; } movie_output_queue_set_writer(&queue, mock_write, &mock); double total_wait = 0.0; int all_ok = 1; for (size_t f = 0; f < 5; ++f) { MovieOutputJob job = make_job(f); double wait = 0.0; if (movie_output_queue_submit(&queue, &job, &wait)) { all_ok = 0; free(job.clean_rgb8); break; } total_wait += wait; } check(all_ok && movie_output_queue_finish(&queue) == 0, "backpressure: submit/finish"); check(total_wait > 0.05, "backpressure: producer waited on a full queue"); movie_output_queue_destroy(&queue); } /* Writer failure at frame N must propagate, unblock the producer, and join * cleanly. RGBA layers are attached to every job so both ownership paths * are exercised: the queue releases accepted jobs, while the caller releases * the job rejected after the failure is recorded. */ static void test_writer_failure(void) { MovieOutputQueue queue; MockWriter mock; memset(&mock, 0, sizeof mock); mock.fail_at = 3; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, 2, &settings)) { check(0, "failure: queue init"); return; } movie_output_queue_set_writer(&queue, mock_write, &mock); int saw_failure = 0; for (size_t f = 0; f < 8; ++f) { MovieOutputJob job = make_job(f); job.draw_mesh = 1; if (make_layer(&job.mesh_layer, 3)) { free(job.clean_rgb8); check(0, "failure: layer allocation"); break; } if (movie_output_queue_submit(&queue, &job, NULL)) { /* The queue no longer owns these resources. */ free(job.clean_rgb8); mesh_overlay_layer_destroy(&job.mesh_layer); saw_failure = 1; break; } } const int finish = movie_output_queue_finish(&queue); check(saw_failure, "failure: a later submit reported the writer error"); check(finish != 0, "failure: finish reports the recorded error"); check(movie_output_queue_failed(&queue), "failure: failed flag persists"); movie_output_queue_destroy(&queue); } /* Empty finish and never-submitted init must not hang or leak. */ static void test_empty_paths(void) { const PngWriteSettings settings = {-1}; MovieOutputQueue queue; if (movie_output_queue_init(&queue, 2, &settings)) { check(0, "empty: queue init"); return; } check(movie_output_queue_finish(&queue) == 0, "empty: finish with no jobs"); movie_output_queue_destroy(&queue); if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "empty: second init"); return; } movie_output_queue_destroy(&queue); } /* The queued job owns an independent RGBA layer: rasterizing a heap mesh, then * releasing that source mesh before the writer runs, must not disturb the * pixels the writer observes. */ static void test_lines_survive_source_release(void) { LensVertex *vertices = calloc(3, sizeof *vertices); LensTriangle *triangles = calloc(1, sizeof *triangles); if (vertices == NULL || triangles == NULL) { free(vertices); free(triangles); check(0, "source release: allocation"); return; } vertices[0] = (LensVertex){.image_x = 1.5, .image_y = 1.5, .outcome = RAY_OUTCOME_ESCAPED, .traced = 1}; vertices[1] = (LensVertex){.image_x = JOB_WIDTH - 2.5, .image_y = 2.5, .outcome = RAY_OUTCOME_DARK, .traced = 1}; vertices[2] = (LensVertex){.image_x = 5.5, .image_y = JOB_HEIGHT - 2.5, .outcome = RAY_OUTCOME_INCOMPLETE, .traced = 0}; triangles[0].vertex[0] = 0; triangles[0].vertex[1] = 1; triangles[0].vertex[2] = 2; FrameLensMesh mesh = {.vertices = vertices, .vertex_count = 3, .triangles = triangles, .triangle_count = 1}; MeshOverlayLayer layer = {0}; const MeshOverlaySettings overlay = mesh_overlay_default_settings(); const int prepared = mesh_overlay_build_layer(&mesh, JOB_WIDTH, JOB_HEIGHT, &overlay, &layer) == 0; /* Release the source mesh (and its vertex/triangle arrays) before submit. */ free(vertices); free(triangles); check(prepared, "source release: overlay extraction"); MovieOutputQueue queue; MockWriter mock; memset(&mock, 0, sizeof mock); mock.fail_at = -1; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "source release: queue init"); mesh_overlay_layer_destroy(&layer); return; } movie_output_queue_set_writer(&queue, mock_write, &mock); MovieOutputJob job = make_job(0); job.draw_mesh = 1; job.mesh_layer = layer; /* ownership transferred to the job/queue */ int submitted = job.clean_rgb8 != NULL && movie_output_queue_submit(&queue, &job, NULL) == 0; if (!submitted) { free(job.clean_rgb8); mesh_overlay_layer_destroy(&job.mesh_layer); } check(submitted && movie_output_queue_finish(&queue) == 0, "source release: submit/finish"); check(mock.count == 1 && mock.mesh_draw_flags[0] == 1 && mock.mesh_pixel_counts[0] == JOB_WIDTH * JOB_HEIGHT && memcmp(mock.layers[0], (unsigned char[JOB_WIDTH * JOB_HEIGHT * 4]){0}, sizeof mock.layers[0]) != 0, "source release: writer saw the intact RGBA overlay"); movie_output_queue_destroy(&queue); } #ifdef ENABLE_PNG static int decode_png_rgb8(const char *path, unsigned char *out, int width, int height) { FILE *file = fopen(path, "rb"); if (file == NULL) return -1; png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); png_infop info = png != NULL ? png_create_info_struct(png) : NULL; int ok = 0; if (png != NULL && info != NULL && !setjmp(png_jmpbuf(png))) { png_init_io(png, file); png_read_info(png, info); for (int row = 0; row < height; ++row) png_read_row(png, &out[(size_t)row * width * 3], NULL); png_read_end(png, info); ok = 1; } fclose(file); png_destroy_read_struct(&png, &info, NULL); return ok ? 0 : -1; } /* The default writer's clean file must decode unchanged and its mesh sibling * must equal the core overlay drawn in place on that same clean payload. */ static void test_default_writer_mesh(void) { char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX"; if (mkdtemp(directory) == NULL) { check(0, "default mesh: mkdtemp"); return; } char clean_path[PATH_MAX]; char mesh_path[PATH_MAX]; snprintf(clean_path, sizeof clean_path, "%s/frame_000000.png", directory); snprintf(mesh_path, sizeof mesh_path, "%s/frame_000000_mesh.png", directory); const PngWriteSettings settings = {-1}; MovieOutputQueue queue; if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "default mesh: queue init"); rmdir(directory); return; } MovieOutputJob job = make_job(0); job.draw_mesh = 1; snprintf(job.output_path, sizeof job.output_path, "%s", clean_path); snprintf(job.mesh_path, sizeof job.mesh_path, "%s", mesh_path); int layer_ok = make_layer(&job.mesh_layer, 5) == 0; unsigned char *clean_expected = make_rgb8(0); unsigned char mesh_expected[JOB_BYTES]; int expected_ok = layer_ok && clean_expected != NULL; if (expected_ok) { memcpy(mesh_expected, clean_expected, JOB_BYTES); expected_ok = mesh_overlay_composite_rgb8(&job.mesh_layer, mesh_expected, JOB_WIDTH, JOB_HEIGHT) == 0; } int submitted = expected_ok && job.clean_rgb8 != NULL && movie_output_queue_submit(&queue, &job, NULL) == 0; if (!submitted) { free(job.clean_rgb8); mesh_overlay_layer_destroy(&job.mesh_layer); } check(submitted && movie_output_queue_finish(&queue) == 0, "default mesh: submit/finish"); unsigned char clean_decoded[JOB_BYTES]; unsigned char mesh_decoded[JOB_BYTES]; const int decoded_ok = decode_png_rgb8(clean_path, clean_decoded, JOB_WIDTH, JOB_HEIGHT) == 0 && decode_png_rgb8(mesh_path, mesh_decoded, JOB_WIDTH, JOB_HEIGHT) == 0; check(decoded_ok && clean_expected != NULL && memcmp(clean_decoded, clean_expected, JOB_BYTES) == 0, "default mesh: clean image unchanged"); check(decoded_ok && expected_ok && memcmp(mesh_decoded, mesh_expected, JOB_BYTES) == 0, "default mesh: mesh matches core overlay result"); free(clean_expected); unlink(clean_path); unlink(mesh_path); rmdir(directory); movie_output_queue_destroy(&queue); } #endif /* Failure while writing the clean image happens before any overlay work; the * queue must still release the job's resources. */ static void test_default_writer_failure_before_overlay(void) { MovieOutputQueue queue; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "failure before overlay: queue init"); return; } MovieOutputJob job = make_job(0); job.draw_mesh = 1; if (make_layer(&job.mesh_layer, 4)) { free(job.clean_rgb8); check(0, "failure before overlay: layer allocation"); movie_output_queue_destroy(&queue); return; } snprintf(job.output_path, sizeof job.output_path, "/nonexistent-directory-xyz/frame." TEST_IMAGE_EXT); snprintf(job.mesh_path, sizeof job.mesh_path, "/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT); if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) { free(job.clean_rgb8); mesh_overlay_layer_destroy(&job.mesh_layer); check(0, "failure before overlay: submit"); movie_output_queue_destroy(&queue); return; } check(movie_output_queue_finish(&queue) != 0, "failure before overlay: clean write error propagates"); movie_output_queue_destroy(&queue); } /* Failure while writing the mesh image happens after the clean file is written * and the overlay is drawn; that later error must also propagate. */ static void test_default_writer_failure_after_overlay(void) { char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX"; if (mkdtemp(directory) == NULL) { check(0, "failure after overlay: mkdtemp"); return; } char clean_path[PATH_MAX]; snprintf(clean_path, sizeof clean_path, "%s/frame_000000." TEST_IMAGE_EXT, directory); MovieOutputQueue queue; const PngWriteSettings settings = {-1}; if (movie_output_queue_init(&queue, 1, &settings)) { check(0, "failure after overlay: queue init"); rmdir(directory); return; } MovieOutputJob job = make_job(0); job.draw_mesh = 1; if (make_layer(&job.mesh_layer, 4)) { free(job.clean_rgb8); check(0, "failure after overlay: layer allocation"); rmdir(directory); movie_output_queue_destroy(&queue); return; } snprintf(job.output_path, sizeof job.output_path, "%s", clean_path); snprintf(job.mesh_path, sizeof job.mesh_path, "/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT); if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) { free(job.clean_rgb8); mesh_overlay_layer_destroy(&job.mesh_layer); check(0, "failure after overlay: submit"); rmdir(directory); movie_output_queue_destroy(&queue); return; } check(movie_output_queue_finish(&queue) != 0, "failure after overlay: mesh write error propagates"); unlink(clean_path); rmdir(directory); movie_output_queue_destroy(&queue); } int main(void) { /* Standalone/CI runs need not have an OpenCode-created scratch directory. */ if (mkdir(TEST_TMP_ROOT, 0700) != 0 && errno != EEXIST) { perror("create movie-output test scratch directory"); return 1; } test_order_and_pixels(1); test_order_and_pixels(2); test_backpressure(); test_writer_failure(); test_empty_paths(); test_lines_survive_source_release(); #ifdef ENABLE_PNG test_default_writer_mesh(); #endif test_default_writer_failure_before_overlay(); test_default_writer_failure_after_overlay(); if (failures != 0) { fprintf(stderr, "%d movie-output failure(s)\n", failures); return 1; } puts("movie-output tests passed"); return 0; }