#include #include #include #include #include #include "installer.h" #include "pkg_scanner.h" #include "test_fixture.h" /* Wait up to timeout_ms for cond_fn-style polling via status snapshot. */ static int wait_for_state(int want_installing, int want_completed, int timeout_ms) { int waited = 0; while (waited < timeout_ms) { installer_status_t st; installer_get_status(&st); if (!!st.is_installing == !!want_installing && (!want_completed || st.completed)) { return 0; } usleep(100000); waited += 100; } return -1; } int main(void) { int res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); installer_status_t st; installer_get_status(&st); assert(st.is_installing == 0); assert(strcmp(st.status_str, "idle") == 0); char *json = installer_status_to_json(); printf("Initial status JSON: %s\n", json); assert(strstr(json, "\"is_installing\":false") != NULL); free(json); /* Synthetic fixture: no external files needed. */ system("rm -rf /tmp/test_installer_fixtures && mkdir -p /tmp/test_installer_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_installer_fixtures/wc.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); /* Test 0: Missing package file fails immediately with -4 */ int missing_res = installer_start("/tmp/test_installer_fixtures/missing_nonexistent.pkg"); printf("Missing pkg install start result: %d (expected -4)\n", missing_res); assert(missing_res == -4); installer_get_status(&st); assert(st.is_installing == 0); /* Test 1: Start install with standard pkg (streams via worker thread) */ int start_res = installer_start("/tmp/test_installer_fixtures/wc.pkg"); printf("Start install result: %d\n", start_res); assert(start_res == 0); /* Title fields are populated synchronously by installer_start */ installer_get_status(&st); assert(strcmp(st.title_id, "PPSA90012") == 0); assert(strcmp(st.title_name, "WaveCast") == 0); /* The worker streams to completion asynchronously; wait for it */ assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.status_str, "playable") == 0); printf("Single-pkg stream install completed: progress=%.1f%%\n", st.progress_percent); size_t log_sz = 0; char *log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "name='WaveCast (base)'") != NULL); assert(strstr(log_txt, "icon='http://127.0.0.1:18841/stream/install/icon-") != NULL); free(log_txt); char *active_json = installer_status_to_json(); printf("Final status JSON: %s\n", active_json); assert(strstr(active_json, "WaveCast") != NULL); assert(strstr(active_json, "PPSA90012") != NULL); free(active_json); /* Test 1b: the native installer owns the base -> update handoff. This * must complete without any status polling or browser-side callback. */ assert(fixture_write_ps5_pkg("/tmp/test_installer_fixtures/wc_update.pkg", "PPSA90012", "WaveCast", "gp", "01.004.000", 1) == 0); int batch_res = installer_start_batch("/tmp/test_installer_fixtures/wc.pkg", "/tmp/test_installer_fixtures/wc_update.pkg"); printf("Start native base + update batch result: %d\n", batch_res); assert(batch_res == 0); int batch_waited = 0; int update_seen = 0; while (batch_waited < 15000) { installer_get_status(&st); if (strcmp(st.pkg_path, "/tmp/test_installer_fixtures/wc_update.pkg") == 0) { update_seen = 1; } if (update_seen && !st.is_installing && st.completed) break; usleep(100000); batch_waited += 100; } assert(update_seen == 1); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.pkg_path, "/tmp/test_installer_fixtures/wc_update.pkg") == 0); printf("Native base + update batch completed without browser polling\n"); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "name='WaveCast (update v1.04)'") != NULL); assert(strstr(log_txt, "icon='http://127.0.0.1:18841/stream/install/icon-") != NULL); free(log_txt); /* Test 1c: DLC package display name */ assert(fixture_write_ps5_pkg("/tmp/test_installer_fixtures/wc_dlc.pkg", "PPSA90012", "WaveCast DLC", "ac", "01.000.000", 1) == 0); assert(installer_start("/tmp/test_installer_fixtures/wc_dlc.pkg") == 0); assert(wait_for_state(0, 1, 15000) == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "name='WaveCast DLC (DLC)'") != NULL); assert(strstr(log_txt, "icon='http://127.0.0.1:18841/stream/install/icon-") != NULL); free(log_txt); printf("DLC install verified: name contains app title and (DLC), icon URL populated\n"); installer_shutdown(); /* Test 2: Monitor passivity: no browser relaunch machinery remains. * The installer monitor worker is intentionally passive so someone can * play a title while a big package installs in the background. */ printf("Testing monitor passivity (install must keep running)...\n"); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); system("rm -f /tmp/watchdog_big.pkg && truncate -s 3G /tmp/watchdog_big.pkg"); int big_res = installer_start("/tmp/watchdog_big.pkg"); assert(big_res == 0); sleep(8); /* Exceed old 7-second threshold without polling */ installer_get_status(&st); assert(st.is_installing == 1); /* sparse 3GB stream still running */ assert(installer_cancel() == 0); installer_shutdown(); system("rm -f /tmp/watchdog_big.pkg"); /* Test 3: Fallback install on an unparsable/raw file, through to completion */ res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); system("echo 'dummy content' > /tmp/dummy_test.pkg"); int unparsed_res = installer_start("/tmp/dummy_test.pkg"); printf("Unparsed pkg install start result: %d\n", unparsed_res); assert(unparsed_res == 0); /* MUST succeed and not return -3 */ assert(wait_for_state(0, 1, 10000) == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "name='Package (base)', icon=''") != NULL); free(log_txt); char *unparsed_json = installer_status_to_json(); printf("Unparsed status JSON: %s\n", unparsed_json); assert(strstr(unparsed_json, "Package") != NULL); assert(strstr(unparsed_json, "\"completed\":true") != NULL); free(unparsed_json); installer_shutdown(); system("rm -f /tmp/dummy_test.pkg"); /* Test 4: USB package stream install (defaults to stream installer) */ printf("Testing USB package stream install (default)...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); int usb_res = installer_start("/tmp/test_usb_fixtures/wc_usb.pkg"); assert(usb_res == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.status_str, "playable") == 0); assert(st.progress_percent == 100.0f); assert(strstr(st.prompt_message, "ready to play") != NULL); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "Initiating stream install:") != NULL); assert(strstr(log_txt, "raw server listening on 127.0.0.1:18841") != NULL); free(log_txt); char *usb_json = installer_status_to_json(); assert(strstr(usb_json, "\"completed\":true") != NULL); assert(strstr(usb_json, "\"status\":\"playable\"") != NULL); assert(strstr(usb_json, "\"progress\":100.00") != NULL); assert(strstr(usb_json, "\"is_direct_storage\":false") != NULL); free(usb_json); installer_shutdown(); printf("USB package stream install passed!\n"); /* Test 5: Disc package stream install (default) */ printf("Testing Disc package stream install (default)...\n"); setenv("PKG_DISC_DIR", "/tmp/test_disc_fixtures", 1); system("rm -rf /tmp/test_disc_fixtures && mkdir -p /tmp/test_disc_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_disc_fixtures/wc_disc.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); int disc_res = installer_start("/tmp/test_disc_fixtures/wc_disc.pkg"); assert(disc_res == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.status_str, "playable") == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "Initiating stream install:") != NULL); assert(strstr(log_txt, "raw server listening on 127.0.0.1:18841") != NULL); free(log_txt); installer_shutdown(); printf("Disc package stream install passed!\n"); /* Test 6: USB batch install (base + update) via stream installer */ printf("Testing USB batch install (base + update) via stream installer...\n"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb_upd.pkg", "PPSA90012", "WaveCast", "gp", "01.004.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); int usb_batch_res = installer_start_batch("/tmp/test_usb_fixtures/wc_usb.pkg", "/tmp/test_usb_fixtures/wc_usb_upd.pkg"); assert(usb_batch_res == 0); int usb_batch_waited = 0; int usb_update_seen = 0; while (usb_batch_waited < 15000) { installer_get_status(&st); if (strcmp(st.pkg_path, "/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0) { usb_update_seen = 1; } if (usb_update_seen && !st.is_installing && st.completed) break; usleep(100000); usb_batch_waited += 100; } assert(usb_update_seen == 1); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.pkg_path, "/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "Initiating stream install:") != NULL); assert(strstr(log_txt, "raw server listening on 127.0.0.1:18841") != NULL); free(log_txt); installer_shutdown(); printf("USB batch install via stream installer passed!\n"); /* Test 7: Multi-part package on USB must use stream install (not direct filesystem) */ printf("Testing multi-part package on USB uses stream install...\n"); setenv("PKG_TMP_DIR", "/tmp/test_usb_fixtures/tmp", 1); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/src_big.pkg", "PPSA90010", "BigGame", "gd", "01.000.000", 1) == 0); assert(fixture_grow_file("/tmp/test_usb_fixtures/src_big.pkg", 5ULL * 1024 * 1024) == 0); int split_ret = system("python3 tools/pkg_split.py /tmp/test_usb_fixtures/src_big.pkg -o /tmp/test_usb_fixtures -s 2M > /dev/null 2>&1"); assert(split_ret == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); int mp_res = installer_start("/tmp/test_usb_fixtures/src_big.pkg.part1"); assert(mp_res == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); log_txt = install_log_get_text(&log_sz); /* Must be multi-part stream install, NOT direct filesystem install */ assert(strstr(log_txt, "Initiating multi-part stream install:") != NULL); assert(strstr(log_txt, "raw server listening on 127.0.0.1:18841") != NULL); free(log_txt); installer_shutdown(); unsetenv("PKG_USB_PREFIX"); unsetenv("PKG_DISC_DIR"); unsetenv("PKG_TMP_DIR"); system("rm -rf /tmp/test_usb_fixtures /tmp/test_disc_fixtures"); /* Test 8: Unit test path classification with installer_is_filesystem_install */ printf("Testing installer_is_filesystem_install path classification...\n"); assert(installer_is_filesystem_install("/mnt/usb0/game.pkg", 0) == 1); assert(installer_is_filesystem_install("/mnt/usb7/pkg/game.pkg", 0) == 1); assert(installer_is_filesystem_install("/mnt/disc/game.pkg", 0) == 1); assert(installer_is_filesystem_install("/mnt/usb0/game.pkg.part1", 1) == 0); assert(installer_is_filesystem_install("/mnt/disc/game.pkg.part1", 1) == 0); assert(installer_is_filesystem_install("smb://192.168.1.10/share/game.pkg", 0) == 0); assert(installer_is_filesystem_install("live:session123", 0) == 0); assert(installer_is_filesystem_install(NULL, 0) == 0); assert(installer_is_filesystem_install("", 0) == 0); setenv("PKG_SCAN_DIR", "/tmp/custom_scan_path", 1); assert(installer_is_filesystem_install("/tmp/custom_scan_path/title.pkg", 0) == 1); assert(installer_is_filesystem_install("/tmp/custom_scan_path/title.pkg.part1", 1) == 0); unsetenv("PKG_SCAN_DIR"); setenv("PKG_USB_PREFIX", "/media/usb", 1); assert(installer_is_filesystem_install("/media/usb0/title.pkg", 0) == 1); unsetenv("PKG_USB_PREFIX"); setenv("PKG_DISC_DIR", "/media/disc", 1); assert(installer_is_filesystem_install("/media/disc/title.pkg", 0) == 1); unsetenv("PKG_DISC_DIR"); printf("installer_is_filesystem_install path classification passed!\n"); /* Test 9: Canceling an install */ printf("Testing cancel of install...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_big_usb.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); assert(fixture_grow_file("/tmp/test_usb_fixtures/wc_big_usb.pkg", 50ULL * 1024 * 1024) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); assert(installer_start("/tmp/test_usb_fixtures/wc_big_usb.pkg") == 0); assert(installer_cancel() == 0); installer_get_status(&st); assert(st.is_installing == 0); assert(st.failed == 1); assert(strcmp(st.status_str, "canceled") == 0); installer_shutdown(); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_fixtures"); printf("Cancel of install passed!\n"); /* Test 10a: USB package upfront offline direct storage install */ printf("Testing USB package upfront offline direct storage install...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); setenv("PKG_TEST_OFFLINE_NET", "1", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); assert(installer_start("/tmp/test_usb_fixtures/wc_usb.pkg") == 0); /* Verify installer_cancel() rejects canceling direct storage installs (-2) */ assert(installer_cancel() == -2); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(st.is_direct_storage == 1); assert(st.progress_percent == 100.0f); assert(strcmp(st.status_str, "playable") == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "Network not connected; using direct storage install") != NULL); assert(strstr(log_txt, "Cancel rejected: direct storage install cannot be canceled") != NULL); assert(strstr(log_txt, "Direct filesystem install verified completed") != NULL); free(log_txt); char *offline_json = installer_status_to_json(); assert(strstr(offline_json, "\"is_direct_storage\":true") != NULL); assert(strstr(offline_json, "\"direct_storage\"") == NULL); free(offline_json); installer_shutdown(); unsetenv("PKG_TEST_OFFLINE_NET"); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_fixtures"); printf("USB package upfront offline direct storage install passed!\n"); /* Test 10b: USB package runtime fallback (0x80B21121 -> direct storage install) */ printf("Testing USB package runtime fallback (0x80B21121 -> direct storage install)...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); setenv("PKG_TEST_SIMULATE_0x80B21121", "1", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); assert(installer_start("/tmp/test_usb_fixtures/wc_usb.pkg") == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(st.is_direct_storage == 1); assert(st.progress_percent == 100.0f); assert(strcmp(st.status_str, "playable") == 0); log_txt = install_log_get_text(&log_sz); assert(strstr(log_txt, "0x80B21121") != NULL); assert(strstr(log_txt, "falling back to direct storage install") != NULL); assert(strstr(log_txt, "Direct filesystem install verified completed") != NULL); free(log_txt); installer_shutdown(); unsetenv("PKG_TEST_SIMULATE_0x80B21121"); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_fixtures"); printf("USB package runtime fallback passed!\n"); /* Test 10c: USB batch install (base + update) upfront offline direct storage install */ printf("Testing USB batch install (base + update) upfront offline direct storage install...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); setenv("PKG_TEST_OFFLINE_NET", "1", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb.pkg", "PPSA90012", "WaveCast", "gd", "01.003.000", 1) == 0); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb_upd.pkg", "PPSA90012", "WaveCast", "gp", "01.004.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); int usb_direct_batch_res = installer_start_batch("/tmp/test_usb_fixtures/wc_usb.pkg", "/tmp/test_usb_fixtures/wc_usb_upd.pkg"); assert(usb_direct_batch_res == 0); int usb_direct_waited = 0; int usb_direct_update_seen = 0; while (usb_direct_waited < 15000) { installer_get_status(&st); if (strcmp(st.pkg_path, "/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0) { usb_direct_update_seen = 1; assert(st.is_direct_storage == 1); } if (usb_direct_update_seen && !st.is_installing && st.completed) break; usleep(100000); usb_direct_waited += 100; } assert(usb_direct_update_seen == 1); assert(st.completed == 1); assert(st.failed == 0); assert(st.is_direct_storage == 1); assert(strcmp(st.pkg_path, "/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0); installer_shutdown(); unsetenv("PKG_TEST_OFFLINE_NET"); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_fixtures"); printf("USB batch install upfront offline direct storage passed!\n"); /* Test 10d: USB update package only upfront offline direct storage install */ printf("Testing USB update package only upfront offline direct storage install...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_fixtures", 1); setenv("PKG_TEST_OFFLINE_NET", "1", 1); system("rm -rf /tmp/test_usb_fixtures && mkdir -p /tmp/test_usb_fixtures"); assert(fixture_write_ps5_pkg("/tmp/test_usb_fixtures/wc_usb_upd.pkg", "PPSA90012", "WaveCast", "gp", "01.004.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); install_log_clear(); assert(installer_start("/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(st.is_direct_storage == 1); assert(strcmp(st.pkg_path, "/tmp/test_usb_fixtures/wc_usb_upd.pkg") == 0); installer_shutdown(); unsetenv("PKG_TEST_OFFLINE_NET"); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_fixtures"); printf("USB update package only upfront offline direct storage passed!\n"); /* Test 11: Content ID to Title ID parsing logic */ printf("Testing title_id extraction from content_id...\n"); const char *test_cids[] = { "JP0000-CUSA00001_00-TESTCONTENT000000", "UP0000-PPSA00002_00-TESTCONTENT000000", "EP0000-CUSA00003_00-TESTCONTENT000000" }; const char *expected_tids[] = { "CUSA00001", "PPSA00002", "CUSA00003" }; for (size_t i = 0; i < sizeof(test_cids) / sizeof(test_cids[0]); i++) { char extracted[32] = {0}; const char *dash = strchr(test_cids[i], '-'); assert(dash != NULL); const char *tid_start = dash + 1; const char *underscore = strchr(tid_start, '_'); assert(underscore != NULL); size_t tlen = (size_t)(underscore - tid_start); assert(tlen < sizeof(extracted)); strncpy(extracted, tid_start, tlen); extracted[tlen] = '\0'; assert(strcmp(extracted, expected_tids[i]) == 0); } printf("title_id extraction from content_id passed!\n"); /* Test 12: Sequential USB stream installs reset progress state per-install */ printf("Testing sequential USB stream installs reset per-install progress state...\n"); setenv("PKG_USB_PREFIX", "/tmp/test_usb_seq", 1); system("rm -rf /tmp/test_usb_seq && mkdir -p /tmp/test_usb_seq"); assert(fixture_write_ps5_pkg("/tmp/test_usb_seq/seq1.pkg", "PPSA90021", "SeqGame1", "gd", "01.000.000", 1) == 0); assert(fixture_write_ps5_pkg("/tmp/test_usb_seq/seq2.pkg", "PPSA90022", "SeqGame2", "gd", "01.000.000", 1) == 0); res = installer_init("http://127.0.0.1:8085/"); assert(res == 0); /* First install */ assert(installer_start("/tmp/test_usb_seq/seq1.pkg") == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.pkg_path, "/tmp/test_usb_seq/seq1.pkg") == 0); /* Second install in the same process session (verifies per-install progress isolation) */ assert(installer_start("/tmp/test_usb_seq/seq2.pkg") == 0); assert(wait_for_state(0, 1, 15000) == 0); installer_get_status(&st); assert(st.completed == 1); assert(st.failed == 0); assert(strcmp(st.pkg_path, "/tmp/test_usb_seq/seq2.pkg") == 0); assert(st.downloaded_bytes == st.total_bytes); installer_shutdown(); unsetenv("PKG_USB_PREFIX"); system("rm -rf /tmp/test_usb_seq"); printf("Sequential USB stream installs passed!\n"); printf("\n>>> ALL INSTALLER TESTS PASSED! <<<\n"); return 0; }