#define STRVIEW_IMPLEMENTATION
#include "strview.h"
#include "compiler.h"
#include "parser.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
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(custom_final_ffmpeg_arguments) {
FakeProbe fake = {.video_duration = 10, .audio_duration = 10};
Program program; TypedProgram typed; ProbeCache cache; CompiledCommand command; Error error;
const char* arguments[] = {
"-c:v", "libx265", "-crf", "18", "-preset", "slow", "-c:a", "libopus",
};
CHECK(parse_and_check("combined \"episode.mp4\"\n", &program, &typed, &error));
probe_cache_init(&cache, fake_probe, &fake);
CHECK(compile_program_with_ffmpeg_args(&program, &typed, &cache, "out.mkv",
sizeof(arguments) / sizeof(arguments[0]), arguments, &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, "-c:a aac"));
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(custom_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;
}