Files

429 lines
19 KiB
C

/* Exercise the real scheduler with a controllable execution engine. In
* particular, status can become idle while worker cleanup remains busy. */
#include "install_queue.h"
#include "installer.h"
#include "pkg_scanner.h"
#include "ws_upload.h"
#include "sqlite3.h"
#include <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdarg.h>
void install_log(const char *format, ...) { (void)format; }
static pthread_mutex_t engine_mutex = PTHREAD_MUTEX_INITIALIZER;
static installer_status_t status;
static int busy, offline, installed, starts;
static char paths[32][512];
static char session[64];
static char refused_title[32], refusal[128];
int installer_is_busy(void) { pthread_mutex_lock(&engine_mutex); int result = busy; pthread_mutex_unlock(&engine_mutex); return result; }
void installer_get_status(installer_status_t *out) { pthread_mutex_lock(&engine_mutex); *out = status; pthread_mutex_unlock(&engine_mutex); }
int installer_is_network_connected(void) { return !offline; }
int installer_start(const char *path) {
pthread_mutex_lock(&engine_mutex);
assert(!busy);
memset(&status, 0, sizeof(status));
snprintf(status.pkg_path, sizeof(status.pkg_path), "%s", path);
snprintf(paths[starts++], sizeof(paths[0]), "%s", path);
status.total_bytes = 100;
if (offline) {
snprintf(status.status_str, sizeof(status.status_str), "submitted");
status.is_direct_storage = 1;
status.progress_percent = -1;
} else { busy = 1; status.is_installing = 1; }
pthread_mutex_unlock(&engine_mutex);
return 0;
}
int installer_start_live(const char *path) { return installer_start(path); }
int installer_cancel_path(const char *path) {
pthread_mutex_lock(&engine_mutex);
if (!status.is_installing || (path && strcmp(status.pkg_path, path))) { pthread_mutex_unlock(&engine_mutex); return -1; }
status.is_installing = 0;
status.failed = 1;
snprintf(status.status_str, sizeof(status.status_str), "canceled");
/* Busy deliberately remains true until the test releases cleanup. */
pthread_mutex_unlock(&engine_mutex);
return 0;
}
int app_info_check_installed(const char *id, char *version, size_t capacity) { (void)id; (void)version; (void)capacity; return installed; }
int pkg_scanner_find_by_path(const char *path, pkg_detail_t *pkg) { (void)path; (void)pkg; return -1; }
int pkg_parser_parse(const char *path, pkg_detail_t *pkg) {
if (strstr(path, "slow")) usleep(300000);
if (strstr(path, "missing")) return -1;
memset(pkg, 0, sizeof(*pkg));
snprintf(pkg->title_id, sizeof(pkg->title_id), "PPSA0000%c", path[1]);
snprintf(pkg->title_name, sizeof(pkg->title_name), "Game %c", path[1]);
snprintf(pkg->pkg_type_str, sizeof(pkg->pkg_type_str), "%s", strstr(path, "update") ? "update" : strstr(path, "dlc") ? "dlc" : "base");
pkg->pkg_type = strstr(path, "update") ? PKG_TYPE_UPDATE : strstr(path, "dlc") ? PKG_TYPE_DLC : PKG_TYPE_BASE;
pkg->file_size = 100;
return 0;
}
void pkg_scanner_check_install_eligibility(const pkg_detail_t *pkg, pkg_install_eligibility_t *out) {
memset(out, 0, sizeof(*out));
out->can_install = pkg->pkg_type == PKG_TYPE_BASE || installed;
out->disabled_reason = out->can_install ? "" : "Base package is not installed";
pthread_mutex_lock(&engine_mutex);
if (refused_title[0] && !strcmp(refused_title, pkg->title_id)) {
out->can_install = 0;
out->disabled_reason = refusal;
}
pthread_mutex_unlock(&engine_mutex);
}
int ws_direct_owner_matches(const char *owner, const char *sid) { return owner && owner[0] && !strcmp(sid, session); }
int ws_direct_init_owned(const char *filename, uint64_t total, const char *owner, const char *resume,
char *sid, size_t capacity) {
(void)filename; (void)total; (void)owner; (void)resume;
snprintf(session, sizeof(session), "fixture"); snprintf(sid, capacity, "%s", session); return 0;
}
int ws_direct_cancel_owned(const char *owner, const char *sid) { (void)owner; (void)sid; session[0] = 0; return 0; }
static void finish(int cleanup_finished) {
pthread_mutex_lock(&engine_mutex);
status.is_installing = 0; status.completed = 1; status.failed = 0;
snprintf(status.status_str, sizeof(status.status_str), "completed");
busy = !cleanup_finished;
pthread_mutex_unlock(&engine_mutex);
}
static void release_cleanup(void) { pthread_mutex_lock(&engine_mutex); busy = 0; pthread_mutex_unlock(&engine_mutex); }
static int started_count(void) { pthread_mutex_lock(&engine_mutex); int result = starts; pthread_mutex_unlock(&engine_mutex); return result; }
static char *job_state(uint64_t id) {
char *json = install_queue_to_json();
sqlite3 *db; sqlite3_stmt *stmt;
assert(sqlite3_open(":memory:", &db) == SQLITE_OK);
assert(sqlite3_prepare_v2(db, "SELECT json_extract(value,'$.state') FROM json_each(?,'$.jobs') WHERE json_extract(value,'$.id')=?", -1, &stmt, NULL) == SQLITE_OK);
sqlite3_bind_text(stmt, 1, json, -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 2, id);
char *state = sqlite3_step(stmt) == SQLITE_ROW ? strdup((const char *)sqlite3_column_text(stmt, 0)) : strdup("missing");
sqlite3_finalize(stmt); sqlite3_close(db); free(json); return state;
}
static void wait_state(uint64_t id, const char *expected) {
for (int i = 0; i < 150; i++) {
char *state = job_state(id); int matched = !strcmp(state, expected); free(state);
if (matched) return;
usleep(20000);
}
char *json = install_queue_to_json(); fprintf(stderr, "Expected %llu %s: %s\n", (unsigned long long)id, expected, json); free(json); assert(0);
}
static install_queue_request_t request(const char *path) {
install_queue_request_t result = {0}; pkg_detail_t pkg;
snprintf(result.path, sizeof(result.path), "%s", path);
if (!pkg_parser_parse(path, &pkg)) {
snprintf(result.title_id, sizeof(result.title_id), "%s", pkg.title_id);
snprintf(result.kind, sizeof(result.kind), "%s", pkg.pkg_type_str);
}
result.total_bytes = 100;
return result;
}
static void reset(void) {
install_queue_shutdown();
pthread_mutex_lock(&engine_mutex);
memset(&status, 0, sizeof(status)); busy = starts = 0;
refused_title[0] = refusal[0] = 0;
pthread_mutex_unlock(&engine_mutex);
offline = installed = 0;
assert(install_queue_init() == 0);
}
int main(void) {
reset();
install_queue_request_t batch[] = {request("/A/base"), request("/A/update"), request("/B/base")};
uint64_t ids[3];
assert(install_queue_add(batch, 3, ids) == 0);
wait_state(ids[0], "installing");
assert(install_queue_cancel(ids[1]) == 0);
wait_state(ids[1], "canceled");
assert(started_count() == 1);
finish(0); usleep(250000);
assert(started_count() == 1); /* status idle is NOT worker idle */
release_cleanup();
wait_state(ids[2], "installing");
assert(!strcmp(paths[1], "/B/base"));
finish(1); wait_state(ids[2], "completed");
reset();
assert(install_queue_add(batch, 3, ids) == 0);
wait_state(ids[0], "installing");
assert(install_queue_cancel(ids[0]) == 0);
release_cleanup();
wait_state(ids[0], "canceled");
wait_state(ids[1], "blocked");
wait_state(ids[2], "installing");
finish(1); wait_state(ids[2], "completed");
assert(install_queue_retry(ids[0]) == 0);
wait_state(ids[0], "installing");
finish(1); wait_state(ids[1], "installing");
finish(1); wait_state(ids[1], "completed");
reset();
install_queue_request_t checking[] = {request("/A/slowbase"), request("/B/base")};
assert(install_queue_add(checking, 2, ids) == 0);
wait_state(ids[0], "checking");
assert(install_queue_cancel(ids[0]) == 0);
wait_state(ids[0], "canceled");
wait_state(ids[1], "installing");
assert(started_count() == 1 && !strcmp(paths[0], "/B/base"));
finish(1); wait_state(ids[1], "completed");
reset();
offline = 1;
assert(install_queue_add(batch, 3, ids) == 0);
wait_state(ids[2], "submitted");
wait_state(ids[1], "submitted");
assert(started_count() == 3);
assert(!strcmp(paths[0], "/A/base") && !strcmp(paths[1], "/A/update"));
reset();
install_queue_request_t unavailable[] = {request("/A/missing"), request("/B/base")};
assert(install_queue_add(unavailable, 2, ids) == 0);
wait_state(ids[0], "failed"); wait_state(ids[1], "installing");
finish(1); wait_state(ids[1], "completed");
reset();
install_queue_request_t browser = {0};
snprintf(browser.owner, sizeof(browser.owner), "0123456789abcdef-private-token");
snprintf(browser.file_key, sizeof(browser.file_key), "game.pkg|100|1");
snprintf(browser.title_id, sizeof(browser.title_id), "PPSA00001");
snprintf(browser.kind, sizeof(browser.kind), "base"); browser.total_bytes = 100;
assert(install_queue_add(&browser, 1, ids) == 0);
wait_state(ids[0], "preparing");
char *json = install_queue_to_json(); assert(!strstr(json, "private-token")); free(json);
assert(!install_queue_browser_selected(ids[0], "another-tab"));
char sid[64];
assert(install_queue_open_upload(ids[0], browser.owner, "game.pkg", 99, sid, sizeof(sid)) != 0);
assert(install_queue_open_upload(ids[0], browser.owner, "game.pkg", 100, sid, sizeof(sid)) == 0);
assert(install_queue_attach(ids[0], "another-tab", "live:fixture") != 0);
assert(install_queue_attach(ids[0], browser.owner, "live:fixture") == 0);
install_queue_disconnect(browser.owner); release_cleanup(); wait_state(ids[0], "canceled");
const char *redundant[] = {"Installed version is same or newer", "Application is already installed", "DLC is already installed"};
for (size_t i = 0; i < sizeof(redundant) / sizeof(redundant[0]); i++) {
reset();
installed = 1;
snprintf(refused_title, sizeof(refused_title), "%s", browser.title_id);
snprintf(refusal, sizeof(refusal), "%s", redundant[i]);
snprintf(browser.kind, sizeof(browser.kind), "%s", i == 2 ? "dlc" : "base");
snprintf(browser.content_id, sizeof(browser.content_id), "UP0000-PPSA00001_00-REDUNDANTCONTENT");
install_queue_request_t duplicates[] = {browser, request("/B/base")};
assert(install_queue_add(duplicates, 2, ids) == 0);
wait_state(ids[0], "aborted");
wait_state(ids[1], "installing");
assert(started_count() == 1 && !strcmp(paths[0], "/B/base"));
assert(!install_queue_browser_selected(ids[0], browser.owner));
assert(install_queue_retry(ids[0]) != 0);
install_queue_clear_finished();
wait_state(ids[0], "missing");
finish(1); wait_state(ids[1], "completed");
}
/* Redundant non-browser package (already installed) cancels immediately instead of blocking */
reset();
installed = 1;
snprintf(refused_title, sizeof(refused_title), "PPSA0000A");
snprintf(refusal, sizeof(refusal), "DLC is already installed");
install_queue_request_t redundant_file[] = {request("/A/redundant_dlc"), request("/B/base")};
assert(install_queue_add(redundant_file, 2, ids) == 0);
wait_state(ids[0], "canceled");
wait_state(ids[1], "installing");
finish(1); wait_state(ids[1], "completed");
reset();
size_t count;
assert(install_queue_add_json("{\"jobs\":[{\"path\":\"/A/base\"},{\"path\":\"live:bad\"}]}", ids, &count) != 0);
json = install_queue_to_json(); assert(strstr(json, "\"jobs\":[]")); free(json);
assert(install_queue_add_json("{\"jobs\":[{\"path\":\"/A/base\"},{\"path\":\"/A/base\"}]}", ids, &count) == 0);
assert(count == 2 && ids[0] == ids[1]);
assert(install_queue_add_json("{\"jobs\":[]}", ids, &count) != 0);
assert(install_queue_add_json("{\"jobs\":true}", ids, &count) != 0);
assert(install_queue_add_json("{\"jobs\":[{\"path\":\"/A/ba\\u0000se\"}]}", ids, &count) != 0);
/* Test TTL auto-clearing and dependency preservation */
reset();
assert(install_queue_get_finished_ttl() == INSTALL_QUEUE_FINISHED_TTL_DEFAULT);
install_queue_set_finished_ttl(1);
assert(install_queue_get_finished_ttl() == 1);
/* Completed job auto-clears after TTL */
install_queue_request_t single_job = request("/A/base");
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "installing");
finish(1);
wait_state(ids[0], "completed");
char *st = job_state(ids[0]);
assert(!strcmp(st, "completed"));
free(st);
wait_state(ids[0], "missing");
/* Submitted job (offline) auto-clears after TTL */
reset();
install_queue_set_finished_ttl(1);
offline = 1;
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "submitted");
wait_state(ids[0], "missing");
offline = 0;
/* Canceled job auto-clears after TTL */
reset();
install_queue_set_finished_ttl(1);
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "installing");
assert(install_queue_cancel(ids[0]) == 0);
release_cleanup();
wait_state(ids[0], "canceled");
wait_state(ids[0], "missing");
/* Failed job auto-clears after TTL */
reset();
install_queue_set_finished_ttl(1);
install_queue_request_t fail_job = request("/A/missing");
assert(install_queue_add(&fail_job, 1, ids) == 0);
wait_state(ids[0], "failed");
wait_state(ids[0], "missing");
/* Aborted job (redundant browser pkg) auto-clears after TTL */
reset();
install_queue_set_finished_ttl(1);
installed = 1;
snprintf(refused_title, sizeof(refused_title), "%s", browser.title_id);
snprintf(refusal, sizeof(refusal), "%s", "Application is already installed");
snprintf(browser.kind, sizeof(browser.kind), "base");
assert(install_queue_add(&browser, 1, ids) == 0);
wait_state(ids[0], "aborted");
wait_state(ids[0], "missing");
/* Submitted/completed base needed by pending dependent is NOT cleared even if TTL expires */
reset();
install_queue_set_finished_ttl(1);
install_queue_request_t dep_pair[] = {request("/A/base"), request("/A/update")};
assert(install_queue_add(dep_pair, 2, ids) == 0);
wait_state(ids[0], "installing");
finish(1); /* base completes */
wait_state(ids[0], "completed");
wait_state(ids[1], "installing");
usleep(1200000); /* wait 1.2s > 1s TTL */
st = job_state(ids[0]);
assert(!strcmp(st, "completed")); /* base still preserved because update is pending */
free(st);
finish(1);
wait_state(ids[1], "completed");
/* Now dependent update is completed, base auto-clears */
wait_state(ids[0], "missing");
/* Update itself auto-clears after its TTL */
wait_state(ids[1], "missing");
/* Multiple dependents keep base alive until ALL dependents finish */
reset();
install_queue_set_finished_ttl(1);
install_queue_request_t multi_deps[] = {request("/A/base"), request("/A/update"), request("/A/dlc")};
uint64_t multi_ids[3];
assert(install_queue_add(multi_deps, 3, multi_ids) == 0);
wait_state(multi_ids[0], "installing");
finish(1); /* base completes */
wait_state(multi_ids[0], "completed");
wait_state(multi_ids[1], "installing");
usleep(1200000); /* base TTL expires while update is installing */
st = job_state(multi_ids[0]);
assert(!strcmp(st, "completed"));
free(st);
finish(1); /* update completes */
wait_state(multi_ids[1], "completed");
wait_state(multi_ids[2], "installing");
st = job_state(multi_ids[0]);
assert(!strcmp(st, "completed")); /* base STILL preserved because DLC is pending */
free(st);
usleep(1200000); /* update TTL expires while DLC is still pending */
st = job_state(multi_ids[0]);
assert(!strcmp(st, "completed")); /* base still preserved */
free(st);
st = job_state(multi_ids[1]);
assert(!strcmp(st, "missing")); /* update auto-cleared because dependents only need base */
free(st);
finish(1); /* dlc completes */
wait_state(multi_ids[2], "completed");
wait_state(multi_ids[0], "missing");
wait_state(multi_ids[2], "missing");
/* Manual clear_finished clears update but preserves base while dependent DLC is pending */
reset();
install_queue_set_finished_ttl(60); /* long TTL so auto-clear doesn't fire */
assert(install_queue_add(multi_deps, 3, multi_ids) == 0);
wait_state(multi_ids[0], "installing");
finish(1); /* base completes */
wait_state(multi_ids[0], "completed");
wait_state(multi_ids[1], "installing");
finish(1); /* update completes */
wait_state(multi_ids[1], "completed");
wait_state(multi_ids[2], "installing");
install_queue_clear_finished();
st = job_state(multi_ids[0]);
assert(!strcmp(st, "completed")); /* base preserved because DLC is pending */
free(st);
st = job_state(multi_ids[1]);
assert(!strcmp(st, "missing")); /* update cleared because it's not a base */
free(st);
finish(1); /* dlc completes */
wait_state(multi_ids[2], "completed");
install_queue_clear_finished();
wait_state(multi_ids[0], "missing");
wait_state(multi_ids[2], "missing");
/* Dependent package without base installed or queued is canceled, not looped */
reset();
install_queue_request_t orphan = request("/A/update");
assert(install_queue_add(&orphan, 1, ids) == 0);
wait_state(ids[0], "canceled");
/* TTL = 0 clears terminal jobs immediately on next tick */
reset();
install_queue_set_finished_ttl(0);
assert(install_queue_get_finished_ttl() == 0);
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "installing");
finish(1);
wait_state(ids[0], "missing");
/* Negative TTL disables auto-clearing */
reset();
install_queue_set_finished_ttl(-1);
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "installing");
finish(1);
wait_state(ids[0], "completed");
usleep(1200000);
st = job_state(ids[0]);
assert(!strcmp(st, "completed"));
free(st);
/* Retry before TTL resets expiration */
reset();
install_queue_set_finished_ttl(1);
assert(install_queue_add(&single_job, 1, ids) == 0);
wait_state(ids[0], "installing");
assert(install_queue_cancel(ids[0]) == 0);
release_cleanup();
wait_state(ids[0], "canceled");
assert(install_queue_retry(ids[0]) == 0);
wait_state(ids[0], "installing");
usleep(1200000);
st = job_state(ids[0]);
assert(!strcmp(st, "installing"));
free(st);
finish(1);
wait_state(ids[0], "completed");
wait_state(ids[0], "missing");
install_queue_shutdown();
assert(install_queue_init() == 0);
json = install_queue_to_json(); assert(strstr(json, "\"jobs\":[]")); free(json);
install_queue_shutdown();
puts("Install queue: FIFO, dependencies, redundant browser aborts, cancel, cleanup, retry, submission, source ownership, auto-clearing and atomic batches passed");
}