#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "scannerpanel.h" #include "scanner.h" #include "providers/buffer_provider.h" #include "providers/null_provider.h" using namespace rcx; class TestScannerUI : public QObject { Q_OBJECT private: ScannerPanel* m_panel = nullptr; private slots: void init() { m_panel = new ScannerPanel(); m_panel->show(); // Clear mode-dependent filter defaults so BufferProvider scans // (which have no executable regions) work without filter issues. // The initialState_filterCheckboxes test verifies defaults separately. m_panel->execCheck()->setChecked(false); m_panel->writeCheck()->setChecked(false); QApplication::processEvents(); } void cleanup() { delete m_panel; m_panel = nullptr; } // ═══════════════════════════════════════════════════════════════════ // Widget creation and initial state // ═══════════════════════════════════════════════════════════════════ void initialState_modeCombo() { QCOMPARE(m_panel->modeCombo()->count(), 2); QCOMPARE(m_panel->modeCombo()->currentIndex(), 0); // Signature QCOMPARE(m_panel->modeCombo()->itemText(0), QStringLiteral("Signature")); QCOMPARE(m_panel->modeCombo()->itemText(1), QStringLiteral("Value")); } void initialState_signatureMode() { // In signature mode: pattern visible, value hidden QVERIFY(m_panel->patternEdit()->isVisible()); QVERIFY(!m_panel->typeCombo()->isVisible()); QVERIFY(!m_panel->valueEdit()->isVisible()); } void initialState_scanButton() { QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); } void initialState_progressBarHidden() { QVERIFY(!m_panel->progressBar()->isVisible()); } void initialState_resultsEmpty() { QCOMPARE(m_panel->resultsTable()->rowCount(), 0); } void initialState_buttonsDisabled() { QVERIFY(!m_panel->gotoButton()->isEnabled()); QVERIFY(!m_panel->copyButton()->isEnabled()); } void initialState_statusLabel() { QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("Ready")); } void initialState_filterCheckboxes() { // Verify defaults on a fresh panel (init() clears filters for test convenience) ScannerPanel fresh; // Signature mode default: executable checked, writable unchecked QVERIFY(fresh.execCheck()->isChecked()); QVERIFY(!fresh.writeCheck()->isChecked()); // Switching to value mode flips them fresh.modeCombo()->setCurrentIndex(1); QVERIFY(!fresh.execCheck()->isChecked()); QVERIFY(fresh.writeCheck()->isChecked()); // Switching back restores signature defaults fresh.modeCombo()->setCurrentIndex(0); QVERIFY(fresh.execCheck()->isChecked()); QVERIFY(!fresh.writeCheck()->isChecked()); } void initialState_patternPlaceholder() { QVERIFY(!m_panel->patternEdit()->placeholderText().isEmpty()); } void initialState_typeComboHasAllTypes() { QCOMPARE(m_panel->typeCombo()->count(), 10); // int8..double = 10 types } void initialState_resultsTableColumns() { QCOMPARE(m_panel->resultsTable()->columnCount(), 2); } void initialState_noHeaders() { QVERIFY(!m_panel->resultsTable()->horizontalHeader()->isVisible()); QVERIFY(!m_panel->resultsTable()->verticalHeader()->isVisible()); } void initialState_noGrid() { QVERIFY(!m_panel->resultsTable()->showGrid()); } // ═══════════════════════════════════════════════════════════════════ // Mode switching // ═══════════════════════════════════════════════════════════════════ void switchToValueMode() { m_panel->modeCombo()->setCurrentIndex(1); // Value QVERIFY(!m_panel->patternEdit()->isVisible()); QVERIFY(m_panel->typeCombo()->isVisible()); QVERIFY(m_panel->valueEdit()->isVisible()); } void switchBackToSignatureMode() { m_panel->modeCombo()->setCurrentIndex(1); // Value m_panel->modeCombo()->setCurrentIndex(0); // Signature QVERIFY(m_panel->patternEdit()->isVisible()); QVERIFY(!m_panel->typeCombo()->isVisible()); QVERIFY(!m_panel->valueEdit()->isVisible()); } // ═══════════════════════════════════════════════════════════════════ // No provider — error handling // ═══════════════════════════════════════════════════════════════════ void scan_noProvider() { // No provider getter set m_panel->patternEdit()->setText("48 8B"); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(m_panel->statusLabel()->text().contains("No source")); } void scan_nullProvider() { m_panel->setProviderGetter([]() -> std::shared_ptr { return nullptr; }); m_panel->patternEdit()->setText("48 8B"); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(m_panel->statusLabel()->text().contains("No source")); } // ═══════════════════════════════════════════════════════════════════ // Invalid pattern — error handling // ═══════════════════════════════════════════════════════════════════ void scan_emptyPattern() { auto prov = std::make_shared(QByteArray(16, '\0')); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText(""); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive)); } void scan_invalidPattern() { auto prov = std::make_shared(QByteArray(16, '\0')); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("GG HH"); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive)); } // ═══════════════════════════════════════════════════════════════════ // Successful scan — signature mode // ═══════════════════════════════════════════════════════════════════ void scan_signatureFindsResults() { QByteArray data(32, '\0'); data[8] = 0x48; data[9] = 0x8B; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("48 8B"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); QVERIFY(m_panel->statusLabel()->text().contains("1 result")); } void scan_signatureNoResults() { QByteArray data(32, '\0'); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("FF FF"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 0); QVERIFY(m_panel->statusLabel()->text().contains("0 result")); } // ═══════════════════════════════════════════════════════════════════ // Scan button toggle (Scan ↔ Cancel) // ═══════════════════════════════════════════════════════════════════ void scan_buttonShowsCancel() { // Use a large buffer so scan takes a measurable amount of time QByteArray data(4 * 1024 * 1024, '\0'); // 4MB auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("FF"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); // If scan hasn't finished yet, button should be Cancel if (m_panel->engine()->isRunning()) { QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Cancel")); QVERIFY(m_panel->progressBar()->isVisible()); } // Wait for finish QVERIFY(finSpy.wait(30000)); QApplication::processEvents(); // Button back to Scan QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); QVERIFY(!m_panel->progressBar()->isVisible()); } void scan_cancelMidScan() { // This test verifies the cancel codepath works. // The scan may complete faster than we can click cancel, // so we just verify the final state is correct. QByteArray data(1024 * 1024, '\0'); // 1MB auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("FF"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); // Immediately try to cancel (may or may not succeed depending on timing) m_panel->engine()->abort(); // Wait for finished signal if (finSpy.isEmpty()) QVERIFY(finSpy.wait(10000)); QApplication::processEvents(); // After scan completes (or is cancelled), verify button returns to "Scan" // Need to process any remaining events (the finished handler sets button text) QApplication::processEvents(); // If the panel still shows "Cancel", click it to reset if (m_panel->scanButton()->text() == QStringLiteral("Cancel")) { QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QApplication::processEvents(); } QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); } // ═══════════════════════════════════════════════════════════════════ // Value mode scan // ═══════════════════════════════════════════════════════════════════ void scan_valueInt32() { QByteArray data(64, '\0'); int32_t target = 42; std::memcpy(data.data() + 8, &target, 4); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); // Value mode // Find int32 in combo for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText("42"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Preview should show native int32 value, not hex auto* prevItem = m_panel->resultsTable()->item(0, 1); QVERIFY(prevItem); QCOMPARE(prevItem->text(), QStringLiteral("42")); } void scan_valueInvalidInput() { auto prov = std::make_shared(QByteArray(16, '\0')); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); // Value m_panel->valueEdit()->setText("not_a_number"); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive)); } // ═══════════════════════════════════════════════════════════════════ // Go to Address signal // ═══════════════════════════════════════════════════════════════════ void goToAddress_signal() { QByteArray data(32, '\0'); data[16] = 0xAB; data[17] = 0xCD; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("AB CD"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Select the row m_panel->resultsTable()->selectRow(0); QVERIFY(m_panel->gotoButton()->isEnabled()); QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress); QTest::mouseClick(m_panel->gotoButton(), Qt::LeftButton); QCOMPARE(goSpy.size(), 1); QCOMPARE(goSpy.first().first().value(), (uint64_t)16); } void doubleClick_startsEditing() { // Double-click now starts inline editing, not goToAddress QByteArray data(32, '\0'); data[4] = 0xEF; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("EF"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Double-click should NOT emit goToAddress directly QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress); emit m_panel->resultsTable()->cellDoubleClicked(0, 0); QCOMPARE(goSpy.size(), 0); // Edit triggers should be DoubleClicked QVERIFY(m_panel->resultsTable()->editTriggers() & QAbstractItemView::DoubleClicked); } // ═══════════════════════════════════════════════════════════════════ // Copy Address // ═══════════════════════════════════════════════════════════════════ void copyAddress() { QByteArray data(32, '\0'); data[20] = 0xAA; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("AA"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); m_panel->resultsTable()->selectRow(0); QVERIFY(m_panel->copyButton()->isEnabled()); QTest::mouseClick(m_panel->copyButton(), Qt::LeftButton); QString clip = QApplication::clipboard()->text(); QVERIFY(clip.startsWith("0x", Qt::CaseInsensitive)); // 20 = 0x14, verify it's present somewhere QVERIFY(clip.toUpper().endsWith("14")); } // ═══════════════════════════════════════════════════════════════════ // Results table formatting // ═══════════════════════════════════════════════════════════════════ void results_addressFormat() { QByteArray data(32, '\0'); data[0] = (char)0xFF; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("FF"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); // Check address column has WinDbg backtick format auto* item = m_panel->resultsTable()->item(0, 0); QVERIFY(item); QVERIFY(item->text().contains('`')); // backtick separator // Address 0 should be: 00000000`00000000 QCOMPARE(item->text(), QStringLiteral("00000000`00000000")); } void results_previewColumn() { QByteArray data(32, '\0'); data[0] = 0xDE; data[1] = 0xAD; data[2] = 0xBE; data[3] = 0xEF; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("DE AD"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); auto* item = m_panel->resultsTable()->item(0, 1); QVERIFY(item); QVERIFY(item->text().contains("DE")); QVERIFY(item->text().contains("AD")); QVERIFY(item->text().contains("BE")); QVERIFY(item->text().contains("EF")); } // ═══════════════════════════════════════════════════════════════════ // Filter checkboxes // ═══════════════════════════════════════════════════════════════════ void filters_toggleExecutable() { m_panel->execCheck()->setChecked(true); QVERIFY(m_panel->execCheck()->isChecked()); m_panel->execCheck()->setChecked(false); QVERIFY(!m_panel->execCheck()->isChecked()); } void filters_toggleWritable() { m_panel->writeCheck()->setChecked(true); QVERIFY(m_panel->writeCheck()->isChecked()); } // ═══════════════════════════════════════════════════════════════════ // Theme application // ═══════════════════════════════════════════════════════════════════ void theme_apply() { Theme t; t.background = QColor("#1e1e1e"); t.backgroundAlt = QColor("#252526"); t.text = QColor("#d4d4d4"); t.textDim = QColor("#858585"); t.textMuted = QColor("#555555"); t.textFaint = QColor("#333333"); t.border = QColor("#333333"); t.borderFocused = QColor("#007acc"); t.hover = QColor("#2a2d2e"); t.selection = QColor("#264f78"); t.button = QColor("#333333"); t.indHoverSpan = QColor("#007acc"); // Should not crash m_panel->applyTheme(t); // Verify stylesheet was applied (background color in stylesheet) QVERIFY(m_panel->resultsTable()->styleSheet().contains(t.background.name())); QVERIFY(m_panel->resultsTable()->styleSheet().contains(t.hover.name())); } // ═══════════════════════════════════════════════════════════════════ // Multiple scans — results get replaced // ═══════════════════════════════════════════════════════════════════ void scan_resultsReplaced() { QByteArray data(32, '\0'); data[0] = 0xAA; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); // First scan m_panel->patternEdit()->setText("AA"); QSignalSpy finSpy1(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy1.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Second scan with different pattern (no results) m_panel->patternEdit()->setText("FF FF FF"); QSignalSpy finSpy2(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy2.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 0); } // ═══════════════════════════════════════════════════════════════════ // Selection enables/disables action buttons // ═══════════════════════════════════════════════════════════════════ void buttons_enableOnSelection() { QByteArray data(32, '\0'); data[0] = 0xBB; data[16] = 0xBB; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("BB"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 2); // No selection yet QVERIFY(!m_panel->gotoButton()->isEnabled()); QVERIFY(!m_panel->copyButton()->isEnabled()); // Select row m_panel->resultsTable()->selectRow(0); QVERIFY(m_panel->gotoButton()->isEnabled()); QVERIFY(m_panel->copyButton()->isEnabled()); // Clear selection m_panel->resultsTable()->clearSelection(); QVERIFY(!m_panel->gotoButton()->isEnabled()); QVERIFY(!m_panel->copyButton()->isEnabled()); } // ═══════════════════════════════════════════════════════════════════ // Wildcard signature scan // ═══════════════════════════════════════════════════════════════════ void scan_wildcardSignature() { QByteArray data(32, '\0'); data[0] = 0x48; data[1] = 0x99; data[2] = 0x05; data[16] = 0x48; data[17] = 0xAA; data[18] = 0x05; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("48 ?? 05"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 2); } // ═══════════════════════════════════════════════════════════════════ // Multiple results — status label pluralization // ═══════════════════════════════════════════════════════════════════ void status_singleResult() { QByteArray data(16, '\0'); data[0] = 0xCC; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("CC"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("1 result")); } void status_multipleResults() { QByteArray data(16, '\0'); data[0] = 0xDD; data[8] = 0xDD; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("DD"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->statusLabel()->text(), QStringLiteral("2 results")); } // ═══════════════════════════════════════════════════════════════════ // Provider getter is lazy (captures at scan time) // ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════ // Inline editing // ═══════════════════════════════════════════════════════════════════ void edit_addressExpression() { QByteArray data(64, '\0'); data[0] = 0xAA; data[32] = 0x55; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("AA"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Edit address cell with hex expression QSignalSpy goSpy(m_panel, &ScannerPanel::goToAddress); m_panel->resultsTable()->item(0, 0)->setText("0x20"); QApplication::processEvents(); // Should emit goToAddress with the new address QCOMPARE(goSpy.size(), 1); QCOMPARE(goSpy.first().first().value(), (uint64_t)0x20); } void edit_previewHex() { QByteArray data(32, '\0'); data[0] = 0xCC; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("CC"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Edit preview cell with new hex bytes m_panel->resultsTable()->item(0, 1)->setText("DE AD BE EF"); QApplication::processEvents(); // Verify bytes were written QByteArray written = prov->readBytes(0, 4); QCOMPARE((uint8_t)written[0], (uint8_t)0xDE); QCOMPARE((uint8_t)written[1], (uint8_t)0xAD); QCOMPARE((uint8_t)written[2], (uint8_t)0xBE); QCOMPARE((uint8_t)written[3], (uint8_t)0xEF); } void edit_previewValueMode() { QByteArray data(64, '\0'); int32_t target = 100; std::memcpy(data.data() + 8, &target, 4); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); // Value mode for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText("100"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); // Preview shows "100", edit to "999" QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("100")); m_panel->resultsTable()->item(0, 1)->setText("999"); QApplication::processEvents(); // Verify int32 999 was written at offset 8 int32_t written; QByteArray raw = prov->readBytes(8, 4); std::memcpy(&written, raw.constData(), 4); QCOMPARE(written, 999); } void edit_invalidAddress() { QByteArray data(32, '\0'); data[0] = 0xDD; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("DD"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); // Edit with invalid expression — should show error and restore original m_panel->resultsTable()->item(0, 0)->setText("invalid!!!"); QApplication::processEvents(); QVERIFY(m_panel->statusLabel()->text().contains("error", Qt::CaseInsensitive)); // Address should be restored to original (00000000`00000000) QVERIFY(m_panel->resultsTable()->item(0, 0)->text().contains('`')); } // ═══════════════════════════════════════════════════════════════════ // Update button (rescan) // ═══════════════════════════════════════════════════════════════════ void update_disabledInitially() { QVERIFY(!m_panel->updateButton()->isEnabled()); } void update_enabledAfterScan() { QByteArray data(32, '\0'); data[0] = 0xAA; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->patternEdit()->setText("AA"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QVERIFY(m_panel->updateButton()->isEnabled()); } void update_showsPreviousColumn() { QByteArray data(64, '\0'); int32_t val = 50; std::memcpy(data.data() + 8, &val, 4); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); // Value for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText("50"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); QCOMPARE(m_panel->resultsTable()->columnCount(), 2); // no previous yet // Modify via provider — change value from 50 to 99 int32_t newVal = 99; QByteArray newBytes(4, '\0'); std::memcpy(newBytes.data(), &newVal, 4); prov->writeBytes(8, newBytes); // Click update — runs async QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->columnCount(), 3); // Current value = 99, previous = 50 QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("99")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("50")); } // ═══════════════════════════════════════════════════════════════════ // Re-scan: progress completes, values update, table populates // ═══════════════════════════════════════════════════════════════════ void update_progressCompletes() { // Use a buffer with many results to verify progress reaches 100% // and doesn't hang. Place int32 value "7" every 4 bytes. QByteArray data(4096, '\0'); int32_t val = 7; for (int i = 0; i < 1024; i++) std::memcpy(data.data() + i * 4, &val, 4); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); // Value for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText("7"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QVERIFY(m_panel->resultsTable()->rowCount() >= 512); QCOMPARE(m_panel->resultsTable()->columnCount(), 2); // Modify all values: 7 → 21 int32_t newVal = 21; for (int i = 0; i < 1024; i++) { QByteArray nb(4, '\0'); std::memcpy(nb.data(), &newVal, 4); prov->writeBytes(i * 4, nb); } // Click Re-scan — runs async QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); // Progress bar should be hidden (completed) QVERIFY(!m_panel->progressBar()->isVisible()); // Table should have 3 columns now QCOMPARE(m_panel->resultsTable()->columnCount(), 3); // Spot check first and last row QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("21")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("7")); int lastRow = m_panel->resultsTable()->rowCount() - 1; QVERIFY(m_panel->resultsTable()->item(lastRow, 0) != nullptr); QCOMPARE(m_panel->resultsTable()->item(lastRow, 1)->text(), QStringLiteral("21")); QCOMPARE(m_panel->resultsTable()->item(lastRow, 2)->text(), QStringLiteral("7")); // Status should say "Updated" QVERIFY(m_panel->statusLabel()->text().contains("Updated")); // Buttons should be re-enabled QVERIFY(m_panel->updateButton()->isEnabled()); QVERIFY(m_panel->scanButton()->isEnabled()); } void update_signatureMode() { // Re-scan in signature mode (reads actual bytes, not cached pattern) QByteArray data(64, '\0'); data[0] = 0x48; data[1] = 0x8B; data[2] = 0xAA; data[32] = 0x48; data[33] = 0x8B; data[34] = 0xBB; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(0); // Signature m_panel->execCheck()->setChecked(false); // BufferProvider has no exec regions m_panel->patternEdit()->setText("48 8B ??"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 2); // Modify bytes at first match QByteArray mod(3, '\0'); mod[0] = 0x48; mod[1] = 0x8B; mod[2] = (char)0xFF; prov->writeBytes(0, mod); // Re-scan — runs async QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); QVERIFY(!m_panel->progressBar()->isVisible()); QCOMPARE(m_panel->resultsTable()->columnCount(), 3); // First result current value should contain FF QVERIFY(m_panel->resultsTable()->item(0, 1)->text().contains("FF")); // First result previous value should contain AA QVERIFY(m_panel->resultsTable()->item(0, 2)->text().contains("AA")); } void update_doubleRescan() { // Two consecutive re-scans: previous column updates each time QByteArray data(32, '\0'); int32_t v1 = 10; std::memcpy(data.data() + 4, &v1, 4); auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText("10"); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(5000)); QApplication::processEvents(); QCOMPARE(m_panel->resultsTable()->rowCount(), 1); QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("10")); // First update: 10 → 20 int32_t v2 = 20; QByteArray nb2(4, '\0'); std::memcpy(nb2.data(), &v2, 4); prov->writeBytes(4, nb2); { QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); } QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("20")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("10")); // Second update: 20 → 30 int32_t v3 = 30; QByteArray nb3(4, '\0'); std::memcpy(nb3.data(), &v3, 4); prov->writeBytes(4, nb3); { QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY(rescanSpy.wait(5000)); QApplication::processEvents(); } QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QStringLiteral("30")); QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QStringLiteral("20")); } // ═══════════════════════════════════════════════════════════════════ // Provider getter is lazy (captures at scan time) // ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════ // Benchmark: initial scan + 10 re-scans on a large buffer // ═══════════════════════════════════════════════════════════════════ void bench_rescan10x() { // 1MB buffer, int32 value=42 placed every 512 bytes → 2048 results constexpr int kBufSize = 1 * 1024 * 1024; constexpr int kStride = 512; constexpr int32_t kVal = 42; QByteArray data(kBufSize, '\0'); int planted = 0; for (int off = 0; off + 4 <= kBufSize; off += kStride) { std::memcpy(data.data() + off, &kVal, 4); planted++; } qDebug() << "[bench] buffer:" << (kBufSize / 1024) << "KB," << planted << "planted values, stride:" << kStride; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(1); for (int i = 0; i < m_panel->typeCombo()->count(); i++) { if (m_panel->typeCombo()->itemData(i).toInt() == (int)ValueType::Int32) { m_panel->typeCombo()->setCurrentIndex(i); break; } } m_panel->valueEdit()->setText(QString::number(kVal)); // ── Initial scan ── QElapsedTimer totalTimer; totalTimer.start(); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(30000)); QApplication::processEvents(); int resultCount = m_panel->resultsTable()->rowCount(); qDebug() << "[bench] initial scan:" << totalTimer.elapsed() << "ms," << resultCount << "results displayed"; QVERIFY(resultCount > 0); QVERIFY(m_panel->updateButton()->isEnabled()); // ── 10 re-scans — mutate values each time ── for (int iter = 1; iter <= 10; iter++) { int32_t newVal = kVal + iter; for (int off = 0; off + 4 <= kBufSize; off += kStride) std::memcpy(prov->data().data() + off, &newVal, 4); QElapsedTimer iterTimer; iterTimer.start(); QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY2(rescanSpy.wait(30000), qPrintable(QString("rescan #%1 timed out").arg(iter))); QApplication::processEvents(); qDebug() << "[bench] rescan #" << iter << ":" << iterTimer.elapsed() << "ms" << "| val:" << newVal << "| rows:" << m_panel->resultsTable()->rowCount() << "| status:" << m_panel->statusLabel()->text(); QVERIFY(m_panel->resultsTable()->item(0, 1) != nullptr); QCOMPARE(m_panel->resultsTable()->item(0, 1)->text(), QString::number(newVal)); if (m_panel->resultsTable()->columnCount() >= 3) { QCOMPARE(m_panel->resultsTable()->item(0, 2)->text(), QString::number(newVal - 1)); } QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); QVERIFY(!m_panel->progressBar()->isVisible()); QVERIFY(m_panel->updateButton()->isEnabled()); } qDebug() << "[bench] total (scan + 10 rescans):" << totalTimer.elapsed() << "ms"; } // ═══════════════════════════════════════════════════════════════════ // Benchmark: signature re-scan (16 bytes per result) // ═══════════════════════════════════════════════════════════════════ void bench_rescanSignature10x() { // 1MB buffer with "MZ" planted every 4096 bytes → 256 results constexpr int kBufSize = 1 * 1024 * 1024; constexpr int kStride = 4096; QByteArray data(kBufSize, '\0'); int planted = 0; for (int off = 0; off + 2 <= kBufSize; off += kStride) { data[off] = 0x4D; data[off + 1] = 0x5A; planted++; } qDebug() << "[bench-sig] buffer:" << (kBufSize / 1024) << "KB," << planted << "planted sigs"; auto prov = std::make_shared(data); m_panel->setProviderGetter([prov]() { return prov; }); m_panel->modeCombo()->setCurrentIndex(0); m_panel->patternEdit()->setText("4D 5A"); QElapsedTimer totalTimer; totalTimer.start(); QSignalSpy finSpy(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy.wait(30000)); QApplication::processEvents(); qDebug() << "[bench-sig] initial scan:" << totalTimer.elapsed() << "ms," << m_panel->resultsTable()->rowCount() << "results"; QVERIFY(m_panel->resultsTable()->rowCount() > 0); for (int iter = 1; iter <= 10; iter++) { for (int off = 0; off + 3 <= kBufSize; off += kStride) prov->data().data()[off + 2] = (char)(iter & 0xFF); QElapsedTimer iterTimer; iterTimer.start(); QSignalSpy rescanSpy(m_panel->engine(), &ScanEngine::rescanFinished); QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton); QVERIFY2(rescanSpy.wait(30000), qPrintable(QString("sig rescan #%1 timed out").arg(iter))); QApplication::processEvents(); qDebug() << "[bench-sig] rescan #" << iter << ":" << iterTimer.elapsed() << "ms" << "| status:" << m_panel->statusLabel()->text(); QCOMPARE(m_panel->scanButton()->text(), QStringLiteral("Scan")); QVERIFY(!m_panel->progressBar()->isVisible()); QVERIFY(m_panel->updateButton()->isEnabled()); } qDebug() << "[bench-sig] total:" << totalTimer.elapsed() << "ms"; } // ═══════════════════════════════════════════════════════════════════ // Provider getter is lazy (captures at scan time) // ═══════════════════════════════════════════════════════════════════ void providerGetter_lazy() { auto prov1 = std::make_shared(QByteArray(16, '\xAA')); auto prov2 = std::make_shared(QByteArray(16, '\xBB')); auto current = prov1; m_panel->setProviderGetter([¤t]() { return current; }); // Scan with prov1 — should find AA m_panel->patternEdit()->setText("AA"); QSignalSpy finSpy1(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy1.wait(5000)); QApplication::processEvents(); QVERIFY(m_panel->resultsTable()->rowCount() > 0); // Switch provider current = prov2; // Scan for BB — should find in new provider m_panel->patternEdit()->setText("BB"); QSignalSpy finSpy2(m_panel->engine(), &ScanEngine::finished); QTest::mouseClick(m_panel->scanButton(), Qt::LeftButton); QVERIFY(finSpy2.wait(5000)); QApplication::processEvents(); QVERIFY(m_panel->resultsTable()->rowCount() > 0); } }; QTEST_MAIN(TestScannerUI) #include "test_scanner_ui.moc"