Files
terebi/Checks/progress-reporting.mm
T

305 lines
15 KiB
Plaintext

#include "../Sources/Core/progress_reporter.hpp"
#import <Foundation/Foundation.h>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <signal.h>
#include <spawn.h>
#include <string>
#include <stdexcept>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <thread>
#include <unistd.h>
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<NSDictionary *> *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<NSDictionary *> *result = [NSMutableArray array];
NSArray<NSString *> *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<double>::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<double>::infinity();
if (scenario == 3) state.duration = std::numeric_limits<double>::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<int> ready{0}, successes{0}, failures{0};
std::atomic<bool> 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<char *>("--write-failure"),
const_cast<char *>(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;
}