remove updater tests after validation
This commit is contained in:
parent
b41c23d338
commit
f1764a0bba
|
|
@ -116,12 +116,12 @@ jobs:
|
|||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
|
||||
shader_recompiler_compute_tests ^
|
||||
resource_tracking_tests scalar_provenance_tests ^
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests update_release_tests update_installer_tests --parallel
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
|
||||
|
||||
- name: Test
|
||||
shell: cmd
|
||||
run: |
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation|update_release|update_installer)$"
|
||||
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
|
||||
|
||||
- name: Install
|
||||
shell: cmd
|
||||
|
|
@ -205,14 +205,14 @@ jobs:
|
|||
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests update_release_tests update_installer_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/macos --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation|update_release|update_installer)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
|
@ -390,14 +390,14 @@ jobs:
|
|||
--target launcher page_manager_tests memory_tracker_tests \
|
||||
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
|
||||
resource_tracking_tests scalar_provenance_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests update_release_tests update_installer_tests \
|
||||
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
|
||||
--parallel
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
run: |
|
||||
ctest --test-dir _Build/linux --output-on-failure \
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation|update_release|update_installer)$'
|
||||
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
|
||||
|
||||
- name: Install
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -51,15 +51,6 @@ add_custom_target(stage_updater
|
|||
VERBATIM)
|
||||
add_dependencies(launcher kyty_emulator stage_updater)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(update_release_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/UpdateReleaseTests.cpp" src/updateRelease.cpp)
|
||||
target_include_directories(update_release_tests PRIVATE include)
|
||||
target_link_libraries(update_release_tests PRIVATE Qt6::Core)
|
||||
add_test(NAME update_release COMMAND update_release_tests)
|
||||
add_dependencies(kyty_tests update_release_tests)
|
||||
endif()
|
||||
|
||||
if(KYTY_CLANG_CL)
|
||||
add_custom_command(TARGET launcher POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_PDB_FILE:kyty_emulator> $<TARGET_FILE_DIR:launcher>/$<TARGET_PDB_FILE_NAME:kyty_emulator>
|
||||
|
|
|
|||
|
|
@ -3,24 +3,13 @@
|
|||
if(MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
endif()
|
||||
add_library(update_installer STATIC UpdateInstaller.cpp)
|
||||
target_include_directories(update_installer PUBLIC "${KYTY_SOURCE_DIR}")
|
||||
add_executable(kyty_updater main.cpp)
|
||||
target_link_libraries(kyty_updater PRIVATE update_installer)
|
||||
add_executable(kyty_updater main.cpp UpdateInstaller.cpp)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(update_installer PUBLIC /EHsc)
|
||||
target_compile_options(kyty_updater PRIVATE /EHsc)
|
||||
else()
|
||||
target_compile_options(update_installer PUBLIC -fexceptions)
|
||||
target_compile_options(kyty_updater PRIVATE -fexceptions)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_link_options(kyty_updater PRIVATE -static -municode)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(update_installer_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/UpdateInstallerTests.cpp")
|
||||
target_link_libraries(update_installer_tests PRIVATE update_installer)
|
||||
add_dependencies(update_installer_tests kyty_updater)
|
||||
add_test(NAME update_installer COMMAND update_installer_tests $<TARGET_FILE:kyty_updater>)
|
||||
add_dependencies(kyty_tests update_installer_tests)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -1,335 +0,0 @@
|
|||
#include "updater/UpdateInstaller.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
namespace fs = std::filesystem;
|
||||
using Kyty::Updater::Install;
|
||||
using Kyty::Updater::Validate;
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "UpdateInstallerTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
std::string Read(const fs::path& path) {
|
||||
std::ifstream input(path);
|
||||
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
void Write(const fs::path& path, const std::string& content) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream(path) << content;
|
||||
fs::permissions(path, fs::perms::owner_all, fs::perm_options::add);
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
fs::path root = fs::temp_directory_path() /
|
||||
("kyty-updater-test-" +
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
fs::path source = root / "release";
|
||||
fs::path target = root / "installed";
|
||||
#ifdef _WIN32
|
||||
fs::path launcher = "launcher.exe";
|
||||
const char* emulator = "kyty_emulator.exe";
|
||||
const char* updater = "kyty_updater.exe";
|
||||
#else
|
||||
fs::path launcher = "launcher";
|
||||
const char* emulator = "kyty_emulator";
|
||||
const char* updater = "kyty_updater";
|
||||
#endif
|
||||
Fixture() {
|
||||
fs::create_directories(target);
|
||||
Release();
|
||||
Write(target / launcher, "old launcher");
|
||||
}
|
||||
void Release() {
|
||||
Write(source / launcher, "new launcher");
|
||||
Write(source / emulator, "new emulator");
|
||||
Write(source / updater, "new updater");
|
||||
}
|
||||
~Fixture() {
|
||||
std::error_code ignored;
|
||||
fs::remove_all(root, ignored);
|
||||
}
|
||||
};
|
||||
|
||||
void TestOverlayPreservesUserFiles() {
|
||||
Fixture fixture;
|
||||
Write(fixture.target / "_SaveData" / "save.dat", "valuable save");
|
||||
Write(fixture.target / "config.ini", "user settings");
|
||||
Write(fixture.source / "lib" / "runtime", "new runtime");
|
||||
Check(Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"valid release rejected");
|
||||
Check(Install(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"valid release failed to install");
|
||||
Check(Read(fixture.target / fixture.launcher) == "new launcher", "launcher was not updated");
|
||||
Check(!fs::exists(fixture.source / fixture.launcher),
|
||||
"update copied rather than moved its staged executable");
|
||||
Check(Read(fixture.target / "lib" / "runtime") == "new runtime",
|
||||
"nested runtime was not installed");
|
||||
Check(Read(fixture.target / "_SaveData" / "save.dat") == "valuable save",
|
||||
"savedata was removed");
|
||||
Check(Read(fixture.target / "config.ini") == "user settings", "settings were overwritten");
|
||||
for (const auto& entry: fs::directory_iterator(fixture.target)) {
|
||||
Check(!entry.path().filename().string().starts_with(".kyty-update"),
|
||||
"successful update left a backup");
|
||||
}
|
||||
}
|
||||
|
||||
void TestExclusiveInstallLock() {
|
||||
Fixture fixture;
|
||||
fs::create_directory(fixture.target / ".kyty-update-lock");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"validation ignored existing install lock");
|
||||
const auto result = Install(fixture.source, fixture.target, fixture.launcher);
|
||||
Check(!result.success, "concurrent install lock was ignored");
|
||||
Check(Read(fixture.target / fixture.launcher) == "old launcher",
|
||||
"locked update changed installed files");
|
||||
Check(fs::exists(fixture.target / ".kyty-update-lock"), "another updater's lock was removed");
|
||||
}
|
||||
|
||||
void TestValidation() {
|
||||
Fixture fixture;
|
||||
Check(!Validate(fixture.source, fixture.target, "../launcher").success,
|
||||
"parent traversal accepted");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.source / fixture.launcher).success,
|
||||
"absolute launcher accepted");
|
||||
Check(!Validate(fixture.source, fixture.source, fixture.launcher).success,
|
||||
"overlapping source accepted");
|
||||
fs::remove(fixture.source / fixture.updater);
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"missing updater accepted");
|
||||
Write(fixture.source / fixture.updater, "updater");
|
||||
Write(fixture.source / ".kyty-update-result", "spoofed result");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"reserved updater file accepted");
|
||||
fs::remove(fixture.source / ".kyty-update-result");
|
||||
Write(fixture.target / "lib", "user file");
|
||||
Write(fixture.source / "lib" / "runtime", "runtime");
|
||||
Check(!Install(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"directory/file collision accepted");
|
||||
Check(Read(fixture.target / fixture.launcher) == "old launcher",
|
||||
"validation failure changed existing launcher");
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
void TestSymlinks() {
|
||||
Fixture fixture;
|
||||
Write(fixture.source / "lib" / "runtime.2", "runtime v2");
|
||||
fs::create_symlink("runtime.2", fixture.source / "lib" / "runtime");
|
||||
const auto first = Install(fixture.source, fixture.target, fixture.launcher);
|
||||
if (!first.success) {
|
||||
std::fprintf(stderr, "%s\n", first.message.c_str());
|
||||
}
|
||||
Check(first.success, "internal release symlink rejected");
|
||||
Check(fs::is_symlink(fixture.target / "lib" / "runtime"), "release symlink was dereferenced");
|
||||
Check(Read(fixture.target / "lib" / "runtime") == "runtime v2",
|
||||
"installed symlink target is wrong");
|
||||
fixture.Release();
|
||||
Write(fixture.source / "lib" / "runtime.2", "runtime v3");
|
||||
fs::create_symlink("runtime.2", fixture.source / "lib" / "runtime");
|
||||
Check(Install(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"second update rejected existing internal link");
|
||||
fixture.Release();
|
||||
fs::create_symlink("../../installed/launcher", fixture.source / "lib" / "escape");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"escaping source symlink accepted");
|
||||
fs::remove(fixture.source / "lib" / "escape");
|
||||
fs::create_symlink(fixture.source / fixture.launcher, fixture.source / "absolute-link");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"absolute source symlink accepted");
|
||||
fs::remove(fixture.source / "absolute-link");
|
||||
fs::remove_all(fixture.target / "lib");
|
||||
fs::create_directory(fixture.root / "user-data");
|
||||
fs::create_directory_symlink(fixture.root / "user-data", fixture.target / "lib");
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"destination symlink directory accepted");
|
||||
Check(fs::is_empty(fixture.root / "user-data"), "validation wrote through destination symlink");
|
||||
}
|
||||
|
||||
void TestExecutePermissions() {
|
||||
Fixture fixture;
|
||||
fs::permissions(fixture.source / fixture.launcher,
|
||||
fs::perms::owner_read | fs::perms::owner_write);
|
||||
Check(!Validate(fixture.source, fixture.target, fixture.launcher).success,
|
||||
"non-executable launcher accepted");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void TestRollback() {
|
||||
#ifndef _WIN32
|
||||
// A root test runner bypasses file permissions; normal users exercise a real
|
||||
// rename failure after the launcher and a new nested file have been
|
||||
// installed.
|
||||
if (geteuid() == 0) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Fixture fixture;
|
||||
Write(fixture.source / "a-new" / "runtime", "new file");
|
||||
Write(fixture.source / "z-locked" / "runtime", "new library");
|
||||
Write(fixture.target / "z-locked" / "runtime", "old library");
|
||||
#ifdef _WIN32
|
||||
HANDLE locked_file =
|
||||
CreateFileW((fixture.target / "z-locked" / "runtime").c_str(), GENERIC_READ,
|
||||
FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
Check(locked_file != INVALID_HANDLE_VALUE, "could not lock destination for rollback test");
|
||||
#else
|
||||
fs::permissions(fixture.target / "z-locked", fs::perms::owner_read | fs::perms::owner_exec);
|
||||
#endif
|
||||
const auto result = Install(fixture.source, fixture.target, fixture.launcher);
|
||||
#ifdef _WIN32
|
||||
CloseHandle(locked_file);
|
||||
#else
|
||||
fs::permissions(fixture.target / "z-locked", fs::perms::owner_all);
|
||||
#endif
|
||||
Check(!result.success, "locked destination did not fail the transaction");
|
||||
Check(Read(fixture.source / fixture.launcher) == "new launcher",
|
||||
"rollback did not restore the staged release");
|
||||
Check(Read(fixture.target / fixture.launcher) == "old launcher",
|
||||
"rollback did not restore the launcher");
|
||||
Check(Read(fixture.target / "z-locked" / "runtime") == "old library",
|
||||
"rollback damaged an unchanged file");
|
||||
Check(!fs::exists(fixture.target / "a-new"), "rollback retained newly installed directories");
|
||||
Check(!fs::exists(fixture.target / fixture.emulator),
|
||||
"rollback retained a newly installed executable");
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
void TestCommandLine(const fs::path& helper) {
|
||||
for (int scenario = 0; scenario < 3; ++scenario) {
|
||||
Fixture fixture;
|
||||
const auto workspace = fixture.target / ".kyty-update-test";
|
||||
fs::create_directory(workspace);
|
||||
fs::rename(fixture.source, workspace / "payload");
|
||||
fixture.source = workspace / "payload";
|
||||
const auto original_cwd = fixture.root / "original working directory";
|
||||
fs::create_directory(original_cwd);
|
||||
const std::string old_launcher =
|
||||
"#!/bin/sh\nprintf 'old\\n' > restarted-version\npwd > restarted-cwd\n";
|
||||
Write(fixture.target / fixture.launcher, old_launcher);
|
||||
Write(fixture.source / fixture.launcher,
|
||||
"#!/bin/sh\nprintf 'new\\n' > restarted-version\npwd > "
|
||||
"restarted-cwd\n");
|
||||
if (scenario == 1) {
|
||||
// Invalid payload exercises the CLI's failed-install/old-launcher path.
|
||||
fs::remove(fixture.source / fixture.updater);
|
||||
} else if (scenario == 2) {
|
||||
Write(fixture.target / ".kyty-update-result" / "blocker", "keep");
|
||||
}
|
||||
int parent_pipe[2];
|
||||
Check(pipe(parent_pipe) == 0, "cannot create parent synchronization pipe");
|
||||
const auto parent = fork();
|
||||
Check(parent >= 0, "cannot fork simulated launcher");
|
||||
if (parent == 0) {
|
||||
close(parent_pipe[1]);
|
||||
char signal;
|
||||
static_cast<void>(read(parent_pipe[0], &signal, 1));
|
||||
_exit(0);
|
||||
}
|
||||
close(parent_pipe[0]);
|
||||
const auto parent_pid = std::to_string(parent);
|
||||
const auto updater = fork();
|
||||
Check(updater >= 0, "cannot fork update helper");
|
||||
if (updater == 0) {
|
||||
close(parent_pipe[1]);
|
||||
execl(helper.c_str(), helper.c_str(), "--install", fixture.source.c_str(),
|
||||
fixture.target.c_str(), fixture.launcher.c_str(), parent_pid.c_str(),
|
||||
original_cwd.c_str(), static_cast<char*>(nullptr));
|
||||
_exit(127);
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
int status = 0;
|
||||
Check(waitpid(updater, &status, WNOHANG) == 0, "helper exited before its parent");
|
||||
Check(Read(fixture.target / fixture.launcher) == old_launcher,
|
||||
"helper updated a running launcher");
|
||||
close(parent_pipe[1]);
|
||||
waitpid(parent, nullptr, 0);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
pid_t finished;
|
||||
while ((finished = waitpid(updater, &status, WNOHANG)) == 0 &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
if (finished == 0) {
|
||||
kill(updater, SIGKILL);
|
||||
waitpid(updater, nullptr, 0);
|
||||
}
|
||||
Check(finished == updater && WIFEXITED(status) &&
|
||||
WEXITSTATUS(status) == (scenario == 1 ? 1 : 0),
|
||||
"helper did not complete with the expected status");
|
||||
while (Read(original_cwd / "restarted-cwd").empty() &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
Check(Read(original_cwd / "restarted-version") == (scenario == 1 ? "old\n" : "new\n"),
|
||||
"wrong launcher restarted");
|
||||
Check(Read(original_cwd / "restarted-cwd") == fs::canonical(original_cwd).string() + "\n",
|
||||
"restart changed the working directory");
|
||||
if (scenario != 2) {
|
||||
Check(Read(fixture.target / ".kyty-update-result")
|
||||
.starts_with(scenario == 1 ? "failure\n" : "success\n"),
|
||||
"helper did not record the update result");
|
||||
}
|
||||
if (scenario != 1) {
|
||||
Check(!fs::exists(workspace), "helper did not clean the successful download");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestMacBundleScope() {
|
||||
Fixture fixture;
|
||||
const fs::path launcher = "KytyPS5.app/Contents/MacOS/KytyPS5";
|
||||
Write(fixture.source / launcher, "new mac launcher");
|
||||
Write(fixture.source / launcher.parent_path() / "kyty_emulator", "new mac emulator");
|
||||
Write(fixture.source / launcher.parent_path() / "kyty_updater", "new mac updater");
|
||||
Write(fixture.target / launcher, "old mac launcher");
|
||||
Write(fixture.target / launcher.parent_path() / "_SaveData" / "save", "mac save");
|
||||
Write(fixture.source / "unrelated.txt", "must stay outside installation");
|
||||
Check(Install(fixture.source, fixture.target, launcher).success, "mac bundle update failed");
|
||||
Check(Read(fixture.target / launcher) == "new mac launcher", "mac launcher not replaced");
|
||||
Check(Read(fixture.target / launcher.parent_path() / "_SaveData" / "save") == "mac save",
|
||||
"mac savedata was removed");
|
||||
Check(!fs::exists(fixture.target / "unrelated.txt"), "mac update installed archive siblings");
|
||||
Check(Read(fixture.target / fixture.launcher) == "old launcher",
|
||||
"mac update changed flat launcher sibling");
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
TestOverlayPreservesUserFiles();
|
||||
TestValidation();
|
||||
TestExclusiveInstallLock();
|
||||
TestRollback();
|
||||
#ifndef _WIN32
|
||||
TestSymlinks();
|
||||
TestExecutePermissions();
|
||||
TestMacBundleScope();
|
||||
if (argc == 2) {
|
||||
TestCommandLine(fs::absolute(argv[1]));
|
||||
}
|
||||
#else
|
||||
static_cast<void>(argc);
|
||||
static_cast<void>(argv);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
#include "updateRelease.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
|
||||
const QUrl EXPECTED_PAGE_URL(
|
||||
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/KytyPS5-2026-10-10-abcdef0"));
|
||||
const QByteArray EXPECTED_DOWNLOAD_URL =
|
||||
"https://github.com/KytyPS5/KytyPS5/releases/download/v1%2Fstable%2Btest/"
|
||||
"v1%2Fstable%2Btest-Linux-x86_64.tar.gz";
|
||||
|
||||
const QString TAG = QStringLiteral("KytyPS5-2026-10-10-abcdef0");
|
||||
const QString SUFFIX = QStringLiteral("-Linux-x86_64.tar.gz");
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "UpdateReleaseTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject Release(const QString& suffix = SUFFIX, const QString& tag = TAG) {
|
||||
const QJsonObject asset {
|
||||
{QStringLiteral("name"), tag + suffix},
|
||||
{QStringLiteral("size"), 1234},
|
||||
{QStringLiteral("digest"), QStringLiteral("sha256:") + QString(64, QLatin1Char('a'))}};
|
||||
return {{QStringLiteral("tag_name"), tag},
|
||||
{QStringLiteral("published_at"), QStringLiteral("2026-10-10T12:00:00Z")},
|
||||
{QStringLiteral("draft"), false},
|
||||
{QStringLiteral("prerelease"), false},
|
||||
{QStringLiteral("assets"), QJsonArray {asset}}};
|
||||
}
|
||||
|
||||
UpdateRelease::Info Parse(const QJsonObject& root, const QString& suffix = SUFFIX) {
|
||||
return UpdateRelease::Parse(QJsonDocument(root).toJson(QJsonDocument::Compact), suffix);
|
||||
}
|
||||
|
||||
void CheckInvalidAsset(const QString& key, const QJsonValue& value) {
|
||||
auto root = Release();
|
||||
auto asset = root.value(QStringLiteral("assets")).toArray().first().toObject();
|
||||
asset.insert(key, value);
|
||||
root.insert(QStringLiteral("assets"), QJsonArray {asset});
|
||||
const auto info = Parse(root);
|
||||
Check(!info.error.isEmpty() && info.download_url.isEmpty() && info.sha256.isEmpty(),
|
||||
"invalid asset was accepted");
|
||||
Check(info.tag == TAG && !info.page_url.isEmpty() && info.published_at.isValid(),
|
||||
"invalid package discarded valid release metadata");
|
||||
}
|
||||
|
||||
void TestParsing() {
|
||||
for (const auto& suffix:
|
||||
{SUFFIX, QStringLiteral("-Windows-x64.zip"), QStringLiteral("-macOS-x86_64.zip")}) {
|
||||
const auto info = Parse(Release(suffix), suffix);
|
||||
Check(info.error.isEmpty() && info.tag == TAG && info.size == 1234,
|
||||
"valid platform package was rejected");
|
||||
Check(info.sha256 == QByteArray(32, static_cast<char>(0xaa)), "checksum was not decoded");
|
||||
Check(info.download_url.fileName() == TAG + suffix, "wrong asset was selected");
|
||||
Check(info.page_url == EXPECTED_PAGE_URL,
|
||||
"release page was not derived from the official repository");
|
||||
}
|
||||
const auto encoded = Parse(Release(SUFFIX, QStringLiteral("v1/stable+test")));
|
||||
Check(encoded.download_url.toEncoded() == EXPECTED_DOWNLOAD_URL,
|
||||
"release tag and asset name were not encoded as URL segments");
|
||||
Check(!UpdateRelease::Parse("not JSON", SUFFIX).error.isEmpty(), "malformed JSON was accepted");
|
||||
Check(!UpdateRelease::Parse("[]", SUFFIX).error.isEmpty(), "JSON array was accepted");
|
||||
Check(!Parse(Release(), {}).error.isEmpty(), "unsupported platform was accepted");
|
||||
Check(!Parse(Release(), QStringLiteral("-Windows-x64.zip")).error.isEmpty(),
|
||||
"missing asset was accepted");
|
||||
|
||||
for (const auto& key: {QStringLiteral("draft"), QStringLiteral("prerelease")}) {
|
||||
auto root = Release();
|
||||
root.insert(key, true);
|
||||
Check(!Parse(root).error.isEmpty(), "draft or prerelease was accepted");
|
||||
}
|
||||
for (const auto& field: {QStringLiteral("tag_name"), QStringLiteral("published_at")}) {
|
||||
auto root = Release();
|
||||
root.remove(field);
|
||||
Check(!Parse(root).error.isEmpty(), "missing release metadata was accepted");
|
||||
}
|
||||
|
||||
for (const auto& digest: {QString(), QStringLiteral("sha256:abc"),
|
||||
QStringLiteral("sha256:") + QString(64, QLatin1Char('z'))}) {
|
||||
CheckInvalidAsset(QStringLiteral("digest"), digest);
|
||||
}
|
||||
for (const double size: {-1., 0., 1.5}) {
|
||||
CheckInvalidAsset(QStringLiteral("size"), size);
|
||||
}
|
||||
CheckInvalidAsset(QStringLiteral("name"), QStringLiteral("another-release") + SUFFIX);
|
||||
}
|
||||
|
||||
void TestOrdering() {
|
||||
using UpdateRelease::Compare;
|
||||
using UpdateRelease::VersionRelation;
|
||||
const auto latest = Parse(Release());
|
||||
auto current = latest;
|
||||
Check(Compare(latest, current) == VersionRelation::Current, "matching tags were not current");
|
||||
current.tag = QStringLiteral("KytyPS5-2026-10-10-1234567");
|
||||
Check(Compare(latest, current) == VersionRelation::Unknown,
|
||||
"equal timestamps implied an update");
|
||||
current.published_at = latest.published_at.addSecs(-60);
|
||||
Check(Compare(latest, current) == VersionRelation::Newer, "same-day newer release was missed");
|
||||
current.published_at = latest.published_at.addSecs(60);
|
||||
Check(Compare(latest, current) == VersionRelation::Older,
|
||||
"older release would cause a downgrade");
|
||||
current.published_at = {};
|
||||
Check(Compare(latest, current) == VersionRelation::Unknown,
|
||||
"missing current metadata implied an update");
|
||||
Check(Compare(latest, {}) == VersionRelation::Unknown,
|
||||
"empty current release implied an update");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestParsing();
|
||||
TestOrdering();
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue