feat: scanner panel with signature/value search, rescan, address delegate

- Signature mode (IDA-style patterns with wildcards) and value mode (typed exact match)
- Async scan engine with progress, cancel support
- Re-scan updates all results with unified progress (single-pass read + table build)
- Previous value column appears after first re-scan
- WinDbg backtick address format with dimmed leading zeros (AddressDelegate)
- Inline editing: address expressions navigate, value edits write to provider
- Right-click context menu: Copy Address, Copy Value, Go to Address
- Auto-sized columns, themed buttons with icons, dynamic combo width
- 49 UI tests covering scan, rescan, editing, theming, progress completion
This commit is contained in:
IChooseYou
2026-02-28 11:53:51 -07:00
committed by IChooseYou
parent 95faf027a9
commit 41e2f9f662
8 changed files with 3487 additions and 0 deletions

960
tests/test_scanner_ui.cpp Normal file
View File

@@ -0,0 +1,960 @@
#include <QTest>
#include <QSignalSpy>
#include <QApplication>
#include <QComboBox>
#include <QLineEdit>
#include <QCheckBox>
#include <QPushButton>
#include <QProgressBar>
#include <QTableWidget>
#include <QHeaderView>
#include <QLabel>
#include <QClipboard>
#include <cstring>
#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();
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() {
QVERIFY(!m_panel->execCheck()->isChecked());
QVERIFY(!m_panel->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<Provider> {
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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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>(), (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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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>(), (uint64_t)0x20);
}
void edit_previewHex() {
QByteArray data(32, '\0');
data[0] = 0xCC;
auto prov = std::make_shared<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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<BufferProvider>(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
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
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<BufferProvider>(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
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
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);
// All rows should be populated
QCOMPARE(m_panel->resultsTable()->rowCount(), m_panel->resultsTable()->rowCount());
// 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<BufferProvider>(data);
m_panel->setProviderGetter([prov]() { return prov; });
m_panel->modeCombo()->setCurrentIndex(0); // Signature
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
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
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<BufferProvider>(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);
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
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);
QTest::mouseClick(m_panel->updateButton(), Qt::LeftButton);
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)
// ═══════════════════════════════════════════════════════════════════
void providerGetter_lazy() {
auto prov1 = std::make_shared<BufferProvider>(QByteArray(16, '\xAA'));
auto prov2 = std::make_shared<BufferProvider>(QByteArray(16, '\xBB'));
auto current = prov1;
m_panel->setProviderGetter([&current]() { 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"