Fix for windows
This commit is contained in:
parent
23c3f366b1
commit
c2fc0104c6
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ Makefile
|
|||||||
/Contents/
|
/Contents/
|
||||||
/application.log
|
/application.log
|
||||||
/database/app_data.db
|
/database/app_data.db
|
||||||
|
/build_x64/
|
||||||
|
|||||||
@ -216,6 +216,27 @@ if (WIN32)
|
|||||||
)
|
)
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
|
# Copy mock-server test scripts into the build so they ship with the archive
|
||||||
|
set(_MOCK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests/mock_server")
|
||||||
|
set(_MOCK_FILES
|
||||||
|
"${_MOCK_DIR}/mock_server.py"
|
||||||
|
"${_MOCK_DIR}/gen_mock_data.py"
|
||||||
|
"${_MOCK_DIR}/gen_fetch_transactions.py"
|
||||||
|
"${_MOCK_DIR}/inject_random.py"
|
||||||
|
"${_MOCK_DIR}/run.sh"
|
||||||
|
"${_MOCK_DIR}/inject.sh"
|
||||||
|
"${_MOCK_DIR}/README.md"
|
||||||
|
"${_MOCK_DIR}/WINDOWS.md"
|
||||||
|
"${_MOCK_DIR}/seed_profile.example.json"
|
||||||
|
"${_MOCK_DIR}/setup.ps1"
|
||||||
|
)
|
||||||
|
foreach(_f ${_MOCK_FILES})
|
||||||
|
add_custom_command(TARGET ARC POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:ARC>/tests/mock_server"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_f}" "$<TARGET_FILE_DIR:ARC>/tests/mock_server/"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
# Copy Tesseract, Leptonica, and their transitive dependency DLLs
|
# Copy Tesseract, Leptonica, and their transitive dependency DLLs
|
||||||
file(GLOB _tesseract_dlls "${VCPKG_BIN_DIR}/tesseract*.dll"
|
file(GLOB _tesseract_dlls "${VCPKG_BIN_DIR}/tesseract*.dll"
|
||||||
"${VCPKG_BIN_DIR}/leptonica*.dll"
|
"${VCPKG_BIN_DIR}/leptonica*.dll"
|
||||||
|
|||||||
@ -200,7 +200,7 @@ void CommonScript::updateTransactionData(
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
|
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) {
|
||||||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
||||||
|
|
||||||
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
||||||
@ -236,7 +236,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
|
|||||||
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||||||
profileBody["bank_name"] = appCode;
|
profileBody["bank_name"] = appCode;
|
||||||
profileBody["phone_number"] = phone;
|
profileBody["phone_number"] = phone;
|
||||||
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
|
const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount;
|
||||||
|
profileBody["balance"] = QString::number(qRound64(balance * 100));
|
||||||
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
||||||
profileBody["device_id"] = device.apiId;
|
profileBody["device_id"] = device.apiId;
|
||||||
profileBody["currency"] = currencyCode;
|
profileBody["currency"] = currencyCode;
|
||||||
|
|||||||
@ -47,7 +47,7 @@ protected:
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Создаёт bank_profile на сервере если ещё не создан
|
// Создаёт bank_profile на сервере если ещё не создан
|
||||||
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode);
|
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0);
|
||||||
|
|
||||||
bool m_running = true;
|
bool m_running = true;
|
||||||
ScreenXmlParser xmlScreenParser;
|
ScreenXmlParser xmlScreenParser;
|
||||||
|
|||||||
@ -235,7 +235,7 @@ void CommonScript::updateTransactionData(
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
|
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) {
|
||||||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
||||||
|
|
||||||
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
||||||
@ -270,7 +270,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
|
|||||||
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||||||
profileBody["bank_name"] = appCode;
|
profileBody["bank_name"] = appCode;
|
||||||
profileBody["phone_number"] = phone;
|
profileBody["phone_number"] = phone;
|
||||||
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
|
const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount;
|
||||||
|
profileBody["balance"] = QString::number(qRound64(balance * 100));
|
||||||
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
||||||
profileBody["device_id"] = device.apiId;
|
profileBody["device_id"] = device.apiId;
|
||||||
profileBody["currency"] = currencyCode;
|
profileBody["currency"] = currencyCode;
|
||||||
|
|||||||
@ -45,7 +45,7 @@ protected:
|
|||||||
const QString &newDesc
|
const QString &newDesc
|
||||||
);
|
);
|
||||||
|
|
||||||
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode);
|
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0);
|
||||||
|
|
||||||
bool m_running = true;
|
bool m_running = true;
|
||||||
ScreenXmlParser xmlScreenParser;
|
ScreenXmlParser xmlScreenParser;
|
||||||
|
|||||||
@ -51,9 +51,11 @@ void GetProfileInfoScript::doStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Парсим баланс с домашнего экрана
|
// Парсим баланс с домашнего экрана
|
||||||
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
|
const double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen();
|
||||||
qDebug() << "[Dushanbe::GetProfileInfo] balance:" << balance;
|
qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance:" << parsedBalance;
|
||||||
BankProfileDAO::updateAmount(app.id, balance);
|
if (parsedBalance > 0.0) {
|
||||||
|
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
||||||
|
}
|
||||||
|
|
||||||
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
||||||
// Это первый clickable ImageView в верхней панели
|
// Это первый clickable ImageView в верхней панели
|
||||||
@ -105,7 +107,7 @@ void GetProfileInfoScript::doStart() {
|
|||||||
BankProfileDAO::updateProfileInfo(app.id, fullName, phone, "");
|
BankProfileDAO::updateProfileInfo(app.id, fullName, phone, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
createBankProfileIfNeeded(device.id, m_appCode);
|
createBankProfileIfNeeded(device.id, m_appCode, parsedBalance);
|
||||||
|
|
||||||
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
const QList<MaterialInfo> allAccounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||||
QList<QPair<MaterialInfo, Node>> accountsToPost;
|
QList<QPair<MaterialInfo, Node>> accountsToPost;
|
||||||
|
|||||||
@ -330,7 +330,7 @@ void CommonScript::updateTransactionData(
|
|||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode) {
|
void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) {
|
||||||
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode);
|
||||||
|
|
||||||
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) {
|
||||||
@ -366,7 +366,8 @@ void CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QStr
|
|||||||
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||||||
profileBody["bank_name"] = appCode;
|
profileBody["bank_name"] = appCode;
|
||||||
profileBody["phone_number"] = phone;
|
profileBody["phone_number"] = phone;
|
||||||
profileBody["balance"] = QString::number(qRound64(app.amount * 100));
|
const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount;
|
||||||
|
profileBody["balance"] = QString::number(qRound64(balance * 100));
|
||||||
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
profileBody["state"] = (app.status == "active") ? "enabled" : "disabled";
|
||||||
profileBody["device_id"] = device.apiId;
|
profileBody["device_id"] = device.apiId;
|
||||||
profileBody["currency"] = currencyCode;
|
profileBody["currency"] = currencyCode;
|
||||||
|
|||||||
@ -58,7 +58,7 @@ protected:
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Создаёт bank_profile на сервере если ещё не создан
|
// Создаёт bank_profile на сервере если ещё не создан
|
||||||
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode);
|
void createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance = -1.0);
|
||||||
|
|
||||||
// Проверяет что приложение ещё запущено на устройстве
|
// Проверяет что приложение ещё запущено на устройстве
|
||||||
static bool isAppRunning(const QString &deviceId, const QString &packageName);
|
static bool isAppRunning(const QString &deviceId, const QString &packageName);
|
||||||
|
|||||||
@ -71,6 +71,23 @@ void GetProfileInfoScript::doStart() {
|
|||||||
QThread::msleep(500);
|
QThread::msleep(500);
|
||||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
|
||||||
|
// Парсим баланс с домашнего экрана
|
||||||
|
const QList<MaterialInfo> parsedCards = xmlScreenParser.parseCardsOnHomeScreen();
|
||||||
|
double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount;
|
||||||
|
qDebug() << "[GetProfileInfo] parseCardsOnHomeScreen returned"
|
||||||
|
<< parsedCards.size() << "cards, balance:" << parsedBalance;
|
||||||
|
// Fallback: если парсинг не нашёл баланс, берём из materials
|
||||||
|
if (parsedBalance <= 0.0) {
|
||||||
|
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||||
|
for (const auto &acc : accounts) {
|
||||||
|
if (acc.amount > 0.0) { parsedBalance = acc.amount; break; }
|
||||||
|
}
|
||||||
|
qDebug() << "[GetProfileInfo] Fallback balance from materials:" << parsedBalance;
|
||||||
|
}
|
||||||
|
if (parsedBalance > 0.0) {
|
||||||
|
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
||||||
|
}
|
||||||
|
|
||||||
// Находим имя пользователя в верхней части домашнего экрана и тапаем по нему
|
// Находим имя пользователя в верхней части домашнего экрана и тапаем по нему
|
||||||
Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight);
|
Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight);
|
||||||
if (userNameNode.isEmpty()) {
|
if (userNameNode.isEmpty()) {
|
||||||
@ -118,7 +135,7 @@ void GetProfileInfoScript::doStart() {
|
|||||||
qWarning() << m_error;
|
qWarning() << m_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем bank_profile на сервер
|
// Отправляем bank_profile на сервер — используем свежепарсенные данные
|
||||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||||
|
|
||||||
const QMap<QString, int> currencyCodes = {
|
const QMap<QString, int> currencyCodes = {
|
||||||
@ -133,7 +150,7 @@ void GetProfileInfoScript::doStart() {
|
|||||||
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
|
||||||
profileBody["bank_name"] = m_appCode;
|
profileBody["bank_name"] = m_appCode;
|
||||||
profileBody["phone_number"] = phone;
|
profileBody["phone_number"] = phone;
|
||||||
profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100));
|
profileBody["balance"] = QString::number(qRound64(parsedBalance * 100));
|
||||||
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
|
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
|
||||||
profileBody["device_id"] = device.apiId;
|
profileBody["device_id"] = device.apiId;
|
||||||
profileBody["currency"] = currencyCode;
|
profileBody["currency"] = currencyCode;
|
||||||
|
|||||||
@ -63,8 +63,17 @@ void LoginAndCheckAccountsScript::doStart() {
|
|||||||
if (!m_resultScreenshot.isEmpty())
|
if (!m_resultScreenshot.isEmpty())
|
||||||
emit screenshotReady(m_resultScreenshot);
|
emit screenshotReady(m_resultScreenshot);
|
||||||
|
|
||||||
// Создаём bank profile если ещё не создан
|
// Парсим баланс с домашнего экрана
|
||||||
createBankProfileIfNeeded(device.id, m_appCode);
|
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||||
|
const QList<MaterialInfo> parsedCards = xmlScreenParser.parseCardsOnHomeScreen();
|
||||||
|
double parsedBalance = parsedCards.isEmpty() ? 0.0 : parsedCards[0].amount;
|
||||||
|
qDebug() << "[LoginAndCheck] Parsed balance:" << parsedBalance << "cards:" << parsedCards.size();
|
||||||
|
if (parsedBalance > 0.0) {
|
||||||
|
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаём/обновляем bank profile на сервере
|
||||||
|
createBankProfileIfNeeded(device.id, m_appCode, parsedBalance);
|
||||||
|
|
||||||
if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) {
|
if (goToAllProducts(m_deviceId, device.screenWidth, device.screenHeight)) {
|
||||||
QList<QPair<MaterialInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
|
QList<QPair<MaterialInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
|
||||||
|
|||||||
85
build_x64.bat
Normal file
85
build_x64.bat
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
set PROJECT_DIR=%~dp0
|
||||||
|
set BUILD_DIR=%PROJECT_DIR%build_x64
|
||||||
|
set RELEASE_DIR=%BUILD_DIR%\Release
|
||||||
|
set QT_DIR=C:\Qt\6.9.0\msvc2022_64
|
||||||
|
set WINDEPLOYQT=%QT_DIR%\bin\windeployqt.exe
|
||||||
|
set CONFIG_FILE=%PROJECT_DIR%config.ini
|
||||||
|
set TG_BOT_TOKEN=8256716069:AAFgZRB_0Y6KTpqFQmCxIlZiWdY5m2dR2D8
|
||||||
|
set TG_CHAT_ID=-5177086220
|
||||||
|
|
||||||
|
for /f "tokens=2 delims==" %%V in ('findstr /i "version=" "%CONFIG_FILE%"') do set OLD_VERSION=%%V
|
||||||
|
set CURRENT_VERSION=%OLD_VERSION%
|
||||||
|
|
||||||
|
if "%~1"=="-up-version" (
|
||||||
|
echo === [0] Incrementing build version ===
|
||||||
|
for /f "tokens=1-3 delims=." %%A in ("!OLD_VERSION!") do (
|
||||||
|
set MAJOR=%%A
|
||||||
|
set MINOR=%%B
|
||||||
|
set /a PATCH=%%C+1
|
||||||
|
)
|
||||||
|
set CURRENT_VERSION=!MAJOR!.!MINOR!.!PATCH!
|
||||||
|
powershell -NoProfile -Command "(Get-Content '!CONFIG_FILE!') -replace 'version=!OLD_VERSION!', 'version=!CURRENT_VERSION!' | Set-Content '!CONFIG_FILE!'"
|
||||||
|
echo Version: !OLD_VERSION! -^> !CURRENT_VERSION!
|
||||||
|
) else (
|
||||||
|
echo Version: !CURRENT_VERSION!
|
||||||
|
)
|
||||||
|
|
||||||
|
set ZIP_NAME=ARC_x64_!CURRENT_VERSION!.zip
|
||||||
|
|
||||||
|
echo === [1/5] Configuring CMake for x64 Release ===
|
||||||
|
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
|
||||||
|
cd /d "%BUILD_DIR%"
|
||||||
|
cmake .. -G "Visual Studio 17 2022" -A x64 -DCMAKE_PREFIX_PATH="%QT_DIR%" -DCMAKE_BUILD_TYPE=Release
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: CMake configure failed
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === [2/5] Building Release ===
|
||||||
|
cmake --build . --config Release
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: Build failed
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === [3/5] Deploying Qt dependencies ===
|
||||||
|
"%WINDEPLOYQT%" "%RELEASE_DIR%\ARC.exe"
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: windeployqt failed
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === [4/5] Creating zip archive ===
|
||||||
|
cd /d "%PROJECT_DIR%"
|
||||||
|
if exist "%ZIP_NAME%" del "%ZIP_NAME%"
|
||||||
|
powershell -NoProfile -Command "Compress-Archive -Path '%RELEASE_DIR%\*' -DestinationPath '%ZIP_NAME%' -Force"
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: Failed to create zip archive
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo === [5/5] Uploading and sending link to Telegram ===
|
||||||
|
for /f "delims=" %%B in ('powershell -NoProfile -Command "[guid]::NewGuid().ToString('N').Substring(0,16)"') do set BIN_ID=arc-%%B
|
||||||
|
curl -s -H "Content-Type: application/octet-stream" -X POST --data-binary "@%PROJECT_DIR%%ZIP_NAME%" "https://filebin.net/!BIN_ID!/%ZIP_NAME%" > nul
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: Failed to upload archive
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
set DOWNLOAD_URL=https://filebin.net/!BIN_ID!/%ZIP_NAME%
|
||||||
|
echo Upload URL: !DOWNLOAD_URL!
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot%TG_BOT_TOKEN%/sendMessage" -d "chat_id=%TG_CHAT_ID%" -d "text=ARC x64 v!CURRENT_VERSION!: !DOWNLOAD_URL!"
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo ERROR: Failed to send message to Telegram
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === Build complete! ===
|
||||||
|
echo Version: %NEW_VERSION%
|
||||||
|
echo Archive: %PROJECT_DIR%%ZIP_NAME%
|
||||||
|
echo Download: !DOWNLOAD_URL!
|
||||||
|
endlocal
|
||||||
@ -3,10 +3,10 @@ list=ozon, black, dushanbe_city_bank
|
|||||||
|
|
||||||
[common]
|
[common]
|
||||||
currencies=USD, ARS, RUB, TJS
|
currencies=USD, ARS, RUB, TJS
|
||||||
version=0.0.1
|
version=0.0.9
|
||||||
|
|
||||||
[db]
|
[db]
|
||||||
dbPath=~/CLionProjects/ARCDeskProject/ARCDeskProject/database/app_data.db
|
dbPath=./database/app_data.db
|
||||||
|
|
||||||
[net]
|
[net]
|
||||||
#apiBase=http://93.183.75.134:8005
|
#apiBase=http://93.183.75.134:8005
|
||||||
@ -35,4 +35,4 @@ phone_code=7
|
|||||||
android_package_name=tj.dc.next1
|
android_package_name=tj.dc.next1
|
||||||
currency=TJS
|
currency=TJS
|
||||||
name=Dushanbe City
|
name=Dushanbe City
|
||||||
phone_code=992
|
phone_code=992
|
||||||
|
|||||||
@ -278,16 +278,12 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
|||||||
|
|
||||||
QJsonValue taskData;
|
QJsonValue taskData;
|
||||||
if (event.type == EventType::FetchProfile) {
|
if (event.type == EventType::FetchProfile) {
|
||||||
// FETCH_PROFILE всегда один объект. Amount читаем актуальный:
|
// FETCH_PROFILE: баланс из bank_profiles (скрипт обновляет через
|
||||||
// - для Ozon скрипт обновляет MaterialDAO → берём из account;
|
// BankProfileDAO::updateAmount), fallback на materials.
|
||||||
// - для остальных (Dushanbe и т.п.) скрипт обновляет BankProfileDAO
|
const BankProfileInfo freshApp = BankProfileDAO::getApplication(
|
||||||
// через updateAmount → берём из bank_profiles.
|
account.deviceId, event.bankName);
|
||||||
double freshAmount = account.amount;
|
double freshAmount = (freshApp.id != -1 && freshApp.amount > 0.0)
|
||||||
if (event.bankName != "ozon") {
|
? freshApp.amount : account.amount;
|
||||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(
|
|
||||||
account.deviceId, event.bankName);
|
|
||||||
if (freshApp.id != -1) freshAmount = freshApp.amount;
|
|
||||||
}
|
|
||||||
EventDAO::updateEventAmount(event.id, freshAmount);
|
EventDAO::updateEventAmount(event.id, freshAmount);
|
||||||
QJsonObject obj;
|
QJsonObject obj;
|
||||||
obj["bank_account_id"] = account.materialId;
|
obj["bank_account_id"] = account.materialId;
|
||||||
@ -569,8 +565,12 @@ void EventHandler::clearAll() {
|
|||||||
for (QThread *thread : m_threads) {
|
for (QThread *thread : m_threads) {
|
||||||
if (thread->isRunning()) {
|
if (thread->isRunning()) {
|
||||||
thread->requestInterruption();
|
thread->requestInterruption();
|
||||||
thread->terminate();
|
thread->quit();
|
||||||
thread->wait();
|
if (!thread->wait(3000)) {
|
||||||
|
qWarning() << "[EventHandler] Thread did not stop in 3s, terminating";
|
||||||
|
thread->terminate();
|
||||||
|
thread->wait(2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
thread->deleteLater();
|
thread->deleteLater();
|
||||||
}
|
}
|
||||||
|
|||||||
48
split_zip.ps1
Normal file
48
split_zip.ps1
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
param(
|
||||||
|
[string]$SourceDir,
|
||||||
|
[string]$OutputDir,
|
||||||
|
[string]$Prefix = "ARC_x64_part",
|
||||||
|
[long]$MaxSize = 48000000
|
||||||
|
)
|
||||||
|
|
||||||
|
$SourceDir = $SourceDir.Trim('"', "'", ' ')
|
||||||
|
$OutputDir = $OutputDir.Trim('"', "'", ' ')
|
||||||
|
|
||||||
|
$part = 1
|
||||||
|
$currentSize = 0
|
||||||
|
$currentFiles = @()
|
||||||
|
$allItems = Get-ChildItem -Path $SourceDir -Recurse -File | Sort-Object Length
|
||||||
|
|
||||||
|
function Flush-Part {
|
||||||
|
param($files, $partNum, $size)
|
||||||
|
$zipName = Join-Path $OutputDir "$Prefix$partNum.zip"
|
||||||
|
Write-Host "Creating part $partNum ($([math]::Round($size/1MB,1)) MB, $($files.Count) files)"
|
||||||
|
$tempDir = Join-Path $env:TEMP "arc_split_$partNum"
|
||||||
|
if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
|
||||||
|
foreach ($cf in $files) {
|
||||||
|
$dest = Join-Path $tempDir $cf.Rel
|
||||||
|
$destDir = Split-Path $dest -Parent
|
||||||
|
if (!(Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
|
||||||
|
Copy-Item $cf.Full $dest
|
||||||
|
}
|
||||||
|
Compress-Archive -Path (Join-Path $tempDir '*') -DestinationPath $zipName -Force
|
||||||
|
Remove-Item $tempDir -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($f in $allItems) {
|
||||||
|
$rel = $f.FullName.Substring($SourceDir.TrimEnd('\').Length)
|
||||||
|
if ($currentSize + $f.Length -gt $MaxSize -and $currentFiles.Count -gt 0) {
|
||||||
|
Flush-Part $currentFiles $part $currentSize
|
||||||
|
$part++
|
||||||
|
$currentSize = 0
|
||||||
|
$currentFiles = @()
|
||||||
|
}
|
||||||
|
$currentFiles += @{ Full = $f.FullName; Rel = $rel }
|
||||||
|
$currentSize += $f.Length
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentFiles.Count -gt 0) {
|
||||||
|
Flush-Part $currentFiles $part $currentSize
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done: $part part(s) created."
|
||||||
@ -1,20 +1,203 @@
|
|||||||
# Запуск тестов на Windows
|
# Запуск тестов на Windows
|
||||||
|
|
||||||
|
## Оглавление
|
||||||
|
|
||||||
|
1. [Быстрый старт](#1-быстрый-старт)
|
||||||
|
2. [Вспомогательные скрипты](#2-вспомогательные-скрипты)
|
||||||
|
3. [Генерация FETCH_TRANSACTIONS задач](#3-генерация-fetch_transactions-задач)
|
||||||
|
4. [Ручной запуск (пошагово)](#4-ручной-запуск-пошагово)
|
||||||
|
5. [Мониторинг и отчёт](#5-мониторинг-и-отчёт)
|
||||||
|
6. [Ручной инжект задач](#6-ручной-инжект-задач)
|
||||||
|
7. [ADB на Windows](#7-adb-на-windows)
|
||||||
|
8. [Типовые проблемы](#8-типовые-проблемы)
|
||||||
|
9. [Быстрый чеклист](#9-быстрый-чеклист)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Установка зависимостей
|
## 1. Быстрый старт
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Python 3.9+ (если нет — скачать с python.org, при установке включить «Add to PATH»)
|
cd tests\mock_server
|
||||||
|
powershell -ExecutionPolicy Bypass -File setup.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт `setup.ps1` автоматически:
|
||||||
|
1. Проверяет / устанавливает **Python 3.12+** (через winget)
|
||||||
|
2. Устанавливает **Flask**
|
||||||
|
3. Создаёт вспомогательные скрипты `gen_transactions.ps1` и `run_server.ps1`
|
||||||
|
4. Находит `config.ini` и проверяет **базу данных**
|
||||||
|
5. Удаляет старые сгенерированные данные (профиль + scenarios, пул не трогает)
|
||||||
|
6. Генерирует `default_profile.json` и FETCH_PROFILE scenarios из БД
|
||||||
|
7. Если порт занят — убивает старый процесс
|
||||||
|
8. Переключает `config.ini` на mock (`apiBase=http://127.0.0.1:8888`)
|
||||||
|
9. Показывает сводку: устройства, профили, материалы, пул транзакций
|
||||||
|
10. Запускает mock-сервер и открывает HTML-отчёт в браузере
|
||||||
|
|
||||||
|
При первом логине ARC мок автоматически ставит **FETCH_PROFILE** задачи
|
||||||
|
на каждый материал. **FETCH_TRANSACTIONS** задачи нужно генерировать отдельно
|
||||||
|
(см. [раздел 3](#3-генерация-fetch_transactions-задач)).
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Параметр | По умолчанию | Описание |
|
||||||
|
|---|---|---|
|
||||||
|
| `-Port` | 8888 | Порт mock-сервера |
|
||||||
|
| `-FtPerMaterial` | 3 | FETCH_TRANSACTIONS на материал при бутстрапе (из пула) |
|
||||||
|
| `-SkipInstall` | — | Пропустить установку Python/Flask |
|
||||||
|
| `-GenerateOnly` | — | Только сгенерировать данные, не запускать сервер |
|
||||||
|
| `-Clean` | — | Также очистить пул транзакций (`scenarios/pool/`) |
|
||||||
|
|
||||||
|
### Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Всё автоматически (установка + генерация + запуск)
|
||||||
|
.\setup.ps1
|
||||||
|
|
||||||
|
# На другом порту с 5 транзакциями на материал
|
||||||
|
.\setup.ps1 -Port 9999 -FtPerMaterial 5
|
||||||
|
|
||||||
|
# Пропустить установку (Python/Flask уже есть)
|
||||||
|
.\setup.ps1 -SkipInstall
|
||||||
|
|
||||||
|
# Только подготовить данные, сервер не запускать
|
||||||
|
.\setup.ps1 -GenerateOnly
|
||||||
|
|
||||||
|
# Полная очистка (включая пул транзакций)
|
||||||
|
.\setup.ps1 -Clean
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Вспомогательные скрипты
|
||||||
|
|
||||||
|
После первого запуска `setup.ps1` в папке `tests\mock_server\` появляются
|
||||||
|
скрипты с зашитым путём до Python — искать `python.exe` вручную не нужно:
|
||||||
|
|
||||||
|
| Скрипт | Назначение | Запуск |
|
||||||
|
|---|---|---|
|
||||||
|
| `gen_transactions.ps1` | Генерация FETCH_TRANSACTIONS с устройства | `powershell -ExecutionPolicy Bypass -File gen_transactions.ps1` |
|
||||||
|
| `run_server.ps1` | Запуск mock-сервера | `powershell -ExecutionPolicy Bypass -File run_server.ps1` |
|
||||||
|
| `setup.ps1` | Полная установка + генерация + запуск | `powershell -ExecutionPolicy Bypass -File setup.ps1` |
|
||||||
|
|
||||||
|
Скрипты принимают дополнительные аргументы, которые передаются в Python-скрипт:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Генерация с явным серийником
|
||||||
|
powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --serial 28291FDH300712
|
||||||
|
|
||||||
|
# Запуск сервера на другом порту
|
||||||
|
powershell -ExecutionPolicy Bypass -File run_server.ps1 --port 9999
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Генерация FETCH_TRANSACTIONS задач
|
||||||
|
|
||||||
|
FETCH_TRANSACTIONS задачи генерируются **только вручную** с живого устройства.
|
||||||
|
Скрипт подключается по ADB, проходит по списку транзакций на экране
|
||||||
|
(тап -> снятие суммы/даты -> назад) и сохраняет JSON-файлы в `scenarios/pool/`.
|
||||||
|
|
||||||
|
### Подготовка
|
||||||
|
|
||||||
|
1. Подключите устройство по USB
|
||||||
|
2. Убедитесь что ADB видит устройство:
|
||||||
|
```powershell
|
||||||
|
tools\adb\windows\adb.exe devices
|
||||||
|
# Должно показать серийник со статусом "device"
|
||||||
|
```
|
||||||
|
3. Откройте на устройстве банковское приложение на экране **списка транзакций**
|
||||||
|
- Ozon: раздел "Операции" -> список переводов/пополнений
|
||||||
|
- Dushanbe City Bank: раздел "Операции" -> список P2P операций
|
||||||
|
|
||||||
|
### Запуск
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Через вспомогательный скрипт (рекомендуется)
|
||||||
|
cd tests\mock_server
|
||||||
|
powershell -ExecutionPolicy Bypass -File gen_transactions.ps1
|
||||||
|
|
||||||
|
# Или напрямую (если Python в PATH)
|
||||||
|
python tests\mock_server\gen_fetch_transactions.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
1. Покажет меню с активными материалами:
|
||||||
|
```
|
||||||
|
Available materials (3):
|
||||||
|
1. Pixel 7 / ozon / Галина Терехина (material 30bc106c... serial=28291FDH30xxxx)
|
||||||
|
2. Pixel 7 / dushanbe_city_bank / Мехрубон (material 61d967d1...)
|
||||||
|
3. TECNO KM4 / ozon / Владислав Касаткин (material 49001bb5...)
|
||||||
|
Pick [1-3] or 'all':
|
||||||
|
```
|
||||||
|
2. Выберите нужный материал (номер) или `all` для всех
|
||||||
|
3. Скрипт попросит подтвердить что список транзакций открыт -> нажмите **Enter**
|
||||||
|
4. Автоматически пройдёт по каждой транзакции на экране (**не трогайте телефон!**)
|
||||||
|
5. Сохранит задачи в `scenarios/pool/`
|
||||||
|
6. Если mock-сервер запущен — сразу инжектнёт задачи в него
|
||||||
|
|
||||||
|
### Флаги
|
||||||
|
|
||||||
|
| Флаг | Описание |
|
||||||
|
|---|---|
|
||||||
|
| `--serial 28291FDH30xxxx` | Явный ADB-серийник (если несколько устройств) |
|
||||||
|
| `--kopecks 100` | Множитель суммы (по умолчанию 100: рубли -> копейки) |
|
||||||
|
| `--config path` | Путь к config.ini |
|
||||||
|
|
||||||
|
### Примеры
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Интерактивный выбор материала
|
||||||
|
powershell -ExecutionPolicy Bypass -File gen_transactions.ps1
|
||||||
|
|
||||||
|
# Для конкретного устройства
|
||||||
|
powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --serial 28291FDH300712
|
||||||
|
|
||||||
|
# С другим config
|
||||||
|
powershell -ExecutionPolicy Bypass -File gen_transactions.ps1 --config cmake-build-debug\config.ini
|
||||||
|
```
|
||||||
|
|
||||||
|
### Результат
|
||||||
|
|
||||||
|
После генерации в `scenarios/pool/` появятся файлы вида:
|
||||||
|
```
|
||||||
|
pixel_7_ozon_ft_01_plus_15000_143022.json
|
||||||
|
pixel_7_ozon_ft_02_minus_500_150133.json
|
||||||
|
tecno_km4_ozon_ft_01_plus_3000_091500.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Каждый файл содержит задачу с реальной суммой (в копейках) и временем (UTC):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"bank_name": "ozon",
|
||||||
|
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
||||||
|
"type": "FETCH_TRANSACTIONS",
|
||||||
|
"body": {
|
||||||
|
"amount": 1500000,
|
||||||
|
"created_at": "2026-04-10T11:30:22Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
При следующем запуске `setup.ps1` пул сохраняется (если не указать `-Clean`),
|
||||||
|
и мок будет раздавать задачи из него при бутстрапе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Ручной запуск (пошагово)
|
||||||
|
|
||||||
|
Если не хотите использовать `setup.ps1`:
|
||||||
|
|
||||||
|
### 4.1. Установка зависимостей
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Python 3.9+ (если нет — скачать с python.org, включить «Add to PATH»)
|
||||||
python --version
|
python --version
|
||||||
|
|
||||||
# Flask
|
# Flask
|
||||||
pip install flask
|
pip install flask
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 4.2. Подготовка config.ini
|
||||||
|
|
||||||
## 2. Подготовка config.ini
|
|
||||||
|
|
||||||
В `config.ini` (рядом с `ARC.exe`) переключить `apiBase`:
|
В `config.ini` (рядом с `ARC.exe`) переключить `apiBase`:
|
||||||
|
|
||||||
@ -23,12 +206,23 @@ pip install flask
|
|||||||
apiBase=http://127.0.0.1:8888
|
apiBase=http://127.0.0.1:8888
|
||||||
```
|
```
|
||||||
|
|
||||||
> Совет: держите отдельную копию `config.ini.mock`, а перед тестом
|
> Совет: держите копию `config.ini.mock`, а перед тестом
|
||||||
> `copy config.ini.mock config.ini`.
|
> `copy config.ini.mock config.ini`.
|
||||||
|
|
||||||
---
|
### 4.3. Генерация профиля из БД
|
||||||
|
|
||||||
## 3. Запуск mock-сервера
|
```powershell
|
||||||
|
# Из корня проекта
|
||||||
|
python tests\mock_server\gen_mock_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Создаёт `default_profile.json` и FETCH_PROFILE scenarios.
|
||||||
|
|
||||||
|
### 4.4. Генерация пула транзакций (опционально)
|
||||||
|
|
||||||
|
См. [раздел 3](#3-генерация-fetch_transactions-задач).
|
||||||
|
|
||||||
|
### 4.5. Запуск mock-сервера
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd tests\mock_server
|
cd tests\mock_server
|
||||||
@ -43,26 +237,23 @@ python mock_server.py --port 8888
|
|||||||
* Running on http://127.0.0.1:8888
|
* Running on http://127.0.0.1:8888
|
||||||
```
|
```
|
||||||
|
|
||||||
Флаги:
|
### 4.6. Запуск ARC
|
||||||
- `--ft-per-material 3` — сколько FETCH_TRANSACTIONS на материал (default 3)
|
|
||||||
- `--seed 42` — фиксированный RNG для воспроизводимости
|
|
||||||
|
|
||||||
---
|
В отдельном терминале:
|
||||||
|
|
||||||
## 4. Запуск ARC
|
|
||||||
|
|
||||||
В отдельном терминале (cmd/PowerShell):
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd build\Release
|
cd build\Release
|
||||||
ARC.exe
|
ARC.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
Или через CLion: Run → ARC.
|
Или через CLion: Run -> ARC.
|
||||||
|
|
||||||
При логине мок автоматически поставит задачи (1 FETCH_PROFILE + 3 FETCH_TRANSACTIONS
|
При логине мок автоматически ставит задачи:
|
||||||
на каждый материал). Задачи раздаются по 1 на material за поллинг — следующая
|
- 1 x FETCH_PROFILE на каждый материал (всегда)
|
||||||
только после результата предыдущей.
|
- N x FETCH_TRANSACTIONS из пула (если пул не пуст)
|
||||||
|
|
||||||
|
Задачи раздаются по 1 на material за поллинг — следующая только
|
||||||
|
после результата предыдущей.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -89,20 +280,25 @@ curl -s http://127.0.0.1:8888/_admin/report
|
|||||||
Invoke-WebRequest http://127.0.0.1:8888/_admin/report -OutFile report.json
|
Invoke-WebRequest http://127.0.0.1:8888/_admin/report -OutFile report.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
На главной странице отчёта (`/_admin/report/html`) отображаются:
|
||||||
|
- Счётчики задач (всего / выполнено / в очереди)
|
||||||
|
- **Загруженные данные** — устройства, профили, материалы с количеством задач в пуле (группировка по устройствам)
|
||||||
|
- Задачи с результатами, сгруппированные по устройствам
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Ручной инжект задач
|
## 6. Ручной инжект задач
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Одна задача
|
# Одна задача из файла
|
||||||
curl -X POST http://127.0.0.1:8888/_admin/inject ^
|
curl -X POST http://127.0.0.1:8888/_admin/inject ^
|
||||||
-H "Content-Type: application/json" ^
|
-H "Content-Type: application/json" ^
|
||||||
--data-binary @scenarios\tecno_ozon_fetch_profile.json
|
--data-binary @scenarios\pixel_7_ozon_fetch_profile.json
|
||||||
|
|
||||||
# 5 случайных из пула
|
# N случайных из пула
|
||||||
python inject_random.py --count 5
|
python inject_random.py --count 5
|
||||||
|
|
||||||
# Повторный бутстрап
|
# Повторный бутстрап (1 FP + N FT на material)
|
||||||
curl -X POST http://127.0.0.1:8888/_admin/bootstrap
|
curl -X POST http://127.0.0.1:8888/_admin/bootstrap
|
||||||
|
|
||||||
# Сброс (очистка очереди + результатов)
|
# Сброс (очистка очереди + результатов)
|
||||||
@ -113,15 +309,15 @@ curl -X POST http://127.0.0.1:8888/_admin/reset
|
|||||||
|
|
||||||
## 7. ADB на Windows
|
## 7. ADB на Windows
|
||||||
|
|
||||||
ARC использует ADB из `tools\adb\windows\`. Убедитесь, что:
|
ARC использует ADB из `tools\adb\windows\`. Проверка:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
tools\adb\windows\adb.exe devices
|
tools\adb\windows\adb.exe devices
|
||||||
```
|
```
|
||||||
|
|
||||||
показывает устройства в статусе `device`.
|
Должно показать устройства в статусе `device`.
|
||||||
|
|
||||||
Если ADB занят другим процессом (Android Studio, scrcpy), закройте его:
|
Если ADB занят другим процессом (Android Studio, scrcpy):
|
||||||
```powershell
|
```powershell
|
||||||
tools\adb\windows\adb.exe kill-server
|
tools\adb\windows\adb.exe kill-server
|
||||||
tools\adb\windows\adb.exe start-server
|
tools\adb\windows\adb.exe start-server
|
||||||
@ -130,37 +326,32 @@ tools\adb\windows\adb.exe devices
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Типовые проблемы (Windows-специфичные)
|
## 8. Типовые проблемы
|
||||||
|
|
||||||
- **`python` не найден** → использовать `py` или `python3`. Или добавить
|
| Проблема | Решение |
|
||||||
Python в PATH.
|
|---|---|
|
||||||
- **Порт 8888 занят** → `netstat -ano | findstr :8888`, затем
|
| `python` не найден | Запустите `setup.ps1` — установит автоматически. Или используйте скрипты `gen_transactions.ps1` / `run_server.ps1` — в них путь уже зашит |
|
||||||
`taskkill /PID <pid> /F`. Или запустить мок на другом порту:
|
| Порт 8888 занят | `setup.ps1` убивает старый процесс автоматически. Вручную: `netstat -ano \| findstr :8888` -> `taskkill /PID <pid> /F` |
|
||||||
`python mock_server.py --port 9999` и поменять `apiBase` в `config.ini`.
|
| `curl` нет | `Invoke-WebRequest` (PowerShell) или `winget install curl` |
|
||||||
- **`curl` нет** → использовать `Invoke-WebRequest` (PowerShell) или
|
| Firewall блокирует | Разрешить `python.exe` в Windows Firewall. Мок слушает только `127.0.0.1` |
|
||||||
установить curl через `winget install curl`.
|
| ARC не видит устройства | Проверить USB-драйверы, включить USB debugging, подтвердить RSA-ключ |
|
||||||
- **Firewall блокирует** → разрешить `python.exe` в Windows Firewall
|
| Кракозябры в консоли | `chcp 65001` перед запуском (UTF-8). Скрипты `.ps1` делают это автоматически |
|
||||||
(или отключить на время теста). Мок слушает только `127.0.0.1` — внешний
|
| BOM в config.ini | `setup.ps1` пишет без BOM. Если правили вручную — пересохраните в UTF-8 без BOM (VS Code) |
|
||||||
доступ не нужен.
|
| No active materials | БД пустая. Запустите ARC с боевым сервером, залогиньтесь, добавьте устройства |
|
||||||
- **ARC не видит устройства** → проверить USB-драйверы, включить
|
| Пул транзакций пуст | Сгенерируйте: [раздел 3](#3-генерация-fetch_transactions-задач) |
|
||||||
«USB debugging» на телефоне, подтвердить RSA-ключ.
|
| `No ADB devices match` | Устройства не подключены или serial не совпадает с БД. Проверьте `adb devices` |
|
||||||
- **Кодировка в консоли** → `chcp 65001` перед запуском мока (UTF-8),
|
| SSL ошибки при сборке | OpenSSL DLL копируются автоматически через CMake post-build |
|
||||||
иначе русские символы в логах могут быть кракозябрами.
|
|
||||||
- **SSL ошибки при сборке ARC** → убедиться, что OpenSSL DLL скопированы
|
|
||||||
в директорию сборки (`cmake --build` делает это автоматически через
|
|
||||||
post-build commands в CMakeLists.txt).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Быстрый чеклист
|
## 9. Быстрый чеклист
|
||||||
|
|
||||||
```
|
```
|
||||||
[ ] Python 3.9+ установлен, pip install flask
|
[ ] powershell -ExecutionPolicy Bypass -File setup.ps1
|
||||||
[ ] config.ini: apiBase=http://127.0.0.1:8888
|
[ ] Устройства в adb devices -> device
|
||||||
[ ] Устройства в adb devices → device
|
[ ] (опционально) powershell -ExecutionPolicy Bypass -File gen_transactions.ps1
|
||||||
[ ] Терминал 1: python mock_server.py --port 8888
|
[ ] ARC.exe запущен (отдельный терминал)
|
||||||
[ ] Терминал 2: ARC.exe
|
|
||||||
[ ] Браузер: http://127.0.0.1:8888/_admin/report/html
|
[ ] Браузер: http://127.0.0.1:8888/_admin/report/html
|
||||||
[ ] Дождаться completed = 12 в отчёте
|
[ ] Дождаться completed = всего в отчёте
|
||||||
[ ] Вернуть config.ini: apiBase=http://93.183.75.134:8005
|
[ ] Вернуть config.ini: apiBase=http://93.183.75.134:8005
|
||||||
```
|
```
|
||||||
|
|||||||
@ -95,6 +95,28 @@ def adb(serial, *args):
|
|||||||
return subprocess.run(cmd, check=True, capture_output=True)
|
return subprocess.run(cmd, check=True, capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_TO_BANK = {
|
||||||
|
"ru.ozon.fintech.finance": "ozon",
|
||||||
|
"tj.dc.next1": "dushanbe_city_bank",
|
||||||
|
"app.blackwallet.ar": "black",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_foreground_package(serial) -> str | None:
|
||||||
|
"""Return the package name of the currently focused app."""
|
||||||
|
try:
|
||||||
|
r = adb(serial, "shell", "dumpsys", "window", "windows")
|
||||||
|
for line in r.stdout.decode("utf-8", errors="replace").splitlines():
|
||||||
|
if "mCurrentFocus" in line or "mFocusedApp" in line:
|
||||||
|
# e.g. mCurrentFocus=Window{... u0 tj.dc.next1/tj.dc.next1.MainActivity}
|
||||||
|
m = re.search(r"u\d+\s+(\S+)/", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dump_xml(serial) -> str:
|
def dump_xml(serial) -> str:
|
||||||
adb(serial, "shell", "uiautomator", "dump", "/sdcard/_gen_ft.xml")
|
adb(serial, "shell", "uiautomator", "dump", "/sdcard/_gen_ft.xml")
|
||||||
r = adb(serial, "exec-out", "cat", "/sdcard/_gen_ft.xml")
|
r = adb(serial, "exec-out", "cat", "/sdcard/_gen_ft.xml")
|
||||||
@ -219,8 +241,13 @@ def short_label(amount, time_str):
|
|||||||
|
|
||||||
def collect_tasks(serial, bank, mid, device_tz, kopecks, prefix):
|
def collect_tasks(serial, bank, mid, device_tz, kopecks, prefix):
|
||||||
xml = dump_xml(serial)
|
xml = dump_xml(serial)
|
||||||
is_ozon = bank == "ozon" or is_ozon_list(xml)
|
# Detect bank by foreground app package first, fall back to XML heuristics
|
||||||
is_dush = "dushanbe" in bank or is_dushanbe_list(xml)
|
fg_pkg = get_foreground_package(serial)
|
||||||
|
fg_bank = PACKAGE_TO_BANK.get(fg_pkg) if fg_pkg else None
|
||||||
|
if fg_bank:
|
||||||
|
print(f"Foreground app: {fg_pkg} -> {fg_bank}")
|
||||||
|
is_dush = fg_bank == "dushanbe_city_bank" or "dushanbe" in bank or is_dushanbe_list(xml)
|
||||||
|
is_ozon = not is_dush and (fg_bank == "ozon" or bank == "ozon" or is_ozon_list(xml))
|
||||||
|
|
||||||
if not is_ozon and not is_dush:
|
if not is_ozon and not is_dush:
|
||||||
print("ERROR: not on a recognized transactions list", file=sys.stderr)
|
print("ERROR: not on a recognized transactions list", file=sys.stderr)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ Or specify config explicitly:
|
|||||||
python tests\\mock_server\\gen_mock_data.py --config C:\\path\\to\\config.ini
|
python tests\\mock_server\\gen_mock_data.py --config C:\\path\\to\\config.ini
|
||||||
|
|
||||||
On Windows:
|
On Windows:
|
||||||
python tests\mock_server\gen_mock_data.py
|
python tests\\mock_server\\gen_mock_data.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@ -30,7 +30,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
def resolve_db_path(config_path: Path) -> Path:
|
def resolve_db_path(config_path: Path) -> Path:
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(str(config_path), encoding="utf-8")
|
config.read(str(config_path), encoding="utf-8-sig")
|
||||||
raw = config.get("db", "dbPath", fallback="")
|
raw = config.get("db", "dbPath", fallback="")
|
||||||
if not raw:
|
if not raw:
|
||||||
print(f"ERROR: [db] dbPath not found in {config_path}", file=sys.stderr)
|
print(f"ERROR: [db] dbPath not found in {config_path}", file=sys.stderr)
|
||||||
@ -169,18 +169,6 @@ def generate(materials: list, out_dir: Path):
|
|||||||
)
|
)
|
||||||
print(f" wrote scenarios/{fname}")
|
print(f" wrote scenarios/{fname}")
|
||||||
|
|
||||||
# Also write a fetch_transactions scenario (empty body, for manual use)
|
|
||||||
ft_fname = f"{dev_short}_{bank}_fetch_transactions.json"
|
|
||||||
ft_task = {
|
|
||||||
"bank_name": bank,
|
|
||||||
"material_id": row["material_id"],
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {},
|
|
||||||
}
|
|
||||||
(scenarios_dir / ft_fname).write_text(
|
|
||||||
json.dumps(ft_task, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
devices_seen.setdefault(dev_short, []).append(
|
devices_seen.setdefault(dev_short, []).append(
|
||||||
f"{bank} ({row['material_id'][:8]}…)"
|
f"{bank} ({row['material_id'][:8]}…)"
|
||||||
)
|
)
|
||||||
|
|||||||
16
tests/mock_server/gen_transactions.ps1
Normal file
16
tests/mock_server/gen_transactions.ps1
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
$_configCandidates = @(
|
||||||
|
(Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "..\..\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "..\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "config.ini")
|
||||||
|
)
|
||||||
|
$_configPath = $null
|
||||||
|
foreach ($_c in $_configCandidates) {
|
||||||
|
$_resolved = [System.IO.Path]::GetFullPath($_c)
|
||||||
|
if (Test-Path $_resolved) { $_configPath = $_resolved; break }
|
||||||
|
}
|
||||||
|
if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 }
|
||||||
|
$_configArg = @("--config", $_configPath)
|
||||||
|
Write-Host "Config: $_configPath" -ForegroundColor Gray
|
||||||
|
& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args
|
||||||
@ -93,7 +93,7 @@ def load_default_profile():
|
|||||||
if not p.exists():
|
if not p.exists():
|
||||||
print(f"[mock] no {p.name} found — profile starts empty")
|
print(f"[mock] no {p.name} found — profile starts empty")
|
||||||
return
|
return
|
||||||
data = json.loads(p.read_text())
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
_bank_profiles = data.get("bank_profiles", [])
|
_bank_profiles = data.get("bank_profiles", [])
|
||||||
mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles)
|
mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles)
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ def load_pool():
|
|||||||
count = 0
|
count = 0
|
||||||
for f in sorted(pool_dir.glob("*.json")):
|
for f in sorted(pool_dir.glob("*.json")):
|
||||||
try:
|
try:
|
||||||
task = json.loads(f.read_text())
|
task = json.loads(f.read_text(encoding="utf-8"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[mock] skipping {f.name}: {e}")
|
print(f"[mock] skipping {f.name}: {e}")
|
||||||
continue
|
continue
|
||||||
@ -559,6 +559,97 @@ def render_entry_html(e) -> str:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_profile_section() -> str:
|
||||||
|
"""Render loaded bank profiles / materials as an HTML info block."""
|
||||||
|
import html as _html
|
||||||
|
if not _bank_profiles:
|
||||||
|
return '<div class="profile-section muted">Профиль не загружен</div>'
|
||||||
|
|
||||||
|
# Group by device
|
||||||
|
devices: dict = {}
|
||||||
|
for bp in _bank_profiles:
|
||||||
|
dev_id = bp.get("device_id", "?")
|
||||||
|
label = bp.get("device_label") or (dev_id[:8] + "…")
|
||||||
|
if dev_id not in devices:
|
||||||
|
devices[dev_id] = {"label": label, "profiles": []}
|
||||||
|
devices[dev_id]["profiles"].append(bp)
|
||||||
|
|
||||||
|
device_blocks = []
|
||||||
|
for dev_id, dev in devices.items():
|
||||||
|
label = _html.escape(dev["label"])
|
||||||
|
dev_mats = sum(len(bp.get("materials", [])) for bp in dev["profiles"])
|
||||||
|
bp_cards = []
|
||||||
|
for bp in dev["profiles"]:
|
||||||
|
bank = _html.escape(bp.get("bank_name", "?"))
|
||||||
|
first = bp.get("first_name") or ""
|
||||||
|
last = bp.get("last_name") or ""
|
||||||
|
name = _html.escape(f"{first} {last}".strip() or "—")
|
||||||
|
phone = _html.escape(bp.get("phone_number") or "—")
|
||||||
|
mats = bp.get("materials", [])
|
||||||
|
mat_rows = []
|
||||||
|
for m in mats:
|
||||||
|
mid = _html.escape(m.get("id", "?")[:12])
|
||||||
|
card = _html.escape(m.get("card_number") or "—")
|
||||||
|
cur = m.get("currency", "")
|
||||||
|
state = _html.escape(m.get("state", "?"))
|
||||||
|
# Count pool tasks for this material
|
||||||
|
pool_n = len(_pool_by_material.get(m.get("id", ""), []))
|
||||||
|
pool_badge = (f'<span class="pool-ok">{pool_n}</span>'
|
||||||
|
if pool_n > 0 else '<span class="muted">0</span>')
|
||||||
|
mat_rows.append(
|
||||||
|
f'<tr><td class="mono">{mid}…</td>'
|
||||||
|
f'<td>{card}</td><td>{cur}</td>'
|
||||||
|
f'<td><span class="badge-sm {state}">{state}</span></td>'
|
||||||
|
f'<td>{pool_badge}</td></tr>'
|
||||||
|
)
|
||||||
|
mat_table = ""
|
||||||
|
if mat_rows:
|
||||||
|
mat_table = (
|
||||||
|
'<table class="mat-table">'
|
||||||
|
'<tr><th>Material ID</th><th>Карта</th><th>Валюта</th>'
|
||||||
|
'<th>Статус</th><th>Пул</th></tr>'
|
||||||
|
+ "".join(mat_rows)
|
||||||
|
+ '</table>'
|
||||||
|
)
|
||||||
|
bp_cards.append(
|
||||||
|
f'<div class="bp-card">'
|
||||||
|
f'<div class="bp-header">'
|
||||||
|
f'<span class="bp-bank">{bank}</span>'
|
||||||
|
f'<span class="bp-name">{name}</span>'
|
||||||
|
f'<span class="bp-phone">{phone}</span>'
|
||||||
|
f'</div>'
|
||||||
|
f'{mat_table}'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
device_blocks.append(
|
||||||
|
f'<div class="dev-group">'
|
||||||
|
f'<div class="dev-group-header">'
|
||||||
|
f'<span class="dev-label">{label}</span>'
|
||||||
|
f'<span class="muted">{len(dev["profiles"])} профилей, {dev_mats} материалов</span>'
|
||||||
|
f'</div>'
|
||||||
|
f'<div class="bp-grid">{"".join(bp_cards)}</div>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
pool_count = sum(len(v) for v in _pool_by_material.values())
|
||||||
|
pool_info = (f'<span class="pool-ok">{pool_count} задач в пуле</span>'
|
||||||
|
if pool_count > 0
|
||||||
|
else '<span class="pool-empty">пул пуст</span>')
|
||||||
|
total_mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles)
|
||||||
|
|
||||||
|
return (
|
||||||
|
'<details class="profile-section" open>'
|
||||||
|
'<summary>Загруженные данные '
|
||||||
|
f'<span class="muted">({len(devices)} устройств, '
|
||||||
|
f'{len(_bank_profiles)} профилей, '
|
||||||
|
f'{total_mats} материалов, '
|
||||||
|
f'{pool_info})</span></summary>'
|
||||||
|
+ "".join(device_blocks)
|
||||||
|
+ '</details>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_report_html(entries, summary) -> str:
|
def render_report_html(entries, summary) -> str:
|
||||||
import html as _html
|
import html as _html
|
||||||
|
|
||||||
@ -697,6 +788,82 @@ def render_report_html(entries, summary) -> str:
|
|||||||
.group h2 .g-done {{ color: var(--completed); font-weight: 700; }}
|
.group h2 .g-done {{ color: var(--completed); font-weight: 700; }}
|
||||||
.group h2 .g-total {{ color: var(--muted); }}
|
.group h2 .g-total {{ color: var(--muted); }}
|
||||||
.updating {{ opacity: 0.6; }}
|
.updating {{ opacity: 0.6; }}
|
||||||
|
/* ─── Profile section ─── */
|
||||||
|
.profile-section {{
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 10px 14px;
|
||||||
|
}}
|
||||||
|
.profile-section summary {{
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}}
|
||||||
|
.bp-grid {{
|
||||||
|
display: flex; flex-wrap: wrap; gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}}
|
||||||
|
.bp-card {{
|
||||||
|
background: #161921;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 320px;
|
||||||
|
flex: 1;
|
||||||
|
}}
|
||||||
|
.bp-header {{
|
||||||
|
display: flex; gap: 10px; align-items: center;
|
||||||
|
margin-bottom: 8px; flex-wrap: wrap;
|
||||||
|
}}
|
||||||
|
.dev-label {{ color: #ffb86c; font-weight: 700; }}
|
||||||
|
.bp-bank {{ color: #9cdcfe; font-weight: 600; }}
|
||||||
|
.bp-name {{ color: var(--text); }}
|
||||||
|
.bp-phone {{ color: var(--muted); font-family: monospace; font-size: 12px; }}
|
||||||
|
.mat-table {{
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}}
|
||||||
|
.mat-table th {{
|
||||||
|
text-align: left;
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}}
|
||||||
|
.mat-table td {{
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-bottom: 1px solid #1f232d;
|
||||||
|
}}
|
||||||
|
.mono {{ font-family: "SF Mono", Menlo, Consolas, monospace; }}
|
||||||
|
.badge-sm {{
|
||||||
|
padding: 1px 6px; border-radius: 8px;
|
||||||
|
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
||||||
|
}}
|
||||||
|
.badge-sm.enabled {{ background: var(--completed); color: #0a1a0a; }}
|
||||||
|
.badge-sm.disabled {{ background: var(--muted); color: #1a1d25; }}
|
||||||
|
.pool-ok {{ color: var(--completed); }}
|
||||||
|
.pool-empty {{ color: var(--pending); }}
|
||||||
|
.dev-group {{
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #13161d;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}}
|
||||||
|
.dev-group-header {{
|
||||||
|
display: flex; align-items: baseline; gap: 10px;
|
||||||
|
padding-bottom: 8px; margin-bottom: 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}}
|
||||||
|
.dev-group-header .dev-label {{ font-size: 14px; }}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -707,6 +874,7 @@ def render_report_html(entries, summary) -> str:
|
|||||||
<div class="cell pending"><div class="n">{summary['pending']}</div>в очереди</div>
|
<div class="cell pending"><div class="n">{summary['pending']}</div>в очереди</div>
|
||||||
<div class="cell unsolicited"><div class="n">{summary['unsolicited']}</div>незапрошенные</div>
|
<div class="cell unsolicited"><div class="n">{summary['unsolicited']}</div>незапрошенные</div>
|
||||||
</div>
|
</div>
|
||||||
|
{render_profile_section()}
|
||||||
<div id="report-body">
|
<div id="report-body">
|
||||||
{body_rows}
|
{body_rows}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
tests/mock_server/run_server.ps1
Normal file
2
tests/mock_server/run_server.ps1
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
& "C:\Users\User\AppData\Local\Programs\Python\Python312-arm64\python.exe" "$PSScriptRoot\mock_server.py" --port 8888 @args
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_PROFILE",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_PROFILE",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -100,
|
|
||||||
"created_at": "2026-04-10T09:39:15Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_01_embedded_minus_1"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -100,
|
|
||||||
"created_at": "2026-04-05T14:31:21Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_02_minus_1_2131"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -100,
|
|
||||||
"created_at": "2026-04-05T14:25:48Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_03_minus_1_2125"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -100,
|
|
||||||
"created_at": "2026-04-05T11:36:06Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_04_minus_1_1836"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -100,
|
|
||||||
"created_at": "2026-04-05T11:19:43Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_05_minus_1_1819"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "dushanbe_city_bank",
|
|
||||||
"material_id": "61d967d1-8d3e-4d05-ba01-8c8ab327b945",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 100,
|
|
||||||
"created_at": "2026-04-03T13:52:06Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_dushanbe_06_plus_1_2052"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 10000,
|
|
||||||
"created_at": "2026-04-11T10:39:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_01_evgeny_plus_100"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -10000,
|
|
||||||
"created_at": "2026-04-10T09:40:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_02_vlad_minus_100"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -30000,
|
|
||||||
"created_at": "2026-04-08T08:34:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_03_vlad_minus_300"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 10000,
|
|
||||||
"created_at": "2026-04-08T08:14:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_04_vlad_plus_100"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 10000,
|
|
||||||
"created_at": "2026-04-08T08:02:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_05_vlad_plus_100_b"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "30bc106c-6935-4f4b-85f3-f1a06adc5c13",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 10000,
|
|
||||||
"created_at": "2026-04-06T13:54:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "pixel7_ozon_06_evgeny_plus_100_b"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 10000,
|
|
||||||
"created_at": "2026-04-10T09:40:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_01_galina_plus_100"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 30000,
|
|
||||||
"created_at": "2026-04-08T08:34:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_02_galina_plus_300"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -10000,
|
|
||||||
"created_at": "2026-04-08T08:32:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_03_vlad_minus_100_a"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -10000,
|
|
||||||
"created_at": "2026-04-08T08:23:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_04_vlad_minus_100_b"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -10000,
|
|
||||||
"created_at": "2026-04-08T08:14:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_05_galina_minus_100_a"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": -10000,
|
|
||||||
"created_at": "2026-04-08T08:02:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_06_galina_minus_100_b"
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {
|
|
||||||
"amount": 55000,
|
|
||||||
"created_at": "2026-03-20T14:36:00Z"
|
|
||||||
},
|
|
||||||
"_tag": "tecno_ozon_07_vlad_plus_550"
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_PROFILE",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"bank_name": "ozon",
|
|
||||||
"material_id": "49001bb5-3b24-478d-b913-c2242aea0732",
|
|
||||||
"type": "FETCH_TRANSACTIONS",
|
|
||||||
"body": {}
|
|
||||||
}
|
|
||||||
552
tests/mock_server/setup.ps1
Normal file
552
tests/mock_server/setup.ps1
Normal file
@ -0,0 +1,552 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Установка зависимостей, генерация тестовых данных и запуск mock-сервера.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Скрипт выполняет:
|
||||||
|
1. Проверяет/устанавливает Python 3.12+
|
||||||
|
2. Устанавливает Flask (pip install flask)
|
||||||
|
3. Находит config.ini и БД
|
||||||
|
4. Проверяет наличие БД и активных данных (devices, bank_profiles, materials)
|
||||||
|
5. Генерирует default_profile.json и scenarios из БД
|
||||||
|
6. Переключает config.ini на mock (apiBase=http://127.0.0.1:PORT)
|
||||||
|
7. Показывает сводку по данным из БД
|
||||||
|
8. Запускает mock-сервер и открывает отчёт в браузере
|
||||||
|
|
||||||
|
.PARAMETER Port
|
||||||
|
Порт mock-сервера (default: 8888)
|
||||||
|
|
||||||
|
.PARAMETER FtPerMaterial
|
||||||
|
Сколько FETCH_TRANSACTIONS на материал при бутстрапе (default: 3)
|
||||||
|
|
||||||
|
.PARAMETER SkipInstall
|
||||||
|
Пропустить установку Python/Flask (если уже установлены)
|
||||||
|
|
||||||
|
.PARAMETER GenerateOnly
|
||||||
|
Только сгенерировать данные, не запускать сервер
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\setup.ps1
|
||||||
|
.\setup.ps1 -Port 9999 -FtPerMaterial 5
|
||||||
|
.\setup.ps1 -SkipInstall
|
||||||
|
.\setup.ps1 -GenerateOnly
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[int]$Port = 8888,
|
||||||
|
[int]$FtPerMaterial = 3,
|
||||||
|
[switch]$SkipInstall,
|
||||||
|
[switch]$GenerateOnly,
|
||||||
|
[switch]$Clean
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
$Host.UI.RawUI.WindowTitle = "ARC Mock Server Setup"
|
||||||
|
|
||||||
|
# ─── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function Write-Step([string]$n, [string]$msg) {
|
||||||
|
Write-Host "`n=== [$n] $msg ===" -ForegroundColor Cyan
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Ok([string]$msg) {
|
||||||
|
Write-Host " OK: $msg" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Warn([string]$msg) {
|
||||||
|
Write-Host " WARN: $msg" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Err([string]$msg) {
|
||||||
|
Write-Host " ERROR: $msg" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
function Exit-WithError([string]$msg) {
|
||||||
|
Write-Err $msg
|
||||||
|
Write-Host "`nPress any key to exit..." -ForegroundColor Gray
|
||||||
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 1. Find script and project directories ───────────────────────────────
|
||||||
|
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
$configPath = $null
|
||||||
|
$configCandidates = @(
|
||||||
|
(Join-Path $scriptDir "..\..\cmake-build-debug\config.ini"),
|
||||||
|
(Join-Path $scriptDir "..\..\config.ini"),
|
||||||
|
(Join-Path $scriptDir "..\config.ini"),
|
||||||
|
(Join-Path $scriptDir "config.ini")
|
||||||
|
)
|
||||||
|
foreach ($c in $configCandidates) {
|
||||||
|
$resolved = [System.IO.Path]::GetFullPath($c)
|
||||||
|
if (Test-Path $resolved) {
|
||||||
|
$configPath = $resolved
|
||||||
|
$projectRoot = Split-Path -Parent $resolved
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $configPath) {
|
||||||
|
Exit-WithError "config.ini not found. Run from project root or Release/tests/mock_server/"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Project root : $projectRoot" -ForegroundColor Gray
|
||||||
|
Write-Host "Config : $configPath" -ForegroundColor Gray
|
||||||
|
Write-Host "Script dir : $scriptDir" -ForegroundColor Gray
|
||||||
|
|
||||||
|
# ─── 2. Python ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "1/6" "Python"
|
||||||
|
|
||||||
|
$pythonExe = $null
|
||||||
|
|
||||||
|
function Find-Python {
|
||||||
|
foreach ($cmd in @("python", "python3", "py")) {
|
||||||
|
try {
|
||||||
|
$ver = & $cmd --version 2>&1
|
||||||
|
if ($ver -match "Python\s+3\.(\d+)") {
|
||||||
|
if ([int]$Matches[1] -ge 9) {
|
||||||
|
return (Get-Command $cmd).Source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
$paths = @(
|
||||||
|
"$env:LOCALAPPDATA\Programs\Python\Python3*\python.exe",
|
||||||
|
"$env:LOCALAPPDATA\Programs\Python\Python31*\python.exe",
|
||||||
|
"C:\Python3*\python.exe"
|
||||||
|
)
|
||||||
|
foreach ($pattern in $paths) {
|
||||||
|
$found = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue |
|
||||||
|
Sort-Object Name -Descending | Select-Object -First 1
|
||||||
|
if ($found) {
|
||||||
|
$ver = & $found.FullName --version 2>&1
|
||||||
|
if ($ver -match "Python\s+3\.(\d+)" -and [int]$Matches[1] -ge 9) {
|
||||||
|
return $found.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipInstall) {
|
||||||
|
$pythonExe = Find-Python
|
||||||
|
if ($pythonExe) {
|
||||||
|
$ver = & $pythonExe --version 2>&1
|
||||||
|
Write-Ok "Found: $ver ($pythonExe)"
|
||||||
|
} else {
|
||||||
|
Write-Warn "Python not found - installing via winget..."
|
||||||
|
try {
|
||||||
|
winget install Python.Python.3.12 --accept-source-agreements --accept-package-agreements --silent 2>&1 | Out-Null
|
||||||
|
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" +
|
||||||
|
[System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
$pythonExe = Find-Python
|
||||||
|
if ($pythonExe) {
|
||||||
|
$ver = & $pythonExe --version 2>&1
|
||||||
|
Write-Ok "Installed: $ver"
|
||||||
|
} else {
|
||||||
|
Exit-WithError "Python installed but not in PATH. Restart terminal and try again."
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Exit-WithError "Failed to install Python. Install manually: https://python.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$pythonExe = Find-Python
|
||||||
|
if (-not $pythonExe) {
|
||||||
|
Exit-WithError "Python not found. Remove -SkipInstall or install manually."
|
||||||
|
}
|
||||||
|
Write-Ok "Python: $pythonExe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Generate helper scripts ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
$genFtPs1 = Join-Path $scriptDir "gen_transactions.ps1"
|
||||||
|
$runServerPs1 = Join-Path $scriptDir "run_server.ps1"
|
||||||
|
|
||||||
|
# Shared config-finder block for helper scripts
|
||||||
|
$configFinderBlock = @'
|
||||||
|
$_configCandidates = @(
|
||||||
|
(Join-Path $PSScriptRoot "..\..\cmake-build-debug\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "..\..\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "..\config.ini"),
|
||||||
|
(Join-Path $PSScriptRoot "config.ini")
|
||||||
|
)
|
||||||
|
$_configPath = $null
|
||||||
|
foreach ($_c in $_configCandidates) {
|
||||||
|
$_resolved = [System.IO.Path]::GetFullPath($_c)
|
||||||
|
if (Test-Path $_resolved) { $_configPath = $_resolved; break }
|
||||||
|
}
|
||||||
|
if (-not $_configPath) { Write-Host "ERROR: config.ini not found" -ForegroundColor Red; exit 1 }
|
||||||
|
$_configArg = @("--config", $_configPath)
|
||||||
|
'@
|
||||||
|
|
||||||
|
# gen_transactions.ps1
|
||||||
|
$genFtContent = @"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
$configFinderBlock
|
||||||
|
Write-Host "Config: `$_configPath" -ForegroundColor Gray
|
||||||
|
& "$pythonExe" "`$PSScriptRoot\gen_fetch_transactions.py" @_configArg @args
|
||||||
|
"@
|
||||||
|
[System.IO.File]::WriteAllText($genFtPs1, $genFtContent, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
# run_server.ps1
|
||||||
|
$runServerContent = @"
|
||||||
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
& "$pythonExe" "`$PSScriptRoot\mock_server.py" --port 8888 @args
|
||||||
|
"@
|
||||||
|
[System.IO.File]::WriteAllText($runServerPs1, $runServerContent, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
Write-Ok "Created gen_transactions.ps1, run_server.ps1"
|
||||||
|
|
||||||
|
# ─── 3. Flask ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "2/6" "Flask"
|
||||||
|
|
||||||
|
$flaskInstalled = $false
|
||||||
|
try {
|
||||||
|
$pipOut = & $pythonExe -m pip show flask 2>&1
|
||||||
|
if ($pipOut -match "Name:\s*Flask") {
|
||||||
|
$flaskVer = ($pipOut | Select-String "Version:\s*(.+)").Matches.Groups[1].Value
|
||||||
|
Write-Ok "Flask $flaskVer already installed"
|
||||||
|
$flaskInstalled = $true
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
if (-not $flaskInstalled) {
|
||||||
|
Write-Host " Installing Flask..." -ForegroundColor Gray
|
||||||
|
& $pythonExe -m pip install flask --quiet 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Exit-WithError "Failed to install Flask. Try: $pythonExe -m pip install flask"
|
||||||
|
}
|
||||||
|
Write-Ok "Flask installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 4. Database check ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "3/6" "Database"
|
||||||
|
|
||||||
|
$configContent = Get-Content $configPath -Encoding UTF8
|
||||||
|
$dbMatch = $configContent | Select-String "dbPath\s*=\s*(.+)"
|
||||||
|
if (-not $dbMatch) {
|
||||||
|
Exit-WithError "[db] dbPath not found in config.ini"
|
||||||
|
}
|
||||||
|
$dbPathRaw = $dbMatch.Matches.Groups[1].Value.Trim()
|
||||||
|
|
||||||
|
if ([System.IO.Path]::IsPathRooted($dbPathRaw)) {
|
||||||
|
$dbPath = $dbPathRaw
|
||||||
|
} else {
|
||||||
|
$dbPath = [System.IO.Path]::GetFullPath((Join-Path $projectRoot $dbPathRaw))
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " DB path: $dbPath" -ForegroundColor Gray
|
||||||
|
|
||||||
|
if (-not (Test-Path $dbPath)) {
|
||||||
|
Exit-WithError ("DB not found: $dbPath`n`n" +
|
||||||
|
"To create the DB:`n" +
|
||||||
|
" 1. Set apiBase to production server in config.ini`n" +
|
||||||
|
" 2. Run ARC.exe and log in`n" +
|
||||||
|
" 3. Devices, profiles and materials will appear in the DB`n" +
|
||||||
|
" 4. Re-run this script")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Ok "DB found: $dbPath"
|
||||||
|
|
||||||
|
# Write a temp Python helper that queries DB and outputs structured data
|
||||||
|
$dbHelperPy = Join-Path $env:TEMP "arc_db_check.py"
|
||||||
|
@'
|
||||||
|
import sqlite3, sys, os
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
db_path = sys.argv[1]
|
||||||
|
mode = sys.argv[2] if len(sys.argv) > 2 else "count"
|
||||||
|
pool_dir = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
|
||||||
|
if mode == "count":
|
||||||
|
cnt = conn.execute("""
|
||||||
|
SELECT COUNT(*) FROM devices d
|
||||||
|
JOIN bank_profiles bp ON bp.device_id = d.id
|
||||||
|
JOIN materials m ON m.device_id = d.id AND m.app_code = bp.code
|
||||||
|
WHERE bp.status = 'active' AND m.status = 'active'
|
||||||
|
AND m.material_id != '' AND bp.bank_profile_id != ''
|
||||||
|
""").fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
print(cnt)
|
||||||
|
sys.exit(0 if cnt > 0 else 1)
|
||||||
|
|
||||||
|
elif mode == "clean_events":
|
||||||
|
try:
|
||||||
|
cnt = conn.execute("SELECT COUNT(*) FROM events").fetchone()[0]
|
||||||
|
conn.execute("DELETE FROM events")
|
||||||
|
conn.commit()
|
||||||
|
print(cnt)
|
||||||
|
except Exception:
|
||||||
|
print(0)
|
||||||
|
conn.close()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
elif mode == "summary":
|
||||||
|
devices = conn.execute("SELECT id, name, api_id, adb_serial FROM devices").fetchall()
|
||||||
|
profiles = conn.execute("""
|
||||||
|
SELECT bp.code, bp.full_name, bp.phone, bp.status, d.name AS device
|
||||||
|
FROM bank_profiles bp JOIN devices d ON d.id = bp.device_id
|
||||||
|
WHERE bp.status = 'active' AND bp.bank_profile_id != ''
|
||||||
|
ORDER BY d.name, bp.code
|
||||||
|
""").fetchall()
|
||||||
|
materials = conn.execute("""
|
||||||
|
SELECT m.material_id, m.card_number, m.last_numbers, m.currency, m.status,
|
||||||
|
m.app_code, d.name AS device
|
||||||
|
FROM materials m JOIN devices d ON d.id = m.device_id
|
||||||
|
WHERE m.status = 'active' AND m.material_id != ''
|
||||||
|
ORDER BY d.name, m.app_code
|
||||||
|
""").fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("DEVICES|" + str(len(devices)))
|
||||||
|
for d in devices:
|
||||||
|
serial = d['adb_serial'] or '-'
|
||||||
|
api = d['api_id'] or '-'
|
||||||
|
print(f" DEV|{d['name']}|{serial}|{api}")
|
||||||
|
|
||||||
|
print("PROFILES|" + str(len(profiles)))
|
||||||
|
for p in profiles:
|
||||||
|
name = p['full_name'] or '-'
|
||||||
|
phone = p['phone'] or '-'
|
||||||
|
print(f" PRF|{p['device']}|{p['code']}|{name}|{phone}")
|
||||||
|
|
||||||
|
print("MATERIALS|" + str(len(materials)))
|
||||||
|
for m in materials:
|
||||||
|
card = m['card_number'] or ('****' + m['last_numbers'] if m['last_numbers'] else '-')
|
||||||
|
print(f" MAT|{m['device']}|{m['app_code']}|{card}|{m['currency']}|{m['material_id'][:12]}")
|
||||||
|
|
||||||
|
pool_n = 0
|
||||||
|
if pool_dir and os.path.isdir(pool_dir):
|
||||||
|
pool_n = len([f for f in os.listdir(pool_dir) if f.endswith('.json')])
|
||||||
|
print(f"POOL|{pool_n}")
|
||||||
|
'@ | Set-Content -Path $dbHelperPy -Encoding UTF8
|
||||||
|
|
||||||
|
# Check active materials count
|
||||||
|
$matCount = & $pythonExe $dbHelperPy $dbPath "count" 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Exit-WithError ("DB exists but no active materials found.`n`n" +
|
||||||
|
"To populate:`n" +
|
||||||
|
" 1. Set apiBase to production server`n" +
|
||||||
|
" 2. Run ARC.exe, log in, add devices and profiles`n" +
|
||||||
|
" 3. Re-run this script")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Ok "Active materials in DB: $matCount"
|
||||||
|
|
||||||
|
# Clean events table
|
||||||
|
$eventsCleared = & $pythonExe $dbHelperPy $dbPath "clean_events" 2>&1
|
||||||
|
if ($eventsCleared -and [int]$eventsCleared -gt 0) {
|
||||||
|
Write-Ok "Cleared $eventsCleared events from DB"
|
||||||
|
} else {
|
||||||
|
Write-Ok "Events table already empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 5. Generate mock data ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "4/6" "Generate test data"
|
||||||
|
|
||||||
|
# Clean old generated data
|
||||||
|
$oldProfile = Join-Path $scriptDir "default_profile.json"
|
||||||
|
if (Test-Path $oldProfile) {
|
||||||
|
Remove-Item $oldProfile -Force
|
||||||
|
Write-Host " Cleaned: default_profile.json" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
$scenariosDir = Join-Path $scriptDir "scenarios"
|
||||||
|
if (Test-Path $scenariosDir) {
|
||||||
|
$oldScenarios = Get-ChildItem "$scenariosDir\*.json" -ErrorAction SilentlyContinue
|
||||||
|
if ($oldScenarios) {
|
||||||
|
$oldScenarios | Remove-Item -Force
|
||||||
|
Write-Host " Cleaned: scenarios/ ($($oldScenarios.Count) files)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# -Clean: also wipe the transaction pool
|
||||||
|
$poolDir = Join-Path $scriptDir "scenarios\pool"
|
||||||
|
if ($Clean -and (Test-Path $poolDir)) {
|
||||||
|
$oldPool = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue
|
||||||
|
if ($oldPool) {
|
||||||
|
$oldPool | Remove-Item -Force
|
||||||
|
Write-Host " Cleaned: scenarios/pool/ ($($oldPool.Count) files)" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$genScript = Join-Path $scriptDir "gen_mock_data.py"
|
||||||
|
if (-not (Test-Path $genScript)) {
|
||||||
|
Exit-WithError "gen_mock_data.py not found in $scriptDir"
|
||||||
|
}
|
||||||
|
|
||||||
|
$genOutput = & $pythonExe $genScript --config $configPath 2>&1
|
||||||
|
$genOutput | ForEach-Object { Write-Host " $_" }
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Exit-WithError "Data generation failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
$profileJson = Join-Path $scriptDir "default_profile.json"
|
||||||
|
if (Test-Path $profileJson) {
|
||||||
|
Write-Ok "default_profile.json created"
|
||||||
|
} else {
|
||||||
|
Exit-WithError "default_profile.json was not created"
|
||||||
|
}
|
||||||
|
|
||||||
|
$poolDir = Join-Path $scriptDir "scenarios\pool"
|
||||||
|
$poolCount = 0
|
||||||
|
if (Test-Path $poolDir) {
|
||||||
|
$poolFiles = Get-ChildItem "$poolDir\*.json" -ErrorAction SilentlyContinue
|
||||||
|
if ($poolFiles) { $poolCount = $poolFiles.Count }
|
||||||
|
}
|
||||||
|
if ($poolCount -gt 0) {
|
||||||
|
Write-Ok "Transaction pool: $poolCount files"
|
||||||
|
} else {
|
||||||
|
Write-Warn "Transaction pool is empty - bootstrap will only create FETCH_PROFILE tasks"
|
||||||
|
Write-Host " To generate pool run: $pythonExe gen_fetch_transactions.py" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 6. Switch config to mock ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "5/6" "Switch config.ini to mock"
|
||||||
|
|
||||||
|
$mockBase = "http://127.0.0.1:$Port"
|
||||||
|
$configText = Get-Content $configPath -Raw -Encoding UTF8
|
||||||
|
|
||||||
|
# Find the active (non-commented) apiBase line
|
||||||
|
$configLines = Get-Content $configPath -Encoding UTF8
|
||||||
|
$activeIdx = -1
|
||||||
|
$currentBase = ""
|
||||||
|
for ($i = 0; $i -lt $configLines.Count; $i++) {
|
||||||
|
if ($configLines[$i] -match "^\s*apiBase\s*=\s*(.+)") {
|
||||||
|
$activeIdx = $i
|
||||||
|
$currentBase = $Matches[1].Trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($activeIdx -eq -1) {
|
||||||
|
Exit-WithError "apiBase not found in config.ini"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentBase -eq $mockBase) {
|
||||||
|
Write-Ok "apiBase already points to mock: $mockBase"
|
||||||
|
} else {
|
||||||
|
$backupPath = "$configPath.prod_backup"
|
||||||
|
if (-not (Test-Path $backupPath)) {
|
||||||
|
Copy-Item $configPath $backupPath
|
||||||
|
Write-Host " Backup saved: $backupPath" -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
# Comment out old line, insert mock line after it
|
||||||
|
$configLines[$activeIdx] = "#$($configLines[$activeIdx])"
|
||||||
|
$before = $configLines[0..$activeIdx]
|
||||||
|
$after = if ($activeIdx + 1 -lt $configLines.Count) { $configLines[($activeIdx + 1)..($configLines.Count - 1)] } else { @() }
|
||||||
|
$configLines = $before + @("apiBase=$mockBase") + $after
|
||||||
|
$newContent = ($configLines -join "`r`n") + "`r`n"
|
||||||
|
[System.IO.File]::WriteAllText($configPath, $newContent, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Write-Ok "apiBase switched: $currentBase -> $mockBase"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($GenerateOnly) {
|
||||||
|
Write-Host "`n=== Done (generate only) ===" -ForegroundColor Green
|
||||||
|
Write-Host "To start server: $pythonExe mock_server.py --port $Port" -ForegroundColor Gray
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── 7. Start mock server ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
Write-Step "6/6" "Start mock server"
|
||||||
|
|
||||||
|
# Kill old process on this port if any
|
||||||
|
$portCheck = netstat -ano 2>$null | Select-String ":${Port}\s.*LISTENING"
|
||||||
|
if ($portCheck) {
|
||||||
|
$oldPid = ($portCheck -split '\s+')[-1]
|
||||||
|
if ($oldPid -and $oldPid -ne "0") {
|
||||||
|
Write-Warn "Port $Port occupied by PID $oldPid - killing..."
|
||||||
|
taskkill /PID $oldPid /F 2>$null | Out-Null
|
||||||
|
Start-Sleep -Seconds 1
|
||||||
|
Write-Ok "Old process killed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mockScript = Join-Path $scriptDir "mock_server.py"
|
||||||
|
|
||||||
|
# ─── Summary banner with DB data ──────────────────────────────────────────
|
||||||
|
|
||||||
|
$poolDirArg = Join-Path $scriptDir "scenarios\pool"
|
||||||
|
$bannerLines = & $pythonExe $dbHelperPy $dbPath "summary" $poolDirArg 2>&1
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "================================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " ARC Mock Server - Summary " -ForegroundColor Cyan
|
||||||
|
Write-Host "================================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
foreach ($line in $bannerLines) {
|
||||||
|
$l = $line.ToString().Trim()
|
||||||
|
|
||||||
|
if ($l -match "^DEVICES\|(\d+)") {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Devices ($($Matches[1])):" -ForegroundColor White
|
||||||
|
Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray
|
||||||
|
Write-Host (" {0,-30} {1,-20} {2}" -f "Name", "ADB Serial", "API ID") -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
elseif ($l -match "^PROFILES\|(\d+)") {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Bank profiles ($($Matches[1])):" -ForegroundColor White
|
||||||
|
Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray
|
||||||
|
Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f "Device", "Bank", "Name", "Phone") -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
elseif ($l -match "^MATERIALS\|(\d+)") {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Materials ($($Matches[1])):" -ForegroundColor White
|
||||||
|
Write-Host " ----------------------------------------------------------" -ForegroundColor DarkGray
|
||||||
|
Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f "Device", "Bank", "Card", "Cur.", "Material ID") -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
elseif ($l -match "^POOL\|(\d+)") {
|
||||||
|
Write-Host ""
|
||||||
|
if ([int]$Matches[1] -gt 0) {
|
||||||
|
Write-Host " Transaction pool: $($Matches[1]) tasks" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " Transaction pool: empty (only FETCH_PROFILE at bootstrap)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($l -match "^\s*DEV\|([^|]+)\|([^|]+)\|(.+)") {
|
||||||
|
Write-Host (" {0,-30} {1,-20} {2}" -f $Matches[1], $Matches[2], $Matches[3]) -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
elseif ($l -match "^\s*PRF\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") {
|
||||||
|
Write-Host (" {0,-25} {1,-20} {2,-20} {3}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4]) -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
elseif ($l -match "^\s*MAT\|([^|]+)\|([^|]+)\|([^|]+)\|([^|]+)\|(.+)") {
|
||||||
|
Write-Host (" {0,-25} {1,-20} {2,-20} {3,-5} {4}" -f $Matches[1], $Matches[2], $Matches[3], $Matches[4], $Matches[5]) -ForegroundColor Gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " ==========================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Mock server: " -NoNewline -ForegroundColor White
|
||||||
|
Write-Host "http://127.0.0.1:$Port" -ForegroundColor Green
|
||||||
|
Write-Host " HTML report: " -NoNewline -ForegroundColor White
|
||||||
|
Write-Host "http://127.0.0.1:${Port}/_admin/report/html" -ForegroundColor Green
|
||||||
|
Write-Host " State: " -NoNewline -ForegroundColor White
|
||||||
|
Write-Host "http://127.0.0.1:${Port}/_admin/state" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " FT/material: $FtPerMaterial" -ForegroundColor Gray
|
||||||
|
Write-Host " Config: $configPath" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Ctrl+C to stop the server" -ForegroundColor Yellow
|
||||||
|
Write-Host " After testing restore: apiBase=http://93.183.75.134:8005" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Cleanup temp helper
|
||||||
|
Remove-Item $dbHelperPy -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
# Open report in browser
|
||||||
|
Start-Process "http://127.0.0.1:${Port}/_admin/report/html"
|
||||||
|
|
||||||
|
# Launch server (blocks until Ctrl+C)
|
||||||
|
& $pythonExe $mockScript --host 127.0.0.1 --port $Port --ft-per-material $FtPerMaterial
|
||||||
Loading…
Reference in New Issue
Block a user