wallettests.cpp
1 // Copyright (c) 2015-present The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <qt/test/wallettests.h> 6 #include <qt/test/util.h> 7 8 #include <wallet/coincontrol.h> 9 #include <interfaces/chain.h> 10 #include <interfaces/node.h> 11 #include <key_io.h> 12 #include <qt/bitcoinamountfield.h> 13 #include <qt/bitcoinunits.h> 14 #include <qt/clientmodel.h> 15 #include <qt/optionsmodel.h> 16 #include <qt/overviewpage.h> 17 #include <qt/platformstyle.h> 18 #include <qt/qvalidatedlineedit.h> 19 #include <qt/receivecoinsdialog.h> 20 #include <qt/receiverequestdialog.h> 21 #include <qt/recentrequeststablemodel.h> 22 #include <qt/sendcoinsdialog.h> 23 #include <qt/sendcoinsentry.h> 24 #include <qt/transactiontablemodel.h> 25 #include <qt/transactionview.h> 26 #include <qt/walletmodel.h> 27 #include <script/solver.h> 28 #include <test/util/setup_common.h> 29 #include <validation.h> 30 #include <wallet/test/util.h> 31 #include <wallet/wallet.h> 32 33 #include <chrono> 34 #include <memory> 35 36 #include <QAbstractButton> 37 #include <QAction> 38 #include <QApplication> 39 #include <QCheckBox> 40 #include <QClipboard> 41 #include <QObject> 42 #include <QPushButton> 43 #include <QTimer> 44 #include <QVBoxLayout> 45 #include <QTextEdit> 46 #include <QListView> 47 #include <QDialogButtonBox> 48 49 using wallet::AddWallet; 50 using wallet::CWallet; 51 using wallet::CreateMockableWalletDatabase; 52 using wallet::RemoveWallet; 53 using wallet::WALLET_FLAG_DESCRIPTORS; 54 using wallet::WALLET_FLAG_DISABLE_PRIVATE_KEYS; 55 using wallet::WalletContext; 56 using wallet::WalletDescriptor; 57 using wallet::WalletRescanReserver; 58 59 namespace 60 { 61 //! Press "Yes" or "Cancel" buttons in modal send confirmation dialog. 62 void ConfirmSend(QString* text = nullptr, QMessageBox::StandardButton confirm_type = QMessageBox::Yes) 63 { 64 QTimer::singleShot(0, [text, confirm_type]() { 65 for (QWidget* widget : QApplication::topLevelWidgets()) { 66 if (widget->inherits("SendConfirmationDialog")) { 67 SendConfirmationDialog* dialog = qobject_cast<SendConfirmationDialog*>(widget); 68 if (text) *text = dialog->text(); 69 QAbstractButton* button = dialog->button(confirm_type); 70 button->setEnabled(true); 71 button->click(); 72 } 73 } 74 }); 75 } 76 77 //! Send coins to address and return txid. 78 Txid SendCoins(CWallet& wallet, SendCoinsDialog& sendCoinsDialog, const CTxDestination& address, CAmount amount, bool rbf, 79 QMessageBox::StandardButton confirm_type = QMessageBox::Yes) 80 { 81 QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries"); 82 SendCoinsEntry* entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget()); 83 entry->findChild<QValidatedLineEdit*>("payTo")->setText(QString::fromStdString(EncodeDestination(address))); 84 entry->findChild<BitcoinAmountField*>("payAmount")->setValue(amount); 85 sendCoinsDialog.findChild<QFrame*>("frameFee") 86 ->findChild<QFrame*>("frameFeeSelection") 87 ->findChild<QCheckBox*>("optInRBF") 88 ->setCheckState(rbf ? Qt::Checked : Qt::Unchecked); 89 Txid txid; 90 btcsignals::scoped_connection c(wallet.NotifyTransactionChanged.connect([&txid](const Txid& hash, ChangeType status) { 91 if (status == CT_NEW) txid = hash; 92 })); 93 ConfirmSend(/*text=*/nullptr, confirm_type); 94 bool invoked = QMetaObject::invokeMethod(&sendCoinsDialog, "sendButtonClicked", Q_ARG(bool, false)); 95 assert(invoked); 96 return txid; 97 } 98 99 //! Find index of txid in transaction list. 100 QModelIndex FindTx(const QAbstractItemModel& model, const Txid& txid) 101 { 102 QString hash = QString::fromStdString(txid.ToString()); 103 int rows = model.rowCount({}); 104 for (int row = 0; row < rows; ++row) { 105 QModelIndex index = model.index(row, 0, {}); 106 if (model.data(index, TransactionTableModel::TxHashRole) == hash) { 107 return index; 108 } 109 } 110 return {}; 111 } 112 113 //! Invoke bumpfee on txid and check results. 114 void BumpFee(TransactionView& view, const Txid& txid, bool expectDisabled, std::string expectError, bool cancel) 115 { 116 QTableView* table = view.findChild<QTableView*>("transactionView"); 117 QModelIndex index = FindTx(*table->selectionModel()->model(), txid); 118 QVERIFY2(index.isValid(), "Could not find BumpFee txid"); 119 120 // Select row in table, invoke context menu, and make sure bumpfee action is 121 // enabled or disabled as expected. 122 QAction* action = view.findChild<QAction*>("bumpFeeAction"); 123 table->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); 124 action->setEnabled(expectDisabled); 125 table->customContextMenuRequested({}); 126 QCOMPARE(action->isEnabled(), !expectDisabled); 127 128 action->setEnabled(true); 129 QString text; 130 if (expectError.empty()) { 131 ConfirmSend(&text, cancel ? QMessageBox::Cancel : QMessageBox::Yes); 132 } else { 133 ConfirmMessage(&text, 0ms); 134 } 135 action->trigger(); 136 QVERIFY(text.indexOf(QString::fromStdString(expectError)) != -1); 137 } 138 139 void CompareBalance(WalletModel& walletModel, CAmount expected_balance, QLabel* balance_label_to_check) 140 { 141 BitcoinUnit unit = walletModel.getOptionsModel()->getDisplayUnit(); 142 QString balanceComparison = BitcoinUnits::formatWithUnit(unit, expected_balance, false, BitcoinUnits::SeparatorStyle::ALWAYS); 143 QCOMPARE(balance_label_to_check->text().trimmed(), balanceComparison); 144 } 145 146 // Verify the 'useAvailableBalance' functionality. With and without manually selected coins. 147 // Case 1: No coin control selected coins. 148 // 'useAvailableBalance' should fill the amount edit box with the total available balance 149 // Case 2: With coin control selected coins. 150 // 'useAvailableBalance' should fill the amount edit box with the sum of the selected coins values. 151 void VerifyUseAvailableBalance(SendCoinsDialog& sendCoinsDialog, const WalletModel& walletModel) 152 { 153 // Verify first entry amount and "useAvailableBalance" button 154 QVBoxLayout* entries = sendCoinsDialog.findChild<QVBoxLayout*>("entries"); 155 QVERIFY(entries->count() == 1); // only one entry 156 SendCoinsEntry* send_entry = qobject_cast<SendCoinsEntry*>(entries->itemAt(0)->widget()); 157 QVERIFY(send_entry->getValue().amount == 0); 158 // Now click "useAvailableBalance", check updated balance (the entire wallet balance should be set) 159 Q_EMIT send_entry->useAvailableBalance(send_entry); 160 QVERIFY(send_entry->getValue().amount == walletModel.getCachedBalance().balance); 161 162 // Now manually select two coins and click on "useAvailableBalance". Then check updated balance 163 // (only the sum of the selected coins should be set). 164 int COINS_TO_SELECT = 2; 165 auto coins = walletModel.wallet().listCoins(); 166 CAmount sum_selected_coins = 0; 167 int selected = 0; 168 QVERIFY(coins.size() == 1); // context check, coins received only on one destination 169 for (const auto& [outpoint, tx_out] : coins.begin()->second) { 170 sendCoinsDialog.getCoinControl()->Select(outpoint); 171 sum_selected_coins += tx_out.txout.nValue; 172 if (++selected == COINS_TO_SELECT) break; 173 } 174 QVERIFY(selected == COINS_TO_SELECT); 175 176 // Now that we have 2 coins selected, "useAvailableBalance" should update the balance label only with 177 // the sum of them. 178 Q_EMIT send_entry->useAvailableBalance(send_entry); 179 QVERIFY(send_entry->getValue().amount == sum_selected_coins); 180 } 181 182 void SyncUpWallet(const std::shared_ptr<CWallet>& wallet, interfaces::Node& node) 183 { 184 WalletRescanReserver reserver(*wallet); 185 reserver.reserve(); 186 CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*fUpdate=*/true, /*save_progress=*/false); 187 QCOMPARE(result.status, CWallet::ScanResult::SUCCESS); 188 QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); 189 QVERIFY(result.last_failed_block.IsNull()); 190 } 191 192 std::shared_ptr<CWallet> SetupDescriptorsWallet(interfaces::Node& node, TestChain100Setup& test, bool watch_only = false) 193 { 194 std::shared_ptr<CWallet> wallet = std::make_shared<CWallet>(node.context()->chain.get(), "", CreateMockableWalletDatabase()); 195 LOCK(wallet->cs_wallet); 196 wallet->SetWalletFlag(WALLET_FLAG_DESCRIPTORS); 197 if (watch_only) { 198 wallet->SetWalletFlag(WALLET_FLAG_DISABLE_PRIVATE_KEYS); 199 } else { 200 wallet->SetupDescriptorScriptPubKeyMans(); 201 } 202 203 // Add the coinbase key 204 FlatSigningProvider provider; 205 std::string error; 206 std::string key_str; 207 if (watch_only) { 208 key_str = HexStr(test.coinbaseKey.GetPubKey()); 209 } else { 210 key_str = EncodeSecret(test.coinbaseKey); 211 } 212 auto descs = Parse("combo(" + key_str + ")", provider, error, /* require_checksum=*/ false); 213 assert(!descs.empty()); 214 assert(descs.size() == 1); 215 auto& desc = descs.at(0); 216 WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); 217 Assert(wallet->AddWalletDescriptor(w_desc, provider, "", false)); 218 const PKHash dest{test.coinbaseKey.GetPubKey()}; 219 wallet->SetAddressBook(dest, "", wallet::AddressPurpose::RECEIVE); 220 wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); 221 SyncUpWallet(wallet, node); 222 wallet->SetBroadcastTransactions(true); 223 return wallet; 224 } 225 226 struct MiniGUI { 227 public: 228 SendCoinsDialog sendCoinsDialog; 229 TransactionView transactionView; 230 OptionsModel optionsModel; 231 std::unique_ptr<ClientModel> clientModel; 232 std::unique_ptr<WalletModel> walletModel; 233 234 MiniGUI(interfaces::Node& node, const PlatformStyle* platformStyle) : sendCoinsDialog(platformStyle), transactionView(platformStyle), optionsModel(node) { 235 bilingual_str error; 236 QVERIFY(optionsModel.Init(error)); 237 clientModel = std::make_unique<ClientModel>(node, &optionsModel); 238 } 239 240 void initModelForWallet(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet, const PlatformStyle* platformStyle) 241 { 242 WalletContext& context = *node.walletLoader().context(); 243 AddWallet(context, wallet); 244 walletModel = std::make_unique<WalletModel>(interfaces::MakeWallet(context, wallet), *clientModel, platformStyle); 245 RemoveWallet(context, wallet, /* load_on_start= */ std::nullopt); 246 sendCoinsDialog.setModel(walletModel.get()); 247 transactionView.setModel(walletModel.get()); 248 } 249 250 }; 251 252 //! Simple qt wallet tests. 253 // 254 // Test widgets can be debugged interactively calling show() on them and 255 // manually running the event loop, e.g.: 256 // 257 // sendCoinsDialog.show(); 258 // QEventLoop().exec(); 259 // 260 // This also requires overriding the default minimal Qt platform: 261 // 262 // QT_QPA_PLATFORM=xcb build/bin/test_bitcoin-qt # Linux 263 // QT_QPA_PLATFORM=windows build/bin/test_bitcoin-qt # Windows 264 // QT_QPA_PLATFORM=cocoa build/bin/test_bitcoin-qt # macOS 265 void TestGUI(interfaces::Node& node, const std::shared_ptr<CWallet>& wallet) 266 { 267 // Create widgets for sending coins and listing transactions. 268 std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other")); 269 MiniGUI mini_gui(node, platformStyle.get()); 270 mini_gui.initModelForWallet(node, wallet, platformStyle.get()); 271 WalletModel& walletModel = *mini_gui.walletModel; 272 SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog; 273 TransactionView& transactionView = mini_gui.transactionView; 274 275 // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel. 276 walletModel.pollBalanceChanged(); 277 // Check balance in send dialog 278 CompareBalance(walletModel, walletModel.wallet().getBalance(), sendCoinsDialog.findChild<QLabel*>("labelBalance")); 279 280 // Check 'UseAvailableBalance' functionality 281 VerifyUseAvailableBalance(sendCoinsDialog, walletModel); 282 283 // Send two transactions, and verify they are added to transaction list. 284 TransactionTableModel* transactionTableModel = walletModel.getTransactionTableModel(); 285 QCOMPARE(transactionTableModel->rowCount({}), 105); 286 Txid txid1 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false); 287 Txid txid2 = SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 10 * COIN, /*rbf=*/true); 288 // Transaction table model updates on a QueuedConnection, so process events to ensure it's updated. 289 qApp->processEvents(); 290 QCOMPARE(transactionTableModel->rowCount({}), 107); 291 QVERIFY(FindTx(*transactionTableModel, txid1).isValid()); 292 QVERIFY(FindTx(*transactionTableModel, txid2).isValid()); 293 294 // Call bumpfee. Test canceled fullrbf bump, canceled bip-125-rbf bump, passing bump, and then failing bump. 295 BumpFee(transactionView, txid1, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/true); 296 BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/true); 297 BumpFee(transactionView, txid2, /*expectDisabled=*/false, /*expectError=*/{}, /*cancel=*/false); 298 BumpFee(transactionView, txid2, /*expectDisabled=*/true, /*expectError=*/"already bumped", /*cancel=*/false); 299 300 // Check current balance on OverviewPage 301 OverviewPage overviewPage(platformStyle.get()); 302 overviewPage.setWalletModel(&walletModel); 303 walletModel.pollBalanceChanged(); // Manual balance polling update 304 CompareBalance(walletModel, walletModel.wallet().getBalance(), overviewPage.findChild<QLabel*>("labelBalance")); 305 306 // Check Request Payment button 307 ReceiveCoinsDialog receiveCoinsDialog(platformStyle.get()); 308 receiveCoinsDialog.setModel(&walletModel); 309 RecentRequestsTableModel* requestTableModel = walletModel.getRecentRequestsTableModel(); 310 311 // Label input 312 QLineEdit* labelInput = receiveCoinsDialog.findChild<QLineEdit*>("reqLabel"); 313 labelInput->setText("TEST_LABEL_1"); 314 315 // Amount input 316 BitcoinAmountField* amountInput = receiveCoinsDialog.findChild<BitcoinAmountField*>("reqAmount"); 317 amountInput->setValue(1); 318 319 // Message input 320 QLineEdit* messageInput = receiveCoinsDialog.findChild<QLineEdit*>("reqMessage"); 321 messageInput->setText("TEST_MESSAGE_1"); 322 int initialRowCount = requestTableModel->rowCount({}); 323 QPushButton* requestPaymentButton = receiveCoinsDialog.findChild<QPushButton*>("receiveButton"); 324 requestPaymentButton->click(); 325 QString address; 326 for (QWidget* widget : QApplication::topLevelWidgets()) { 327 if (widget->inherits("ReceiveRequestDialog")) { 328 ReceiveRequestDialog* receiveRequestDialog = qobject_cast<ReceiveRequestDialog*>(widget); 329 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("payment_header")->text(), QString("Payment information")); 330 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("uri_tag")->text(), QString("URI:")); 331 QString uri = receiveRequestDialog->QObject::findChild<QLabel*>("uri_content")->text(); 332 QCOMPARE(uri.count("bitcoin:"), 2); 333 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("address_tag")->text(), QString("Address:")); 334 QVERIFY(address.isEmpty()); 335 address = receiveRequestDialog->QObject::findChild<QLabel*>("address_content")->text(); 336 QVERIFY(!address.isEmpty()); 337 338 QCOMPARE(uri.count("amount=0.00000001"), 2); 339 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_tag")->text(), QString("Amount:")); 340 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("amount_content")->text(), QString::fromStdString("0.00000001 " + CURRENCY_UNIT)); 341 342 QCOMPARE(uri.count("label=TEST_LABEL_1"), 2); 343 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_tag")->text(), QString("Label:")); 344 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("label_content")->text(), QString("TEST_LABEL_1")); 345 346 QCOMPARE(uri.count("message=TEST_MESSAGE_1"), 2); 347 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_tag")->text(), QString("Message:")); 348 QCOMPARE(receiveRequestDialog->QObject::findChild<QLabel*>("message_content")->text(), QString("TEST_MESSAGE_1")); 349 } 350 } 351 352 // Clear button 353 QPushButton* clearButton = receiveCoinsDialog.findChild<QPushButton*>("clearButton"); 354 clearButton->click(); 355 QCOMPARE(labelInput->text(), QString("")); 356 QCOMPARE(amountInput->value(), CAmount(0)); 357 QCOMPARE(messageInput->text(), QString("")); 358 359 // Check addition to history 360 int currentRowCount = requestTableModel->rowCount({}); 361 QCOMPARE(currentRowCount, initialRowCount+1); 362 363 // Check addition to wallet 364 std::vector<std::string> requests = walletModel.wallet().getAddressReceiveRequests(); 365 QCOMPARE(requests.size(), size_t{1}); 366 RecentRequestEntry entry; 367 SpanReader{MakeByteSpan(requests[0])} >> entry; 368 QCOMPARE(entry.nVersion, int{1}); 369 QCOMPARE(entry.id, int64_t{1}); 370 QVERIFY(entry.date.isValid()); 371 QCOMPARE(entry.recipient.address, address); 372 QCOMPARE(entry.recipient.label, QString{"TEST_LABEL_1"}); 373 QCOMPARE(entry.recipient.amount, CAmount{1}); 374 QCOMPARE(entry.recipient.message, QString{"TEST_MESSAGE_1"}); 375 QCOMPARE(entry.recipient.sPaymentRequest, std::string{}); 376 QCOMPARE(entry.recipient.authenticatedMerchant, QString{}); 377 378 // Check Remove button 379 QTableView* table = receiveCoinsDialog.findChild<QTableView*>("recentRequestsView"); 380 table->selectRow(currentRowCount-1); 381 QPushButton* removeRequestButton = receiveCoinsDialog.findChild<QPushButton*>("removeRequestButton"); 382 removeRequestButton->click(); 383 QCOMPARE(requestTableModel->rowCount({}), currentRowCount-1); 384 385 // Check removal from wallet 386 QCOMPARE(walletModel.wallet().getAddressReceiveRequests().size(), size_t{0}); 387 } 388 389 void TestGUIWatchOnly(interfaces::Node& node, TestChain100Setup& test) 390 { 391 const std::shared_ptr<CWallet>& wallet = SetupDescriptorsWallet(node, test, /*watch_only=*/true); 392 393 // Create widgets and init models 394 std::unique_ptr<const PlatformStyle> platformStyle(PlatformStyle::instantiate("other")); 395 MiniGUI mini_gui(node, platformStyle.get()); 396 mini_gui.initModelForWallet(node, wallet, platformStyle.get()); 397 WalletModel& walletModel = *mini_gui.walletModel; 398 SendCoinsDialog& sendCoinsDialog = mini_gui.sendCoinsDialog; 399 400 // Update walletModel cached balance which will trigger an update for the 'labelBalance' QLabel. 401 walletModel.pollBalanceChanged(); 402 // Check balance in send dialog 403 CompareBalance(walletModel, walletModel.wallet().getBalances().balance, 404 sendCoinsDialog.findChild<QLabel*>("labelBalance")); 405 406 // Set change address 407 sendCoinsDialog.getCoinControl()->destChange = PKHash{test.coinbaseKey.GetPubKey()}; 408 409 // Time to reject "save" PSBT dialog ('SendCoins' locks the main thread until the dialog receives the event). 410 QTimer timer; 411 timer.setInterval(500); 412 QObject::connect(&timer, &QTimer::timeout, [&](){ 413 for (QWidget* widget : QApplication::topLevelWidgets()) { 414 if (widget->inherits("QMessageBox") && widget->objectName().compare("psbt_copied_message") == 0) { 415 QMessageBox* dialog = qobject_cast<QMessageBox*>(widget); 416 QAbstractButton* button = dialog->button(QMessageBox::Discard); 417 button->setEnabled(true); 418 button->click(); 419 timer.stop(); 420 break; 421 } 422 } 423 }); 424 timer.start(500); 425 426 // Send tx and verify PSBT copied to the clipboard. 427 SendCoins(*wallet.get(), sendCoinsDialog, PKHash(), 5 * COIN, /*rbf=*/false, QMessageBox::Save); 428 const std::string& psbt_string = QApplication::clipboard()->text().toStdString(); 429 QVERIFY(!psbt_string.empty()); 430 431 // Decode psbt 432 std::optional<std::vector<unsigned char>> decoded_psbt = DecodeBase64(psbt_string); 433 QVERIFY(decoded_psbt); 434 PartiallySignedTransaction psbt; 435 std::string err; 436 QVERIFY(DecodeRawPSBT(psbt, MakeByteSpan(*decoded_psbt), err)); 437 } 438 439 void TestGUI(interfaces::Node& node) 440 { 441 // Set up wallet and chain with 105 blocks (5 mature blocks for spending). 442 TestChain100Setup test; 443 for (int i = 0; i < 5; ++i) { 444 test.CreateAndProcessBlock({}, GetScriptForRawPubKey(test.coinbaseKey.GetPubKey())); 445 } 446 auto wallet_loader = interfaces::MakeWalletLoader(*test.m_node.chain, *Assert(test.m_node.args)); 447 test.m_node.wallet_loader = wallet_loader.get(); 448 node.setContext(&test.m_node); 449 450 // "Full" GUI tests, use descriptor wallet 451 const std::shared_ptr<CWallet>& desc_wallet = SetupDescriptorsWallet(node, test); 452 TestGUI(node, desc_wallet); 453 454 // Legacy watch-only wallet test 455 // Verify PSBT creation. 456 TestGUIWatchOnly(node, test); 457 } 458 459 } // namespace 460 461 void WalletTests::walletTests() 462 { 463 #ifdef Q_OS_MACOS 464 if (QApplication::platformName() == "minimal") { 465 // Disable for mac on "minimal" platform to avoid crashes inside the Qt 466 // framework when it tries to look up unimplemented cocoa functions, 467 // and fails to handle returned nulls 468 // (https://bugreports.qt.io/browse/QTBUG-49686). 469 qWarning() << "Skipping WalletTests on mac build with 'minimal' platform set due to Qt bugs. To run AppTests, invoke " 470 "with 'QT_QPA_PLATFORM=cocoa test_bitcoin-qt' on mac, or else use a linux or windows build."; 471 return; 472 } 473 #endif 474 TestGUI(m_node); 475 }