#define STRVIEW_IMPLEMENTATION #include "strview.h" #include "compiler.h" #include "parser.h" #include #include #include static int failures; static int tests_run; static bool contains(const char* text, const char* part) { return sv_contains(sv_from_cstr(text), sv_from_cstr(part)); } static bool stage_contains(const StagedCommand* stage, const char* part) { for (size_t i = 0; i < stage->argc; i++) if (contains(stage->argv[i], part)) return true; return false; } #define CHECK(condition) do { \ if (!(condition)) { \ fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #condition); \ failures++; \ } \ } while (0) #define TEST(name) static void name(void) #define RUN(name) do { tests_run++; name(); } while (0) typedef struct { int calls; double video_duration; double audio_duration; ProbeStream streams[PROBE_STREAM_LIMIT]; size_t stream_count; ProbeChapter chapters[PROBE_CHAPTER_LIMIT]; size_t chapter_count; ProbeAttachment attachments[PROBE_ATTACHMENT_LIMIT]; size_t attachment_count; ProbeTag tags[PROBE_TAG_LIMIT]; size_t tag_count; } FakeProbe; static bool fake_probe(const char* path, ProbeResult* result, void* context, Error* error) { (void)error; FakeProbe* fake = context; fake->calls++; bool video_only = sv_ends_with(sv_from_cstr(path), svlit(".mov")); bool audio_only = sv_ends_with(sv_from_cstr(path), svlit(".wav")); *result = (ProbeResult){ .has_video = !audio_only, .has_audio = !video_only, .video_duration_known = !audio_only, .audio_duration_known = !video_only, .video_duration = fake->video_duration, .audio_duration = fake->audio_duration, }; if (fake->stream_count != 0) { *result = (ProbeResult){0}; result->stream_count = fake->stream_count; memcpy(result->streams, fake->streams, fake->stream_count * sizeof(*fake->streams)); for (size_t i = 0; i < fake->stream_count; i++) { ProbeStream stream = fake->streams[i]; if (stream.type == TYPE_VIDEO) { result->has_video = true; result->video_duration_known = stream.duration_known; result->video_duration = fmax(result->video_duration, stream.duration); } else if (stream.type == TYPE_AUDIO) { result->has_audio = true; result->audio_duration_known = stream.duration_known; result->audio_duration = fmax(result->audio_duration, stream.duration); } else if (stream.type == TYPE_SUBTITLES) { result->has_subtitles = true; result->subtitle_duration_known = stream.duration_known; result->subtitle_duration = fmax(result->subtitle_duration, stream.duration); } } } result->chapter_count = fake->chapter_count; memcpy(result->chapters, fake->chapters, fake->chapter_count * sizeof(*fake->chapters)); result->attachment_count = fake->attachment_count; memcpy(result->attachments, fake->attachments, fake->attachment_count * sizeof(*fake->attachments)); result->tag_count = fake->tag_count; memcpy(result->tags, fake->tags, fake->tag_count * sizeof(*fake->tags)); return true; } static bool parse_and_check(const char* text, Program* program, TypedProgram* typed, Error* error) { return parse_program(text, program, error) && typecheck_program(program, typed, error); } static bool compile_text(const char* text, FakeProbe* fake, Program* program, TypedProgram* typed, ProbeCache* cache, CompiledCommand* command, Error* error) { if (!parse_and_check(text, program, typed, error)) { return false; } probe_cache_init(cache, fake_probe, fake); return compile_program(program, typed, cache, "out.mp4", command, error); } static bool compile_text_to(const char* text, const char* output, FakeProbe* fake, Program* program, TypedProgram* typed, ProbeCache* cache, CompiledCommand* command, Error* error) { if (!parse_and_check(text, program, typed, error)) return false; probe_cache_init(cache, fake_probe, fake); return compile_program(program, typed, cache, output, command, error); } static void destroy_compilation(Program* program, TypedProgram* typed, ProbeCache* cache, CompiledCommand* command) { compiled_command_destroy(command); probe_cache_destroy(cache); typed_program_destroy(typed); program_destroy(program); } TEST(parser_comments_and_paths) { Program program; Error error; CHECK(parse_program("# heading\nvideo \"close up #1.mov\" # camera\n", &program, &error)); CHECK(program.count == 1); CHECK(sv_equal(sv_from_cstr(program.items[0].as.source.path), svlit("close up #1.mov"))); CHECK(program.items[0].line == 2); program_destroy(&program); } TEST(positional_script_arguments) { const char* arguments[] = {"close up", "dialogue track.wav"}; Program program; Error error; CHECK(parse_program_with_args("video \"clips/$1.mov\"\naudio \"$2\"\nmux\n", 2, arguments, &program, &error)); CHECK(sv_equal(sv_from_cstr(program.items[0].as.source.path), svlit("clips/close up.mov"))); CHECK(sv_equal(sv_from_cstr(program.items[1].as.source.path), sv_from_cstr(arguments[1]))); program_destroy(&program); CHECK(!parse_program_with_args("video \"$3\"\n", 2, arguments, &program, &error)); CHECK(error.line == 1 && contains(error.message, "missing script argument")); CHECK(!parse_program_with_args("video \"$0\"\n", 2, arguments, &program, &error)); CHECK(contains(error.message, "invalid")); CHECK(!parse_program("video \"$1\"\n", &program, &error)); CHECK(contains(error.message, "missing script argument")); CHECK(!parse_program_with_args("video $1\n", 2, arguments, &program, &error)); CHECK(contains(error.message, "must be quoted")); } TEST(arena_alignment_and_reset) { Arena arena = arena_create(); arena.block_size = 32; void* first = arena_alloc_aligned(&arena, 7, 64); void* second = arena_alloc_aligned(&arena, 80, 32); CHECK(first != NULL && (uintptr_t)first % 64 == 0); CHECK(second != NULL && (uintptr_t)second % 32 == 0); arena_reset(&arena); void* reused = arena_alloc_aligned(&arena, 7, 64); CHECK(reused == first); arena_destroy(&arena); } TEST(duration_parsing) { Program program; Error error; CHECK(parse_program("duration 5s\nduration 250ms\nduration 01:23.500\n", &program, &error)); CHECK(fabs(program.items[0].as.number - 5.0) < 1e-9); CHECK(fabs(program.items[1].as.number - 0.25) < 1e-9); CHECK(fabs(program.items[2].as.number - 83.5) < 1e-9); program_destroy(&program); } TEST(scalar_parsing) { Program program; Error error; CHECK(parse_program("scalar 0.5\nscalar 2\n", &program, &error)); CHECK(program.items[0].as.number == 0.5); CHECK(program.items[1].as.number == 2.0); program_destroy(&program); } TEST(parser_errors) { Program program; Error error; CHECK(!parse_program("wibble\n", &program, &error)); CHECK(error.line == 1 && contains(error.message, "unknown")); CHECK(!parse_program("video \"unfinished\n", &program, &error)); CHECK(contains(error.message, "unterminated")); CHECK(!parse_program("duration 12\n", &program, &error)); CHECK(contains(error.message, "malformed duration")); CHECK(!parse_program("scalar nope\n", &program, &error)); CHECK(contains(error.message, "malformed scalar")); CHECK(!parse_program("index 1.5\n", &program, &error)); CHECK(contains(error.message, "malformed index")); } TEST(type_errors) { Program program; TypedProgram typed; Error error; CHECK(parse_program("drop\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "underflow")); program_destroy(&program); CHECK(parse_program("audio \"a.wav\"\nscalar 2\ntake\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "expected DURATION")); program_destroy(&program); CHECK(parse_program("video \"a.mov\"\naudio \"a.wav\"\nconcat\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "same type")); program_destroy(&program); CHECK(parse_program("audio \"a.wav\"\nvideo \"a.mov\"\nmux\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "expected VIDEO")); program_destroy(&program); } TEST(valid_types) { Program program; TypedProgram typed; Error error; CHECK(parse_and_check("video \"a.mov\"\nvideo \"b.mov\"\nconcat\n", &program, &typed, &error)); CHECK(typed.final_type == TYPE_VIDEO); typed_program_destroy(&typed); program_destroy(&program); CHECK(parse_and_check("combined \"a.mp4\"\nsplit\nmux\n", &program, &typed, &error)); CHECK(typed.items[1].produced_count == 2); CHECK(typed.items[1].produced[0] == TYPE_VIDEO); CHECK(typed.items[1].produced[1] == TYPE_AUDIO); typed_program_destroy(&typed); program_destroy(&program); CHECK(parse_and_check("audio \"a.wav\"\nscalar 0.7\nvolume\n", &program, &typed, &error)); CHECK(typed.final_type == TYPE_AUDIO); typed_program_destroy(&typed); program_destroy(&program); } TEST(speed_validation) { Program program; TypedProgram typed; Error error; CHECK(parse_program("video \"a.mov\"\nscalar 0\nspeed\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "greater than zero")); program_destroy(&program); } TEST(probe_cache) { FakeProbe fake = {.video_duration = 10, .audio_duration = 11}; ProbeCache cache; Error error; ProbeResult first; ProbeResult second; probe_cache_init(&cache, fake_probe, &fake); CHECK(probe_cache_get(&cache, "same.mp4", &first, &error)); CHECK(probe_cache_get(&cache, "same.mp4", &second, &error)); CHECK(fake.calls == 1); CHECK(cache.invocation_count == 1); CHECK(first.video_duration == 10 && second.audio_duration == 11); probe_cache_destroy(&cache); } TEST(duration_and_fade_propagation) { const char* text = "video \"a.mov\"\nduration 2s\nduration 20s\nslice\n" "scalar 2\nspeed\nduration 1s\nfade-out\n"; FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "trim=start=2:duration=20")); CHECK(contains(command.filter_complex, "setpts=PTS/2")); CHECK(contains(command.filter_complex, "fade=t=out:st=3:d=1")); destroy_compilation(&program, &typed, &cache, &command); } TEST(length_and_silence) { FakeProbe fake = {.video_duration = 7.5, .audio_duration = 9}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("combined \"a.mp4\"\nlength\nsilence\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "atrim=duration=9")); CHECK(!contains(command.filter_complex, "0:v:0")); CHECK(!contains(command.filter_complex, "0:a:0")); destroy_compilation(&program, &typed, &cache, &command); } TEST(video_duplication) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("video \"a.mov\"\ndup\nconcat\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "split=2")); CHECK(contains(command.filter_complex, "concat=n=2:v=1:a=0")); destroy_compilation(&program, &typed, &cache, &command); } TEST(audio_duplication) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("audio \"a.wav\"\ndup\nmix\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "asplit=2")); CHECK(contains(command.filter_complex, "amix=inputs=2:duration=longest")); destroy_compilation(&program, &typed, &cache, &command); } TEST(no_unnecessary_split_and_mapping) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("combined \"a.mp4\"\nsplit\nmux\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(!contains(command.filter_complex, "split=")); CHECK(!contains(command.filter_complex, "asplit=")); CHECK(contains(command.shell_command, "'[vout]'")); CHECK(contains(command.shell_command, "'[aout]'")); destroy_compilation(&program, &typed, &cache, &command); } TEST(dropped_graph_is_unreachable) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* text = "video \"keep.mov\"\nvideo \"drop.mov\"\nduration 1s\ntake\ndrop\n"; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(!contains(command.filter_complex, "trim=duration=1")); CHECK(contains(command.filter_complex, "[0:0]null[vout]")); CHECK(!contains(command.shell_command, "drop.mov")); destroy_compilation(&program, &typed, &cache, &command); } TEST(subtitle_mapping) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* text = "combined \"a.mp4\"\nsubtitles \"captions.srt\"\nattach-subtitles\n"; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.shell_command, "2:s:0")); CHECK(contains(command.shell_command, "-c:s:0 mov_text")); destroy_compilation(&program, &typed, &cache, &command); } TEST(burn_subtitles_and_video_conversion) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("video \"a.mov\"\nsubtitles \"captions.srt\"\nburn-subtitles\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(command.stage_count == 1); CHECK(contains(command.filter_complex, "subtitles=filename=")); CHECK(!contains(command.filter_complex, "force_style=")); destroy_compilation(&program, &typed, &cache, &command); const char* styled = "video \"a.mov\"\nsubtitles \"captions.srt\"\n" "burn-style \"font=Inter; size=42; color=#FFFFFF; outline-color=#000000; " "outline-width=2; align=bottom-center; margin=48\"\nburn-subtitles\n"; CHECK(compile_text(styled, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "force_style='FontName=Inter,FontSize=42")); CHECK(contains(command.filter_complex, "PrimaryColour=&H00FFFFFF")); CHECK(contains(command.filter_complex, "Alignment=2,MarginV=48")); destroy_compilation(&program, &typed, &cache, &command); CHECK(!compile_text("video \"a.mov\"\nsubtitles \"a.srt\"\n" "burn-style \"size=-1\"\nburn-subtitles\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(error.message, "invalid burn style")); probe_cache_destroy(&cache); typed_program_destroy(&typed); program_destroy(&program); CHECK(parse_and_check("video \"a.mov\"\nvideo-to-combined\n", &program, &typed, &error)); CHECK(typed.final_type == TYPE_COMBINED); typed_program_destroy(&typed); program_destroy(&program); CHECK(parse_program("combined \"a.mp4\"\nsubtitles \"a.srt\"\nburn-subtitles\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "expected VIDEO")); program_destroy(&program); CHECK(!parse_program("combined \"a.mp4\"\nsubtitles \"a.srt\"\nattach\n", &program, &error)); CHECK(contains(error.message, "unknown instruction")); } TEST(bitmap_subtitle_burn_rejected) { FakeProbe fake = { .streams = {{.type = TYPE_VIDEO, .stream_index = 0, .duration_known = true, .duration = 10}, {.type = TYPE_SUBTITLES, .stream_index = 1, .duration_known = true, .duration = 10}}, .stream_count = 2, }; snprintf(fake.streams[1].codec_name, sizeof(fake.streams[1].codec_name), "hdmv_pgs_subtitle"); Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(!compile_text("video \"a.mkv\"\nsubtitles \"a.mkv\"\nburn-subtitles\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(error.message, "text subtitles only")); probe_cache_destroy(&cache); typed_program_destroy(&typed); program_destroy(&program); } TEST(multitrack_combined_mapping) { FakeProbe fake = { .streams = { {TYPE_VIDEO, 0, true, 10}, {TYPE_AUDIO, 1, true, 10}, {TYPE_AUDIO, 2, true, 10}, {TYPE_SUBTITLES, 3, true, 9}, {TYPE_SUBTITLES, 4, true, 8}, }, .stream_count = 5, }; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("combined \"multi.mkv\"\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "[0:0]null[vout]")); CHECK(contains(command.filter_complex, "[0:1]anull[aout]")); CHECK(contains(command.filter_complex, "[0:2]anull[aout1]")); CHECK(command.stage_count == 2); CHECK(stage_contains(&command.stages[0], "0:3")); CHECK(stage_contains(&command.stages[1], "0:4")); CHECK(contains(command.shell_command, "1:s:0")); CHECK(contains(command.shell_command, "2:s:0")); destroy_compilation(&program, &typed, &cache, &command); } TEST(indexed_combined_operations) { FakeProbe fake = { .streams = { {TYPE_VIDEO, 0, true, 10}, {TYPE_AUDIO, 1, true, 10}, {TYPE_AUDIO, 2, true, 10}, {TYPE_SUBTITLES, 3, true, 9}, {TYPE_SUBTITLES, 4, true, 8}, }, .stream_count = 5, }; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* text = "combined \"multi.mkv\"\nindex 1\ndrop-audio\n" "index 1\nindex 0\nmove-subtitles\n"; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "[0:1]anull[aout]")); CHECK(!contains(command.filter_complex, "[0:2]")); CHECK(command.stage_count == 2); CHECK(stage_contains(&command.stages[0], "0:4")); CHECK(stage_contains(&command.stages[1], "0:3")); destroy_compilation(&program, &typed, &cache, &command); CHECK(parse_and_check("combined \"a.mp4\"\nindex 0\ntake-subtitles\nadd-subtitles\n", &program, &typed, &error)); typed_program_destroy(&typed); program_destroy(&program); } TEST(subtitle_timeline_operations) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* text = "subtitles \"a.srt\"\nduration 2s\nduration 4s\nslice\n" "scalar 2\nspeed\n"; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(command.stage_count == 3); CHECK(stage_contains(&command.stages[1], "-ss")); CHECK(stage_contains(&command.stages[1], "-t")); CHECK(stage_contains(&command.stages[2], "-itsscale")); CHECK(contains(command.shell_command, "--internal-subtitle-clip")); destroy_compilation(&program, &typed, &cache, &command); CHECK(compile_text("subtitles \"a.srt\"\nsubtitles \"b.srt\"\nconcat\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(command.stage_count == 3); CHECK(stage_contains(&command.stages[2], "concat")); CHECK(contains(command.shell_command, "--internal-subtitle-concat-list")); destroy_compilation(&program, &typed, &cache, &command); } TEST(atempo_chain) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text("audio \"a.wav\"\nscalar 8\nspeed\n", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "atempo=2,atempo=2,atempo=2")); destroy_compilation(&program, &typed, &cache, &command); } TEST(arithmetic_comparison_and_logic) { Program program; TypedProgram typed; Error error; const char* valid = "video \"a.mov\"\nscalar 2\nscalar 3\nadd\nscalar 5\neq\ntrue\nand\n" "if\nduration 1s\ntake\nelse\nduration 2s\ntake\nend\n"; CHECK(parse_and_check(valid, &program, &typed, &error)); typed_program_destroy(&typed); program_destroy(&program); CHECK(parse_program("video \"a.mov\"\nduration 1s\nscalar 2\nadd\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "matching SCALAR or DURATION")); program_destroy(&program); CHECK(parse_program("video \"a.mov\"\nscalar 1\nscalar 0\ndiv\ndrop\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "division by zero")); program_destroy(&program); } TEST(conditional_compilation) { const char* text = "video \"a.mov\"\nscalar 3\nscalar 2\ngt\nif\n" "duration 1s\ntake\nelse\nduration 9s\ntake\nend\n"; FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "trim=duration=1")); CHECK(!contains(command.filter_complex, "trim=duration=9")); destroy_compilation(&program, &typed, &cache, &command); } TEST(conditional_type_errors) { Program program; TypedProgram typed; Error error; const char* mismatch = "video \"a.mov\"\ntrue\nif\nscalar 1\nelse\nduration 1s\nend\n"; CHECK(parse_program(mismatch, &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "identical stack types")); program_destroy(&program); const char* invalid_unselected = "video \"a.mov\"\ntrue\nif\n" "duration 1s\ntake\nelse\nscalar 1\ntake\nend\n"; CHECK(parse_program(invalid_unselected, &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "expected DURATION")); program_destroy(&program); } TEST(loop_unrolling) { const char* text = "video \"a.mov\"\nscalar 0\nbegin\ndup\nscalar 3\nlt\nwhile\n" "swap\nduration 1s\ntake\nswap\nscalar 1\nadd\nrepeat\ndrop\n"; FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); strview graph = sv_from_cstr(command.filter_complex); size_t take_count = 0; while (sv_contains(graph, svlit("trim=duration=1"))) { bool found = false; size_t offset = sv_find(graph, svlit("trim=duration=1"), &found); CHECK(found); sv_chop_left(&graph, offset + svlit("trim=duration=1").len); take_count++; } CHECK(take_count == 3); destroy_compilation(&program, &typed, &cache, &command); } TEST(nested_control_flow) { const char* text = "video \"a.mov\"\nscalar 0\nbegin\ndup\nscalar 2\nlt\nwhile\n" "dup\nscalar 1\neq\nif\nswap\nduration 1s\ntake\nswap\nelse\n" "end\nscalar 1\nadd\nrepeat\ndrop\n"; FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text(text, &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.filter_complex, "trim=duration=1")); destroy_compilation(&program, &typed, &cache, &command); } TEST(loop_type_errors) { Program program; TypedProgram typed; Error error; const char* bad_condition = "video \"a.mov\"\nbegin\nscalar 1\nwhile\nrepeat\n"; CHECK(parse_program(bad_condition, &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "S BOOL")); program_destroy(&program); const char* bad_body = "video \"a.mov\"\nbegin\ntrue\nwhile\nscalar 1\nrepeat\n"; CHECK(parse_program(bad_body, &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "preserve the loop stack")); program_destroy(&program); CHECK(parse_program("video \"a.mov\"\nbegin\ntrue\n", &program, &error)); CHECK(!typecheck_program(&program, &typed, &error)); CHECK(contains(error.message, "matching repeat")); program_destroy(&program); } TEST(loop_iteration_limit) { const char* text = "video \"a.mov\"\nbegin\ntrue\nwhile\nrepeat\n"; FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(parse_and_check(text, &program, &typed, &error)); probe_cache_init(&cache, fake_probe, &fake); CHECK(!compile_program(&program, &typed, &cache, "out.mp4", &command, &error)); CHECK(contains(error.message, "10000 iterations")); probe_cache_destroy(&cache); typed_program_destroy(&typed); program_destroy(&program); } TEST(metadata_chapters_and_attachments) { FakeProbe fake = { .streams = { {.type = TYPE_VIDEO, .stream_index = 0, .duration_known = true, .duration = 10}, {.type = TYPE_AUDIO, .stream_index = 1, .duration_known = true, .duration = 10, .language = "jpn", .title = "Japanese", .dispositions = DISPOSITION_DEFAULT}, }, .stream_count = 2, .chapters = {{0, 2, "Opening"}}, .chapter_count = 1, .tags = {{"title", "Episode 01"}}, .tag_count = 1, }; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* tracks = "combined \"episode.mkv\"\nindex 0\ntake-audio\n" "string \"eng\"\nset-language\nstring \"Dub\"\nset-title\n" "false\nset-default\nadd-audio\n"; CHECK(compile_text_to(tracks, "out.mkv", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.shell_command, "language=eng")); CHECK(contains(command.shell_command, "title=Dub")); CHECK(contains(command.shell_command, "-disposition:a:0 0")); CHECK(contains(command.shell_command, "--internal-write-file")); CHECK(contains(command.shell_command, "title=Opening")); CHECK(contains(command.shell_command, "title=Episode 01")); destroy_compilation(&program, &typed, &cache, &command); const char* side_data = "combined \"episode.mkv\"\n" "duration 3s\nduration 5s\nstring \"Part B\"\nchapter\nadd-chapter\n" "string \"title\"\nstring \"Retitled\"\nset-tag\n" "attachment \"font.ttf\"\nadd-attachment\n"; CHECK(compile_text_to(side_data, "out.mkv", &fake, &program, &typed, &cache, &command, &error)); CHECK(contains(command.shell_command, "title=Part B")); CHECK(contains(command.shell_command, "title=Retitled")); CHECK(contains(command.shell_command, "-attach font.ttf")); CHECK(contains(command.shell_command, "mimetype=application/x-truetype-font")); destroy_compilation(&program, &typed, &cache, &command); } TEST(subtitle_codec_preservation) { FakeProbe ass = { .streams = { {.type = TYPE_VIDEO, .stream_index = 0, .duration_known = true, .duration = 10}, {.type = TYPE_SUBTITLES, .stream_index = 1, .duration_known = true, .duration = 10, .codec_name = "ass", .subtitle_kind = SUBTITLE_ASS, .language = "eng", .dispositions = DISPOSITION_FORCED}, }, .stream_count = 2, }; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; CHECK(compile_text_to("combined \"episode.mkv\"\n", "out.mkv", &ass, &program, &typed, &cache, &command, &error)); CHECK(command.stage_count == 0); CHECK(contains(command.shell_command, "-map 0:1")); CHECK(contains(command.shell_command, "-c:s:0 copy")); CHECK(contains(command.shell_command, "-disposition:s:0 forced")); destroy_compilation(&program, &typed, &cache, &command); CHECK(compile_text_to("subtitles \"episode.mkv\"\nduration 2s\ntake\n", "out.mks", &ass, &program, &typed, &cache, &command, &error)); CHECK(command.stage_count == 2); CHECK(stage_contains(&command.stages[1], "ass")); CHECK(command.stages[1].clip_subtitles); CHECK(contains(command.shell_command, "-c:s:0 ass")); destroy_compilation(&program, &typed, &cache, &command); CHECK(!compile_text_to("combined \"episode.mkv\"\n", "out.mp4", &ass, &program, &typed, &cache, &command, &error)); CHECK(contains(error.message, "ASS/SSA")); probe_cache_destroy(&cache); typed_program_destroy(&typed); program_destroy(&program); FakeProbe bitmap = { .streams = {{.type = TYPE_SUBTITLES, .stream_index = 0, .duration_known = true, .duration = 10, .codec_name = "hdmv_pgs_subtitle", .subtitle_kind = SUBTITLE_BITMAP}}, .stream_count = 1, }; CHECK(compile_text_to("subtitles \"signs.sup\"\nduration 2s\ntake\n", "out.mks", &bitmap, &program, &typed, &cache, &command, &error)); CHECK(stage_contains(&command.stages[0], "copy")); CHECK(stage_contains(&command.stages[1], "copy")); CHECK(!command.stages[1].clip_subtitles); destroy_compilation(&program, &typed, &cache, &command); CHECK(!compile_text_to("subtitles \"signs.sup\"\n", "out.mp4", &bitmap, &program, &typed, &cache, &command, &error)); CHECK(contains(error.message, "bitmap subtitles")); probe_cache_destroy(&cache); typed_program_destroy(&typed); program_destroy(&program); } TEST(script_final_ffmpeg_arguments) { FakeProbe fake = {.video_duration = 10, .audio_duration = 10}; Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error; const char* text = "encoding \"-c:v libx264 -c:a aac\"\n" "encoding \"-c:v libx265 -crf 18 -preset slow -c:a libopus\"\n" "ffmpeg-args \"-movflags +faststart\"\n" "ffmpeg-args \"-metadata comment='custom value'\"\n" "combined \"episode.mp4\"\n"; CHECK(parse_and_check(text, &program, &typed, &error)); probe_cache_init(&cache, fake_probe, &fake); CHECK(compile_program(&program, &typed, &cache, "out.mkv", &command, &error)); CHECK(contains(command.shell_command, "-c:v libx265 -crf 18 -preset slow -c:a libopus")); CHECK(!contains(command.shell_command, "libx264")); CHECK(contains(command.shell_command, "-movflags")); CHECK(contains(command.shell_command, "+faststart")); CHECK(contains(command.shell_command, "-metadata 'comment=custom value'")); destroy_compilation(&program, &typed, &cache, &command); } int main(void) { RUN(arena_alignment_and_reset); RUN(parser_comments_and_paths); RUN(positional_script_arguments); RUN(duration_parsing); RUN(scalar_parsing); RUN(parser_errors); RUN(type_errors); RUN(valid_types); RUN(speed_validation); RUN(probe_cache); RUN(duration_and_fade_propagation); RUN(length_and_silence); RUN(video_duplication); RUN(audio_duplication); RUN(no_unnecessary_split_and_mapping); RUN(dropped_graph_is_unreachable); RUN(subtitle_mapping); RUN(burn_subtitles_and_video_conversion); RUN(bitmap_subtitle_burn_rejected); RUN(multitrack_combined_mapping); RUN(indexed_combined_operations); RUN(subtitle_timeline_operations); RUN(atempo_chain); RUN(arithmetic_comparison_and_logic); RUN(conditional_compilation); RUN(conditional_type_errors); RUN(loop_unrolling); RUN(nested_control_flow); RUN(loop_type_errors); RUN(loop_iteration_limit); RUN(metadata_chapters_and_attachments); RUN(subtitle_codec_preservation); RUN(script_final_ffmpeg_arguments); if (failures != 0) { fprintf(stderr, "%d checks failed across %d tests\n", failures, tests_run); return 1; } printf("all %d tests passed\n", tests_run); return 0; }