Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/drift-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,49 @@ jobs:
- name: Self-test the AUTOMOC-include checker
run: bash scripts/test_check_automoc_includes.sh

# ── Q_OBJECT headers vs. the source lists that get them mocced (#659) ──
# The complement of the AUTOMOC-include gate above: that one checks the
# includes moc *writes*, this one checks that moc runs on the header at all.
#
# AUTOMOC finds a Q_OBJECT header two ways -- beside a translation unit of
# the same basename, or named in a target's own source list -- and when
# neither holds it silently generates nothing. The .cpp compiles, the static
# library archives, and the first signal is a linker error about a missing
# vtable, in every leg that links the target. On #657 that was six red legs
# at once, the fastest at 4m03s.
#
# The repository had already met this, diagnosed it and written it down, in
# cmake/morph_add_rung.cmake's own comment on its _lib_headers glob
# ("pastebin::app::App, hit the moment ladder_pastebin_tests linked it").
# #652 then hit it again in a different CMakeLists, because nothing reads a
# comment in a file you are not editing. That is the argument for the gate:
# a known failure mode with no control recurs on schedule.
#
# It belongs in this file rather than behind a build because it checks the
# *pairing*, not the moc output. A build tree only covers what its configure
# enabled, so an output check would false-positive on every header behind an
# off-by-default option; a pairing check is configure-independent, needs no
# Qt and no compiler, and runs in under a second.
#
# The self-test runs first and drives the mutation the issue named as its
# close condition -- testkit/fault_proxy.hpp removed from
# morph_ladder_testkit's source list, against a copy of the real
# examples/common/CMakeLists.txt -- because the tree is clean today, so this
# gate ships already green and a broken scan would look exactly like a
# healthy one. The gate itself also refuses to pass when it found no
# Q_OBJECT header at all.
qobject-moc-pairing-lint:
name: Q_OBJECT headers vs. AUTOMOC source lists
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- name: Self-test the Q_OBJECT moc-pairing checker
run: python3 scripts/check_qobject_moc_pairing.py --self-test

- name: Check every Q_OBJECT header is one AUTOMOC will scan
run: python3 scripts/check_qobject_moc_pairing.py

# ── The duplicate-ctest-name gate's own self-test ──────────────────────
# The gate itself (scripts/check_ctest_name_collisions.sh) runs after the
# build in ci.yml's ladder-tests job, because it asks ctest what it would
Expand Down
26 changes: 13 additions & 13 deletions examples/bank/gui/controllers/AccountController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ namespace {
QVariantMap toMap(const bank::dto::AccountInfo& account) {
const bool closed = account.status == static_cast<int>(bank::AccountStatus::Closed);
QVariantMap map;
map[QStringLiteral("id")] = static_cast<qlonglong>(account.id);
map[QStringLiteral("kind")] = fmt::accountKind(account.kind);
map[QStringLiteral("number")] = fmt::last4(account.number);
map[QStringLiteral("balanceText")] = fmt::money(account.balanceMinor, account.currency);
map[QStringLiteral("statusText")] = closed ? QStringLiteral("Closed") : QStringLiteral("Open");
map[QStringLiteral("statusKind")] = closed ? QStringLiteral("neutral") : QStringLiteral("good");
map[QStringLiteral("closed")] = closed;
map[QStringLiteral("hasOverdraft")] = account.overdraftMinor > 0;
map[QStringLiteral("overdraftText")] =
QStringLiteral("Overdraft ") + fmt::money(account.overdraftMinor, account.currency);
map.insert(QStringLiteral("id"), static_cast<qlonglong>(account.id));
map.insert(QStringLiteral("kind"), fmt::accountKind(account.kind));
map.insert(QStringLiteral("number"), fmt::last4(account.number));
map.insert(QStringLiteral("balanceText"), fmt::money(account.balanceMinor, account.currency));
map.insert(QStringLiteral("statusText"), closed ? QStringLiteral("Closed") : QStringLiteral("Open"));
map.insert(QStringLiteral("statusKind"), closed ? QStringLiteral("neutral") : QStringLiteral("good"));
map.insert(QStringLiteral("closed"), closed);
map.insert(QStringLiteral("hasOverdraft"), account.overdraftMinor > 0);
map.insert(QStringLiteral("overdraftText"),
QStringLiteral("Overdraft ") + fmt::money(account.overdraftMinor, account.currency));
return map;
}

Expand All @@ -36,10 +36,10 @@ AccountController::AccountController(BankClient& client, QObject* parent)

void AccountController::refresh() {
_model.execute(bank::dto::ListAccounts{})
.then([this](bank::dto::AccountList list) {
.then([this](const bank::dto::AccountList& list) {
_accounts.clear();
std::int64_t total = 0;
int currency = list.accounts.empty() ? 0 : list.accounts.front().currency;
const int currency = list.accounts.empty() ? 0 : list.accounts.front().currency;
bool sameCurrency = true;
_openCount = 0;
for (const auto& account : list.accounts) {
Expand All @@ -62,7 +62,7 @@ void AccountController::refresh() {
void AccountController::openAccount(int kind, int currency, const QString& overdraft) {
const auto minor = overdraft.trimmed().isEmpty() ? 0 : fmt::parseMinor(overdraft).value_or(0);
_model.execute(bank::dto::OpenAccount{.kind = kind, .currency = currency, .overdraftMinor = minor})
.then([this](bank::dto::AccountInfo) { refresh(); })
.then([this](const bank::dto::AccountInfo&) { refresh(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

Expand Down
4 changes: 2 additions & 2 deletions examples/bank/gui/controllers/AppController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ void AppController::adopt(const QString& principal, const QString& displayName)

void AppController::login(const QString& username, const QString& password) {
_auth.execute(bank::dto::LoginRequest{.username = username.toStdString(), .password = password.toStdString()})
.then([this](bank::dto::AuthResult result) {
.then([this](const bank::dto::AuthResult& result) {
if (result.ok) {
adopt(QString::fromStdString(result.principal), QString::fromStdString(result.displayName));
} else {
Expand All @@ -35,7 +35,7 @@ void AppController::registerUser(const QString& username, const QString& passwor
.execute(bank::dto::RegisterUser{.username = username.toStdString(),
.password = password.toStdString(),
.displayName = displayName.toStdString()})
.then([this](bank::dto::AuthResult result) {
.then([this](const bank::dto::AuthResult& result) {
if (result.ok) {
adopt(QString::fromStdString(result.principal), QString::fromStdString(result.displayName));
} else {
Expand Down
63 changes: 36 additions & 27 deletions examples/bank/gui/controllers/CardController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ void CardController::refresh() {

void CardController::reloadAccounts() {
_accountModel.execute(bank::dto::ListAccounts{})
.then([this](bank::dto::AccountList list) {
.then([this](const bank::dto::AccountList& list) {
_accounts.clear();
for (const auto& account : list.accounts) {
if (account.status == static_cast<int>(bank::AccountStatus::Closed)) {
continue;
}
QVariantMap map;
map[QStringLiteral("id")] = static_cast<qlonglong>(account.id);
map[QStringLiteral("label")] = fmt::last4(account.number);
map.insert(QStringLiteral("id"), static_cast<qlonglong>(account.id));
map.insert(QStringLiteral("label"), fmt::last4(account.number));
_accounts.append(map);
}
emit accountsChanged();
Expand All @@ -42,26 +42,35 @@ void CardController::reloadAccounts() {

void CardController::reloadCards() {
_cardModel.execute(bank::dto::ListCards{})
.then([this](bank::dto::CardList list) {
.then([this](const bank::dto::CardList& list) {
_cards.clear();
for (const auto& card : list.cards) {
const auto status = static_cast<bank::CardStatus>(card.status);
const QString kind = card.kind == static_cast<int>(bank::CardKind::Credit) ? QStringLiteral("Credit")
: QStringLiteral("Debit");
// Cancelled is the fall-through arm rather than a third branch: the
// two `statusText`/`statusKind` chains this replaces were nested
// conditional operators, which is what QML reads as the pill's label
// and colour.
QString statusText = QStringLiteral("Cancelled");
QString statusKind = QStringLiteral("bad");
if (status == bank::CardStatus::Active) {
statusText = QStringLiteral("Active");
statusKind = QStringLiteral("good");
} else if (status == bank::CardStatus::Frozen) {
statusText = QStringLiteral("Frozen");
statusKind = QStringLiteral("warn");
}
QVariantMap map;
map[QStringLiteral("id")] = static_cast<qlonglong>(card.id);
map[QStringLiteral("title")] =
kind + QStringLiteral(" card ••••") + QString::fromStdString(card.panLast4);
map[QStringLiteral("limitText")] =
QStringLiteral("Daily limit ") + fmt::money(card.dailyLimitMinor, 0);
map[QStringLiteral("statusText")] = status == bank::CardStatus::Active ? QStringLiteral("Active")
: status == bank::CardStatus::Frozen ? QStringLiteral("Frozen")
: QStringLiteral("Cancelled");
map[QStringLiteral("statusKind")] = status == bank::CardStatus::Active ? QStringLiteral("good")
: status == bank::CardStatus::Frozen ? QStringLiteral("warn")
: QStringLiteral("bad");
map[QStringLiteral("active")] = status == bank::CardStatus::Active;
map[QStringLiteral("cancelled")] = status == bank::CardStatus::Cancelled;
map.insert(QStringLiteral("id"), static_cast<qlonglong>(card.id));
map.insert(QStringLiteral("title"),
kind + QStringLiteral(" card ••••") + QString::fromStdString(card.panLast4));
map.insert(QStringLiteral("limitText"),
QStringLiteral("Daily limit ") + fmt::money(card.dailyLimitMinor, 0));
map.insert(QStringLiteral("statusText"), statusText);
map.insert(QStringLiteral("statusKind"), statusKind);
map.insert(QStringLiteral("active"), status == bank::CardStatus::Active);
map.insert(QStringLiteral("cancelled"), status == bank::CardStatus::Cancelled);
_cards.append(map);
}
emit cardsChanged();
Expand All @@ -76,25 +85,25 @@ void CardController::issue(qlonglong accountId, int kind, const QString& limit)
}
const auto minor = limit.trimmed().isEmpty() ? 0 : fmt::parseMinor(limit).value_or(0);
_cardModel.execute(bank::dto::IssueCard{.accountId = accountId, .kind = kind, .dailyLimitMinor = minor})
.then([this](bank::dto::CardInfo) { reloadCards(); })
.then([this](const bank::dto::CardInfo&) { reloadCards(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

void CardController::freeze(qlonglong id) {
_cardModel.execute(bank::dto::FreezeCard{.id = id})
.then([this](bank::dto::CommandResult) { reloadCards(); })
void CardController::freeze(qlonglong cardId) {
_cardModel.execute(bank::dto::FreezeCard{.id = cardId})
.then([this](const bank::dto::CommandResult&) { reloadCards(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

void CardController::unfreeze(qlonglong id) {
_cardModel.execute(bank::dto::UnfreezeCard{.id = id})
.then([this](bank::dto::CommandResult) { reloadCards(); })
void CardController::unfreeze(qlonglong cardId) {
_cardModel.execute(bank::dto::UnfreezeCard{.id = cardId})
.then([this](const bank::dto::CommandResult&) { reloadCards(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

void CardController::cancel(qlonglong id) {
_cardModel.execute(bank::dto::CancelCard{.id = id})
.then([this](bank::dto::CommandResult) { reloadCards(); })
void CardController::cancel(qlonglong cardId) {
_cardModel.execute(bank::dto::CancelCard{.id = cardId})
.then([this](const bank::dto::CommandResult&) { reloadCards(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

Expand Down
6 changes: 3 additions & 3 deletions examples/bank/gui/controllers/CardController.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class CardController : public BankController {

Q_INVOKABLE void refresh();
Q_INVOKABLE void issue(qlonglong accountId, int kind, const QString& limit);
Q_INVOKABLE void freeze(qlonglong id);
Q_INVOKABLE void unfreeze(qlonglong id);
Q_INVOKABLE void cancel(qlonglong id);
Q_INVOKABLE void freeze(qlonglong cardId);
Q_INVOKABLE void unfreeze(qlonglong cardId);
Q_INVOKABLE void cancel(qlonglong cardId);

signals:
void cardsChanged();
Expand Down
44 changes: 22 additions & 22 deletions examples/bank/gui/controllers/LoanController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ void LoanController::refresh() {

void LoanController::reloadAccounts() {
_accountModel.execute(bank::dto::ListAccounts{})
.then([this](bank::dto::AccountList list) {
.then([this](const bank::dto::AccountList& list) {
_accounts.clear();
for (const auto& account : list.accounts) {
if (account.status == static_cast<int>(bank::AccountStatus::Closed)) {
continue;
}
QVariantMap map;
map[QStringLiteral("id")] = static_cast<qlonglong>(account.id);
map[QStringLiteral("label")] = fmt::last4(account.number);
map.insert(QStringLiteral("id"), static_cast<qlonglong>(account.id));
map.insert(QStringLiteral("label"), fmt::last4(account.number));
_accounts.append(map);
}
emit accountsChanged();
Expand All @@ -41,23 +41,23 @@ void LoanController::reloadAccounts() {

void LoanController::reloadLoans() {
_loanModel.execute(bank::dto::ListLoans{})
.then([this](bank::dto::LoanList list) {
.then([this](const bank::dto::LoanList& list) {
_loans.clear();
for (const auto& loan : list.loans) {
const auto status = static_cast<bank::LoanStatus>(loan.status);
const bool paid = status == bank::LoanStatus::PaidOff;
QVariantMap map;
map[QStringLiteral("id")] = static_cast<qlonglong>(loan.id);
map[QStringLiteral("accountId")] = static_cast<qlonglong>(loan.accountId);
map[QStringLiteral("title")] = QStringLiteral("Loan #%1").arg(loan.id);
map[QStringLiteral("detail")] = QStringLiteral("Outstanding %1 · %2 bps · %3 mo")
.arg(fmt::money(loan.outstandingMinor, loan.currency))
.arg(loan.rateBps)
.arg(loan.termMonths);
map[QStringLiteral("outstanding")] = static_cast<qlonglong>(loan.outstandingMinor);
map[QStringLiteral("statusText")] = paid ? QStringLiteral("Paid off") : QStringLiteral("Active");
map[QStringLiteral("statusKind")] = paid ? QStringLiteral("good") : QStringLiteral("neutral");
map[QStringLiteral("active")] = status == bank::LoanStatus::Active;
map.insert(QStringLiteral("id"), static_cast<qlonglong>(loan.id));
map.insert(QStringLiteral("accountId"), static_cast<qlonglong>(loan.accountId));
map.insert(QStringLiteral("title"), QStringLiteral("Loan #%1").arg(loan.id));
map.insert(QStringLiteral("detail"), QStringLiteral("Outstanding %1 · %2 bps · %3 mo")
.arg(fmt::money(loan.outstandingMinor, loan.currency))
.arg(loan.rateBps)
.arg(loan.termMonths));
map.insert(QStringLiteral("outstanding"), static_cast<qlonglong>(loan.outstandingMinor));
map.insert(QStringLiteral("statusText"), paid ? QStringLiteral("Paid off") : QStringLiteral("Active"));
map.insert(QStringLiteral("statusKind"), paid ? QStringLiteral("good") : QStringLiteral("neutral"));
map.insert(QStringLiteral("active"), status == bank::LoanStatus::Active);
_loans.append(map);
}
emit loansChanged();
Expand All @@ -76,7 +76,7 @@ void LoanController::apply(qlonglong accountId, const QString& principal, int ra
_loanModel
.execute(bank::dto::ApplyLoan{
.accountId = accountId, .principalMinor = *minor, .rateBps = rateBps, .termMonths = termMonths})
.then([this](bank::dto::LoanInfo) { reloadLoans(); })
.then([this](const bank::dto::LoanInfo&) { reloadLoans(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

Expand All @@ -87,20 +87,20 @@ void LoanController::repay(qlonglong loanId, qlonglong accountId, const QString&
return;
}
_loanModel.execute(bank::dto::RepayLoan{.loanId = loanId, .fromAccountId = accountId, .amountMinor = *minor})
.then([this](bank::dto::LoanInfo) { reloadLoans(); })
.then([this](const bank::dto::LoanInfo&) { reloadLoans(); })
.onError([this](const std::exception_ptr& err) { emit error(errorText(err)); });
}

void LoanController::showSchedule(qlonglong loanId) {
_loanModel.execute(bank::dto::LoanScheduleRequest{.loanId = loanId})
.then([this](bank::dto::LoanScheduleResult result) {
.then([this](const bank::dto::LoanScheduleResult& result) {
_schedule.clear();
for (const auto& inst : result.installments) {
QVariantMap map;
map[QStringLiteral("month")] = inst.month;
map[QStringLiteral("principalText")] = fmt::money(inst.principalMinor, 0);
map[QStringLiteral("interestText")] = fmt::money(inst.interestMinor, 0);
map[QStringLiteral("remainingText")] = fmt::money(inst.remainingMinor, 0);
map.insert(QStringLiteral("month"), inst.month);
map.insert(QStringLiteral("principalText"), fmt::money(inst.principalMinor, 0));
map.insert(QStringLiteral("interestText"), fmt::money(inst.interestMinor, 0));
map.insert(QStringLiteral("remainingText"), fmt::money(inst.remainingMinor, 0));
_schedule.append(map);
}
emit scheduleChanged();
Expand Down
Loading
Loading