tests: add LeastRecentlyUsedCache regression tests (#671)

* launcher: keep primary update feed answer when the fallback request fails

When the primary feed is reachable but its tag differs from the build tag,
the result was discarded and the GitHub API fallback was consulted. If the
fallback request failed (api.github.com rate-limits unauthenticated
requests), the launcher reported an error even though the primary feed had
already returned a usable update answer with a working release URL. Keep
the primary feed answer and show it if the fallback fails.

* emulator: validate numeric CLI arguments

--screen-width, --screen-height, --gpu, and --vblank-frequency were parsed
with Common::ToInt32/ToInt64, which ignores std::from_chars errors and
returns 0, so values like "--screen-width abc" or "--gpu xyz" were
silently accepted: zero screen size, or GPU 0 selected instead of auto
(-1). Reject invalid values with an error message like the other options,
using from_chars-based helpers (ParseUint32/ParseInt32); reject zero
screen dimensions.

* tests: add LeastRecentlyUsedCache regression tests

Cover the LRU cache walk order, tick filtering in ForEachItemBelow,
early-exit boolean callbacks, Touch reordering and same-tick skips,
Free, and freed-id reuse by later inserts. Modeled on BitArrayTests.

* tests: use a tick cutoff that detects a stale newest-item tick

Collect(cache, 35) also visits c when Touch(c, 35) failed to update
its tick (30 <= 35). Use a cutoff of 30 and expect only b.

* Run LRU cache tests in CI

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Saadi
2026-09-25 23:42:31 +01:00
committed by GitHub
parent 1e438136fa
commit 4e8d8c849d
6 changed files with 236 additions and 16 deletions
+7
View File
@@ -3,6 +3,8 @@
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
class QByteArray;
class QWidget;
@@ -29,6 +31,11 @@ private:
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
};
#endif // UPDATE_CHECKER_H
+22 -4
View File
@@ -84,12 +84,30 @@ void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual)
}
reply->deleteLater();
if (!fallback &&
(!info.error.isEmpty() || info.tag != QString::fromLatin1(KYTY_RELEASE_TAG))) {
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
+38 -6
View File
@@ -133,6 +133,26 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) {
return true;
}
static bool ParseUint32(const std::string& value, uint32_t& out) {
uint32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseInt32(const std::string& value, int32_t& out) {
int32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseUserId(const std::string& value, int32_t& out) {
int32_t user_id = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id);
@@ -246,9 +266,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
options.game_patch = path;
} else if (arg == "--screen-width") {
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_width) ||
options.config.screen_width == 0) {
::printf("invalid screen width: %s\n", value.c_str());
return false;
}
} else if (arg == "--screen-height") {
options.config.screen_height = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_height) ||
options.config.screen_height == 0) {
::printf("invalid screen height: %s\n", value.c_str());
return false;
}
} else if (arg == "--user-name") {
if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) {
::printf("invalid user name: must contain 1-%zu bytes\n",
@@ -269,11 +297,15 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
} else if (arg == "--gpu") {
options.config.gpu_index = Common::ToInt32(value);
if (!ParseInt32(value, options.config.gpu_index)) {
::printf("invalid gpu index: %s\n", value.c_str());
return false;
}
} else if (arg == "--vblank-frequency") {
const int32_t vblank_frequency = Common::ToInt32(value);
options.config.vblank_frequency =
static_cast<uint32_t>(vblank_frequency < 0 ? 0 : vblank_frequency);
if (!ParseUint32(value, options.config.vblank_frequency)) {
::printf("invalid vblank frequency: %s\n", value.c_str());
return false;
}
} else if (arg == "--console-language") {
if (!ParseConsoleLanguage(value, options.config.console_language)) {
::printf("invalid console language: %s\n", value.c_str());