#include "../Sources/Core/progress_reporter.hpp" #import #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern char **environ; using playback::ProgressReporter; static const std::string firstURL = "https://www.youtube.com/watch?v=AAAAAAAAAAA"; static const std::string secondURL = "https://www.youtube.com/watch?v=BBBBBBBBBBB"; static void require(bool condition, const char *message) { if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } } static PlayerState sample(PlayerStatus status, double position, double duration = 100) { PlayerState state{}; state.status = status; state.position = position; state.duration = duration; state.has_audio = state.has_video = state.can_seek = true; return state; } static NSString *nativePath(const std::string &path) { return [NSString stringWithUTF8String:path.c_str()]; } static NSArray *records(const std::string &path) { NSString *text = [NSString stringWithContentsOfFile:nativePath(path) encoding:NSUTF8StringEncoding error:nullptr]; require(text != nil && [text hasSuffix:@"\n"], "finished JSONL has a complete final line"); NSMutableArray *result = [NSMutableArray array]; NSArray *lines = [text componentsSeparatedByString:@"\n"]; NSSet *keys = [NSSet setWithArray:@[@"version", @"event", @"media_url", @"position_seconds", @"duration_seconds", @"completion_eligible"]]; for (NSUInteger i = 0; i + 1 < lines.count; ++i) { id object = [NSJSONSerialization JSONObjectWithData:[lines[i] dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nullptr]; require([object isKindOfClass:[NSDictionary class]], "consumer decodes every JSONL record"); NSDictionary *row = object; require([[NSSet setWithArray:row.allKeys] isEqual:keys], "records have exactly the six specified fields"); require([row[@"version"] isKindOfClass:[NSNumber class]] && [row[@"version"] intValue] == 1, "schema version is numeric one"); require([@[@"progress", @"ended", @"closed"] containsObject:row[@"event"]], "known event type"); require([row[@"media_url"] isKindOfClass:[NSString class]] && ProgressReporter::validMediaURL([row[@"media_url"] UTF8String]), "canonical consumer identity"); require([row[@"position_seconds"] isKindOfClass:[NSNumber class]] && std::isfinite([row[@"position_seconds"] doubleValue]) && [row[@"position_seconds"] doubleValue] >= 0, "position is finite and nonnegative"); id duration = row[@"duration_seconds"]; require(duration == [NSNull null] || ([duration isKindOfClass:[NSNumber class]] && std::isfinite([duration doubleValue]) && [duration doubleValue] > 0), "duration is positive or null"); require(CFGetTypeID((__bridge CFTypeRef)row[@"completion_eligible"]) == CFBooleanGetTypeID(), "eligibility is a JSON boolean"); [result addObject:row]; } return result; } static void expect(NSDictionary *row, NSString *event, const std::string &url, double position, bool eligible, double duration = 100) { require([row[@"event"] isEqual:event] && [row[@"media_url"] isEqual:nativePath(url)] && [row[@"position_seconds"] doubleValue] == position && [row[@"completion_eligible"] boolValue] == eligible, "event has the acknowledged consumer snapshot"); require(duration > 0 && std::isfinite(duration) ? [row[@"duration_seconds"] doubleValue] == duration : row[@"duration_seconds"] == [NSNull null], "duration retains precision or is null"); } static bool rejected(const std::string &path) { try { ProgressReporter reporter(path); } catch (const std::exception &error) { return error.what()[0] != '\0'; } return false; } // An actual OS write failure, isolated so the resource limit cannot affect the parent. static int writeFailure(const std::string &path) { require(signal(SIGXFSZ, SIG_IGN) != SIG_ERR, "ignore file-size-limit signal"); struct rlimit limit{1, 1}; require(setrlimit(RLIMIT_FSIZE, &limit) == 0, "install deterministic one-byte file limit"); ProgressReporter reporter(path, [] { return 0.; }); reporter.begin(firstURL); reporter.observe(sample(PLAYER_PLAYING, 1)); reporter.finish(); auto error = reporter.error(); require(!error.empty(), "OS write failure is persistently reported without throwing"); reporter.begin(secondURL); reporter.observe(sample(PLAYER_ENDED, 100)); reporter.finish(); require(reporter.error() == error, "failure remains available after repeated finish and observations"); struct stat info{}; require(stat(path.c_str(), &info) == 0 && info.st_size == 1, "partial write is retained and failed output stays disabled"); return 0; } int main(int argc, char **argv) { if (argc == 3 && std::string(argv[1]) == "--write-failure") return writeFailure(argv[2]); @autoreleasepool { char directory[] = "/tmp/terebi-progress-reporting-XXXXXX"; require(mkdtemp(directory) != nullptr, "create isolated output directory"); const std::string root(directory); double now = 100; const auto path = root + "/cadence.jsonl"; { ProgressReporter reporter(path, [&] { return now; }); reporter.begin(firstURL); reporter.observe(sample(PLAYER_OPENING, 99)); reporter.observe(sample(PLAYER_ERROR, 99)); auto pending = sample(PLAYER_PAUSED, 99); pending.seek_pending = true; reporter.observe(pending); reporter.observe(sample(PLAYER_PLAYING, 1.2345678901234567)); now = 104.999; reporter.observe(sample(PLAYER_PLAYING, 4)); now = 105; reporter.observe(sample(PLAYER_PLAYING, 6.25)); now = 106; pending = sample(PLAYER_PLAYING, 99); pending.seek_pending = true; reporter.observe(pending); now = 107; reporter.observe(sample(PLAYER_PLAYING, 95)); now = 108; reporter.observe(sample(PLAYER_PLAYING, 96)); reporter.observe(sample(PLAYER_PAUSED, 96.5)); pending = sample(PLAYER_PAUSED, 50); pending.seek_pending = true; reporter.observe(pending); reporter.observe(sample(PLAYER_PAUSED, 50)); now = 200; reporter.observe(sample(PLAYER_PAUSED, 97)); reporter.observe(sample(PLAYER_PAUSED, std::numeric_limits::quiet_NaN())); pending.position = 99; reporter.observe(pending); reporter.closeMedia(); reporter.closeMedia(); reporter.finish(); reporter.finish(); require(reporter.error().empty(), "normal output drains without errors"); } auto rows = records(path); require(rows.count == 6, "initial, exact period, acknowledged seek, pause, backward seek, and one close only"); expect(rows[0], @"progress", firstURL, 1.2345678901234567, true); expect(rows[1], @"progress", firstURL, 6.25, true); expect(rows[2], @"progress", firstURL, 95, true); expect(rows[3], @"progress", firstURL, 96.5, true); expect(rows[4], @"progress", firstURL, 50, true); expect(rows[5], @"closed", firstURL, 97, true); const auto identityPath = root + "/identity.jsonl"; now = 0; { ProgressReporter reporter(identityPath, [&] { return now; }); reporter.begin(firstURL); reporter.observe(sample(PLAYER_PLAYING, 10)); now = 1; reporter.observe(sample(PLAYER_PLAYING, 96)); reporter.begin(firstURL); reporter.observe(sample(PLAYER_OPENING, 0)); now = 2; reporter.observe(sample(PLAYER_PAUSED, 96)); now = 2.1; reporter.observe(sample(PLAYER_PLAYING, 20)); now = 3; reporter.observe(sample(PLAYER_PLAYING, 96)); reporter.begin(secondURL); reporter.observe(sample(PLAYER_PLAYING, 90)); now = 3.1; reporter.observe(sample(PLAYER_PLAYING, 96)); now = 4; reporter.observe(sample(PLAYER_ENDED, 100)); reporter.observe(sample(PLAYER_ENDED, 0)); reporter.begin(""); reporter.observe(sample(PLAYER_PLAYING, 10)); reporter.finish(); } rows = records(identityPath); require(rows.count == 8, "quality reopen preserves threshold while replacement resets identity and closes each source"); expect(rows[0], @"progress", firstURL, 10, true); expect(rows[1], @"progress", firstURL, 96, true); expect(rows[2], @"progress", firstURL, 96, true); expect(rows[3], @"closed", firstURL, 96, true); expect(rows[4], @"progress", secondURL, 90, true); expect(rows[5], @"progress", secondURL, 96, true); expect(rows[6], @"ended", secondURL, 100, true); expect(rows[7], @"closed", secondURL, 100, true); const auto terminalPath = root + "/terminal.jsonl"; now = 0; { ProgressReporter reporter(terminalPath, [&] { return now; }); reporter.begin(firstURL); reporter.observe(sample(PLAYER_PLAYING, 94)); now = 1; reporter.observe(sample(PLAYER_PAUSED, 95)); reporter.closeMedia(); reporter.begin(secondURL); reporter.observe(sample(PLAYER_PLAYING, 94)); reporter.observe(sample(PLAYER_ENDED, 95)); reporter.finish(); } rows = records(terminalPath); require(rows.count == 6, "pause/ended threshold crossings do not add redundant progress events"); expect(rows[1], @"progress", firstURL, 95, true); expect(rows[2], @"closed", firstURL, 95, true); expect(rows[4], @"ended", secondURL, 95, true); expect(rows[5], @"closed", secondURL, 95, true); const auto eligibilityPath = root + "/eligibility.jsonl"; { ProgressReporter reporter(eligibilityPath, [] { return 0.; }); for (int scenario = 0; scenario < 7; ++scenario) { auto state = sample(PLAYER_PAUSED, 97); if (scenario == 0) state.duration = 0; if (scenario == 1) state.duration = -1; if (scenario == 2) state.duration = std::numeric_limits::infinity(); if (scenario == 3) state.duration = std::numeric_limits::quiet_NaN(); if (scenario == 5) state.can_seek = false; if (scenario == 6) state.has_video = false; reporter.begin(firstURL, scenario == 4); reporter.observe(state); reporter.closeMedia(); } reporter.finish(); } rows = records(eligibilityPath); require(rows.count == 14, "every ineligible source has an initial and terminal snapshot"); for (NSUInteger i = 0; i < rows.count; ++i) expect(rows[i], i % 2 ? @"closed" : @"progress", firstURL, 97, false, i < 8 ? 0 : 100); require(ProgressReporter::validMediaURL(firstURL) && ProgressReporter::validMediaURL("https://www.youtube.com/watch?v=aA0_-123456"), "eleven ASCII ID characters are accepted"); for (const char *invalid : {"", "http://www.youtube.com/watch?v=AAAAAAAAAAA", "https://youtube.com/watch?v=AAAAAAAAAAA", "https://www.youtube.com/watch?v=AAAAAAAAAA", "https://www.youtube.com/watch?v=AAAAAAAAAAAA", "https://www.youtube.com/watch?v=AAAAAAAAAAA&x=1", "https://www.youtube.com/watch?v=AAAAAAAAAA%", "https://www.youtube.com/watch?v=AAAAAAAAAA\n"}) require(!ProgressReporter::validMediaURL(invalid), "noncanonical URLs are rejected"); const auto clockFailurePath = root + "/clock-failure.jsonl"; { ProgressReporter reporter(clockFailurePath, []() -> double { throw std::runtime_error("clock failure"); }); reporter.begin(firstURL); reporter.observe(sample(PLAYER_PLAYING, 1)); reporter.closeMedia(); reporter.finish(); } require([[NSData dataWithContentsOfFile:nativePath(clockFailurePath)] length] == 0, "a reporting exception cannot emit later close records"); const auto privatePath = root + "/private.jsonl"; mode_t oldMask = umask(0777); { ProgressReporter reporter(privatePath); umask(oldMask); reporter.finish(); } struct stat info{}; require(stat(privatePath.c_str(), &info) == 0 && S_ISREG(info.st_mode) && (info.st_mode & 0777) == 0600 && info.st_uid == geteuid(), "new destination is regular and exactly owner-private despite umask"); const auto existingPath = root + "/existing.jsonl"; NSData *sentinel = [@"unchanged" dataUsingEncoding:NSUTF8StringEncoding]; require([sentinel writeToFile:nativePath(existingPath) atomically:NO], "create existing destination sentinel"); require(rejected(existingPath), "existing destination fails exclusive initialization"); const auto linkPath = root + "/symlink.jsonl"; require(symlink(existingPath.c_str(), linkPath.c_str()) == 0 && rejected(linkPath), "symlink destination fails initialization"); require([[NSData dataWithContentsOfFile:nativePath(existingPath)] isEqual:sentinel], "rejection never changes an existing destination"); const auto concurrentPath = root + "/concurrent.jsonl"; std::atomic ready{0}, successes{0}, failures{0}; std::atomic start{false}; auto contender = [&] { ready.fetch_add(1); while (!start.load()) std::this_thread::yield(); try { ProgressReporter reporter(concurrentPath); ++successes; reporter.finish(); } catch (const std::exception &) { ++failures; } }; std::thread left(contender), right(contender); while (ready.load() != 2) std::this_thread::yield(); start.store(true); left.join(); right.join(); require(successes == 1 && failures == 1, "concurrent initialization admits exactly one destination owner"); const auto failedPath = root + "/failed.jsonl"; char *childArguments[] = {argv[0], const_cast("--write-failure"), const_cast(failedPath.c_str()), nullptr}; pid_t child = 0; require(posix_spawnp(&child, argv[0], nullptr, nullptr, childArguments, environ) == 0, "launch isolated write-failure subprocess"); int status = 0; require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, "resource-limit failure remains nonfatal and output stays disabled"); require([[NSData dataWithContentsOfFile:nativePath(failedPath)] length] == 1, "forced failure permits only its final partial line"); require([[NSFileManager defaultManager] removeItemAtPath:nativePath(root) error:nullptr], "remove isolated check files"); std::puts("JSONL playback progress reporting passed"); } return 0; }