ZaStoGram_desktop/Telegram/SourceFiles/test/test_agent.cpp
John Preston 65e23ba713 [ai] Hold an App Nap opt-out for the whole macOS test run
Task: 2026/10/09/keep-background-testagent-runs-on-macos-unthrottled-and-painting
2026-10-09 23:29:29 +04:00

424 lines
11 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "test/test_agent.h"
#ifdef _DEBUG
#include "test/test_log.h"
#include "ui/style/style_core_scale.h"
#include "wallet/wallet_address.h"
#include "settings.h"
#include <QtCore/QFile>
#include <QtCore/QSaveFile>
#ifdef Q_OS_MAC
// Keep last: <objc/objc.h> defines id, Class, BOOL, YES, NO, nil and Nil.
#include <objc/message.h>
#include <objc/runtime.h>
#endif // Q_OS_MAC
namespace Test {
namespace {
const auto kGramAccountFile = u"test_gram_account.txt"_q;
[[nodiscard]] QString LivePath(const QString &name) {
return cWorkingDir() + name;
}
[[nodiscard]] QString GoldenPath(const QString &name) {
return cExeDir() + u"test_TelegramForcePortable/"_q + name;
}
[[nodiscard]] std::optional<QString> ReadTrimmed(const QString &path) {
auto file = QFile(path);
if (!file.open(QIODevice::ReadOnly)) {
return std::nullopt;
}
const auto value = QString::fromUtf8(file.readAll()).trimmed();
if (value.isEmpty()) {
return std::nullopt;
}
return value;
}
[[nodiscard]] std::optional<GramAccountFixture> ParseGramAccount(
const QString &raw) {
auto result = GramAccountFixture();
auto wordsEnded = false;
for (const auto &line : raw.split(u'\n')) {
const auto trimmed = line.trimmed();
if (!wordsEnded) {
if (trimmed.isEmpty()) {
wordsEnded = true;
} else {
result.words.push_back(trimmed);
}
} else if (!trimmed.isEmpty()) {
result.address = trimmed;
break;
}
}
if (result.words.empty() || result.address.isEmpty()) {
return std::nullopt;
}
return result;
}
[[nodiscard]] bool IsWordLine(const QString &word) {
if (word.isEmpty()) {
return false;
}
for (const auto ch : word) {
if (ch.isSpace()) {
return false;
}
}
return true;
}
[[nodiscard]] bool RewriteExisting(
const QString &path,
const QByteArray &content) {
if (!QFile::exists(path)) {
return false;
}
auto file = QSaveFile(path);
return file.open(QIODevice::WriteOnly)
&& (file.write(content) == content.size())
&& file.commit();
}
[[nodiscard]] bool RewriteCopyWords(
const QString &path,
const QStringList &lines,
const QString &addressRaw) {
const auto raw = ReadTrimmed(path);
const auto current = raw ? ParseGramAccount(*raw) : std::nullopt;
if (!current
|| Wallet::CanonicalAddress(current->address) != addressRaw) {
return false;
}
const auto content = (lines.join(u'\n')
+ u"\n\n"_q
+ current->address
+ u"\n"_q).toUtf8();
return RewriteExisting(path, content);
}
[[nodiscard]] base::flat_set<QString> &FiredEvents() {
static auto result = base::flat_set<QString>();
return result;
}
#ifdef Q_OS_MAC
// NSActivityOptions values from Foundation's NSProcessInfo.h, which a C++
// unit cannot include. NSActivityUserInitiatedAllowingIdleSystemSleep is
// NSActivityUserInitiated without NSActivityIdleSystemSleepDisabled, so idle
// system sleep stays allowed; NSActivityLatencyCritical asks for the highest
// timer and I/O precision available. Held together, they keep a background
// client from being napped and its timers from being coalesced.
// kAppNapHoldOptions is 0xFF00EFFFFF, the SDK's
// NSActivityUserInitiatedAllowingIdleSystemSleep | NSActivityLatencyCritical.
constexpr auto kActivityUserInitiatedAllowingIdleSystemSleep = 0x00EFFFFFULL;
constexpr auto kActivityLatencyCritical = 0xFF00000000ULL;
constexpr auto kAppNapHoldOptions
= kActivityUserInitiatedAllowingIdleSystemSleep
| kActivityLatencyCritical;
// The -beginActivityWithOptions:reason: token of the harness hold, retained
// and never ended or released, so the activity lasts until the process
// exits: normal completion, a stage timeout, the watchdog, the quit fuse, a
// test-run kill and a crash alike. Ending it in Runner::onFinish would end it
// before the drain and the quit teardown.
auto AppNapActivity = (void*)nullptr;
// Runs on the main thread inside the Cocoa event loop (the InvokeQueued of
// Sandbox::launchApplication() that calls Application::run()), so the event
// loop's pool owns the NSString and the token as they are returned; the token
// outlives that pool because it is retained here. Returns an empty string
// once the activity is held, otherwise why it is not.
[[nodiscard]] QString BeginAppNapActivity() {
using SendObject = void*(*)(void*, SEL);
using SendString = void*(*)(void*, SEL, const char*);
using SendBegin = void*(*)(void*, SEL, unsigned long long, void*);
const auto cls = objc_getClass("NSProcessInfo");
if (!cls) {
return u"NSProcessInfo is unavailable"_q;
}
const auto info = reinterpret_cast<SendObject>(objc_msgSend)(
cls,
sel_registerName("processInfo"));
const auto reason = reinterpret_cast<SendString>(objc_msgSend)(
objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"),
"Telegram Desktop -testagent run");
const auto activity = reinterpret_cast<SendBegin>(objc_msgSend)(
info,
sel_registerName("beginActivityWithOptions:reason:"),
kAppNapHoldOptions,
reason);
if (!activity) {
return u"beginActivityWithOptions:reason: returned nil"_q;
}
reinterpret_cast<SendObject>(objc_msgSend)(
activity,
sel_registerName("retain"));
AppNapActivity = activity;
return QString();
}
// TDESKTOP_TEST_APP_NAP=allow leaves the hold off, for a campaign whose
// subject is background scheduling itself; any other non-empty value is
// rejected and the hold is kept. Writes exactly one reading row.
void ApplyAppNapHold() {
const auto value = qEnvironmentVariable("TDESKTOP_TEST_APP_NAP");
auto source = u"default"_q;
auto optOut = false;
if (!value.isEmpty()) {
if (value == u"allow"_q) {
optOut = true;
source = u"environment"_q;
} else {
Note(u"TDESKTOP_TEST_APP_NAP rejected: %1"_q.arg(value));
}
}
const auto failure = optOut ? QString() : BeginAppNapActivity();
const auto held = !optOut && failure.isEmpty();
const auto reason = optOut
? u"opted out: macOS may nap this background client"
" and coalesce its timers"_q
: held
? u"NSProcessInfo activity"
" NSActivityUserInitiatedAllowingIdleSystemSleep"
" | NSActivityLatencyCritical (options 0x%1)"
" held until the process exits"_q.arg(
QString::number(kAppNapHoldOptions, 16))
: failure;
const auto report = u"TDESKTOP_TEST_APP_NAP=[%1] applied: "
"hold=%2 source=%3 - %4"_q.arg(
value,
held ? u"active"_q : u"off"_q,
source,
reason);
Note(report);
}
#endif // Q_OS_MAC
} // namespace
bool Active() {
return cTestAgent();
}
void ApplyStartupOverrides() {
if (!Active()) {
return;
}
const auto value = qEnvironmentVariable("TDESKTOP_TEST_SCALE");
auto selectedScale = style::kScaleDefault;
auto source = u"default"_q;
if (!value.isEmpty()) {
auto ok = false;
const auto scale = value.toInt(&ok);
if (ok && scale >= style::kScaleMin && scale <= style::kScaleMax) {
selectedScale = style::CheckScale(scale);
source = u"environment"_q;
} else {
Note(u"TDESKTOP_TEST_SCALE rejected: %1"_q.arg(value));
}
}
cSetConfigScale(selectedScale);
const auto report = u"TDESKTOP_TEST_SCALE=[%1] applied: %2 source=%3"_q
.arg(
value,
QString::number(selectedScale),
source);
Note(report);
#ifdef Q_OS_MAC
ApplyAppNapHold();
#endif // Q_OS_MAC
}
void Fire(const QString &event) {
if (!Active() || !FiredEvents().emplace(event).second) {
return;
}
Note(u"event fired: %1"_q.arg(event));
}
bool HasFired(const QString &event) {
return Active() && FiredEvents().contains(event);
}
std::optional<QString> FixtureSecret(const QString &name) {
if (!Active()) {
return std::nullopt;
} else if (auto live = ReadTrimmed(LivePath(name))) {
return live;
}
return ReadTrimmed(GoldenPath(name));
}
std::optional<QString> TwoStepPassword() {
return FixtureSecret(u"2svpassword.txt"_q);
}
QString GramAccountLivePath() {
return LivePath(kGramAccountFile);
}
QString GramAccountGoldenPath() {
return GoldenPath(kGramAccountFile);
}
std::optional<GramAccountFixture> GramAccount() {
const auto raw = FixtureSecret(kGramAccountFile);
if (!raw) {
return std::nullopt;
}
auto result = ParseGramAccount(*raw);
if (result) {
result->addressRaw = Wallet::CanonicalAddress(result->address);
}
return result;
}
GramAccountRewrite RewriteGramAccountWords(
const std::vector<QString> &words,
const QString &addressRaw) {
auto result = GramAccountRewrite();
if (!Active()
|| words.empty()
|| addressRaw.isEmpty()
|| !ranges::all_of(words, IsWordLine)) {
return result;
}
const auto lines = QStringList(words.begin(), words.end());
result.live = RewriteCopyWords(GramAccountLivePath(), lines, addressRaw);
result.golden = RewriteCopyWords(
GramAccountGoldenPath(),
lines,
addressRaw);
return result;
}
bool StageGramAccountLiveWords(
const std::vector<QString> &prefix,
const QString &addressRaw) {
if (!Active()
|| prefix.empty()
|| addressRaw.isEmpty()
|| !ranges::all_of(prefix, IsWordLine)) {
return false;
}
const auto path = GramAccountLivePath();
const auto raw = ReadTrimmed(path);
const auto current = raw ? ParseGramAccount(*raw) : std::nullopt;
if (!current
|| Wallet::CanonicalAddress(current->address) != addressRaw
|| prefix.size() >= current->words.size()
|| !std::equal(
prefix.begin(),
prefix.end(),
current->words.begin())) {
return false;
}
const auto lines = QStringList(prefix.begin(), prefix.end());
return RewriteCopyWords(path, lines, addressRaw);
}
bool RestoreGramAccountLiveWords(
const std::vector<QString> &words,
const QString &addressRaw) {
if (!Active()
|| words.empty()
|| addressRaw.isEmpty()
|| !ranges::all_of(words, IsWordLine)) {
return false;
}
const auto path = GramAccountLivePath();
const auto raw = ReadTrimmed(path);
const auto current = raw ? ParseGramAccount(*raw) : std::nullopt;
if (!current
|| Wallet::CanonicalAddress(current->address) != addressRaw
|| current->words.size() >= words.size()
|| !std::equal(
current->words.begin(),
current->words.end(),
words.begin())) {
return false;
}
const auto lines = QStringList(words.begin(), words.end());
return RewriteCopyWords(path, lines, addressRaw);
}
} // namespace Test
#else // _DEBUG
namespace Test {
bool Active() {
return false;
}
void ApplyStartupOverrides() {
}
void Fire(const QString &) {
}
bool HasFired(const QString &) {
return false;
}
std::optional<QString> FixtureSecret(const QString &) {
return std::nullopt;
}
std::optional<QString> TwoStepPassword() {
return std::nullopt;
}
std::optional<GramAccountFixture> GramAccount() {
return std::nullopt;
}
QString GramAccountLivePath() {
return QString();
}
QString GramAccountGoldenPath() {
return QString();
}
GramAccountRewrite RewriteGramAccountWords(
const std::vector<QString> &,
const QString &) {
return {};
}
bool StageGramAccountLiveWords(
const std::vector<QString> &,
const QString &) {
return false;
}
bool RestoreGramAccountLiveWords(
const std::vector<QString> &,
const QString &) {
return false;
}
} // namespace Test
#endif // _DEBUG