Fix for windows
This commit is contained in:
parent
23c3f366b1
commit
c2fc0104c6
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ Makefile
|
||||
/Contents/
|
||||
/application.log
|
||||
/database/app_data.db
|
||||
/build_x64/
|
||||
|
||||
@ -216,6 +216,27 @@ if (WIN32)
|
||||
)
|
||||
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
|
||||
file(GLOB _tesseract_dlls "${VCPKG_BIN_DIR}/tesseract*.dll"
|
||||
"${VCPKG_BIN_DIR}/leptonica*.dll"
|
||||
|
||||
@ -200,7 +200,7 @@ void CommonScript::updateTransactionData(
|
||||
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);
|
||||
|
||||
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["bank_name"] = appCode;
|
||||
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["device_id"] = device.apiId;
|
||||
profileBody["currency"] = currencyCode;
|
||||
|
||||
@ -47,7 +47,7 @@ protected:
|
||||
);
|
||||
|
||||
// Создаёт 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;
|
||||
ScreenXmlParser xmlScreenParser;
|
||||
|
||||
@ -235,7 +235,7 @@ void CommonScript::updateTransactionData(
|
||||
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);
|
||||
|
||||
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["bank_name"] = appCode;
|
||||
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["device_id"] = device.apiId;
|
||||
profileBody["currency"] = currencyCode;
|
||||
|
||||
@ -45,7 +45,7 @@ protected:
|
||||
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;
|
||||
ScreenXmlParser xmlScreenParser;
|
||||
|
||||
@ -51,9 +51,11 @@ void GetProfileInfoScript::doStart() {
|
||||
}
|
||||
|
||||
// Парсим баланс с домашнего экрана
|
||||
const double balance = xmlScreenParser.parseBalanceFromHomeScreen();
|
||||
qDebug() << "[Dushanbe::GetProfileInfo] balance:" << balance;
|
||||
BankProfileDAO::updateAmount(app.id, balance);
|
||||
const double parsedBalance = xmlScreenParser.parseBalanceFromHomeScreen();
|
||||
qDebug() << "[Dushanbe::GetProfileInfo] Parsed balance:" << parsedBalance;
|
||||
if (parsedBalance > 0.0) {
|
||||
BankProfileDAO::updateAmount(app.id, parsedBalance);
|
||||
}
|
||||
|
||||
// Тапаем по кнопке меню (гамбургер, левый верхний угол)
|
||||
// Это первый clickable ImageView в верхней панели
|
||||
@ -105,7 +107,7 @@ void GetProfileInfoScript::doStart() {
|
||||
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);
|
||||
QList<QPair<MaterialInfo, Node>> accountsToPost;
|
||||
|
||||
@ -330,7 +330,7 @@ void CommonScript::updateTransactionData(
|
||||
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);
|
||||
|
||||
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["bank_name"] = appCode;
|
||||
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["device_id"] = device.apiId;
|
||||
profileBody["currency"] = currencyCode;
|
||||
|
||||
@ -58,7 +58,7 @@ protected:
|
||||
);
|
||||
|
||||
// Создаёт 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);
|
||||
|
||||
@ -71,6 +71,23 @@ void GetProfileInfoScript::doStart() {
|
||||
QThread::msleep(500);
|
||||
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);
|
||||
if (userNameNode.isEmpty()) {
|
||||
@ -118,7 +135,7 @@ void GetProfileInfoScript::doStart() {
|
||||
qWarning() << m_error;
|
||||
}
|
||||
|
||||
// Отправляем bank_profile на сервер
|
||||
// Отправляем bank_profile на сервер — используем свежепарсенные данные
|
||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(device.id, m_appCode);
|
||||
|
||||
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["bank_name"] = m_appCode;
|
||||
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["device_id"] = device.apiId;
|
||||
profileBody["currency"] = currencyCode;
|
||||
|
||||
@ -63,8 +63,17 @@ void LoginAndCheckAccountsScript::doStart() {
|
||||
if (!m_resultScreenshot.isEmpty())
|
||||
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)) {
|
||||
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]
|
||||
currencies=USD, ARS, RUB, TJS
|
||||
version=0.0.1
|
||||
version=0.0.9
|
||||
|
||||
[db]
|
||||
dbPath=~/CLionProjects/ARCDeskProject/ARCDeskProject/database/app_data.db
|
||||
dbPath=./database/app_data.db
|
||||
|
||||
[net]
|
||||
#apiBase=http://93.183.75.134:8005
|
||||
|
||||
@ -278,16 +278,12 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
|
||||
|
||||
QJsonValue taskData;
|
||||
if (event.type == EventType::FetchProfile) {
|
||||
// FETCH_PROFILE всегда один объект. Amount читаем актуальный:
|
||||
// - для Ozon скрипт обновляет MaterialDAO → берём из account;
|
||||
// - для остальных (Dushanbe и т.п.) скрипт обновляет BankProfileDAO
|
||||
// через updateAmount → берём из bank_profiles.
|
||||
double freshAmount = account.amount;
|
||||
if (event.bankName != "ozon") {
|
||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(
|
||||
account.deviceId, event.bankName);
|
||||
if (freshApp.id != -1) freshAmount = freshApp.amount;
|
||||
}
|
||||
// FETCH_PROFILE: баланс из bank_profiles (скрипт обновляет через
|
||||
// BankProfileDAO::updateAmount), fallback на materials.
|
||||
const BankProfileInfo freshApp = BankProfileDAO::getApplication(
|
||||
account.deviceId, event.bankName);
|
||||
double freshAmount = (freshApp.id != -1 && freshApp.amount > 0.0)
|
||||
? freshApp.amount : account.amount;
|
||||
EventDAO::updateEventAmount(event.id, freshAmount);
|
||||
QJsonObject obj;
|
||||
obj["bank_account_id"] = account.materialId;
|
||||
@ -569,8 +565,12 @@ void EventHandler::clearAll() {
|
||||
for (QThread *thread : m_threads) {
|
||||
if (thread->isRunning()) {
|
||||
thread->requestInterruption();
|
||||
thread->terminate();
|
||||
thread->wait();
|
||||
thread->quit();
|
||||
if (!thread->wait(3000)) {
|
||||
qWarning() << "[EventHandler] Thread did not stop in 3s, terminating";
|
||||
thread->terminate();
|
||||
thread->wait(2000);
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
## Оглавление
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
# Flask
|
||||
pip install flask
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Подготовка config.ini
|
||||
### 4.2. Подготовка config.ini
|
||||
|
||||
В `config.ini` (рядом с `ARC.exe`) переключить `apiBase`:
|
||||
|
||||
@ -23,12 +206,23 @@ pip install flask
|
||||
apiBase=http://127.0.0.1:8888
|
||||
```
|
||||
|
||||
> Совет: держите отдельную копию `config.ini.mock`, а перед тестом
|
||||
> Совет: держите копию `config.ini.mock`, а перед тестом
|
||||
> `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
|
||||
cd tests\mock_server
|
||||
@ -43,26 +237,23 @@ python mock_server.py --port 8888
|
||||
* Running on http://127.0.0.1:8888
|
||||
```
|
||||
|
||||
Флаги:
|
||||
- `--ft-per-material 3` — сколько FETCH_TRANSACTIONS на материал (default 3)
|
||||
- `--seed 42` — фиксированный RNG для воспроизводимости
|
||||
### 4.6. Запуск ARC
|
||||
|
||||
---
|
||||
|
||||
## 4. Запуск ARC
|
||||
|
||||
В отдельном терминале (cmd/PowerShell):
|
||||
В отдельном терминале:
|
||||
|
||||
```powershell
|
||||
cd build\Release
|
||||
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
|
||||
```
|
||||
|
||||
На главной странице отчёта (`/_admin/report/html`) отображаются:
|
||||
- Счётчики задач (всего / выполнено / в очереди)
|
||||
- **Загруженные данные** — устройства, профили, материалы с количеством задач в пуле (группировка по устройствам)
|
||||
- Задачи с результатами, сгруппированные по устройствам
|
||||
|
||||
---
|
||||
|
||||
## 6. Ручной инжект задач
|
||||
|
||||
```powershell
|
||||
# Одна задача
|
||||
# Одна задача из файла
|
||||
curl -X POST http://127.0.0.1:8888/_admin/inject ^
|
||||
-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
|
||||
|
||||
# Повторный бутстрап
|
||||
# Повторный бутстрап (1 FP + N FT на material)
|
||||
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
|
||||
|
||||
ARC использует ADB из `tools\adb\windows\`. Убедитесь, что:
|
||||
ARC использует ADB из `tools\adb\windows\`. Проверка:
|
||||
|
||||
```powershell
|
||||
tools\adb\windows\adb.exe devices
|
||||
```
|
||||
|
||||
показывает устройства в статусе `device`.
|
||||
Должно показать устройства в статусе `device`.
|
||||
|
||||
Если ADB занят другим процессом (Android Studio, scrcpy), закройте его:
|
||||
Если ADB занят другим процессом (Android Studio, scrcpy):
|
||||
```powershell
|
||||
tools\adb\windows\adb.exe kill-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`, затем
|
||||
`taskkill /PID <pid> /F`. Или запустить мок на другом порту:
|
||||
`python mock_server.py --port 9999` и поменять `apiBase` в `config.ini`.
|
||||
- **`curl` нет** → использовать `Invoke-WebRequest` (PowerShell) или
|
||||
установить curl через `winget install curl`.
|
||||
- **Firewall блокирует** → разрешить `python.exe` в Windows Firewall
|
||||
(или отключить на время теста). Мок слушает только `127.0.0.1` — внешний
|
||||
доступ не нужен.
|
||||
- **ARC не видит устройства** → проверить USB-драйверы, включить
|
||||
«USB debugging» на телефоне, подтвердить RSA-ключ.
|
||||
- **Кодировка в консоли** → `chcp 65001` перед запуском мока (UTF-8),
|
||||
иначе русские символы в логах могут быть кракозябрами.
|
||||
- **SSL ошибки при сборке ARC** → убедиться, что OpenSSL DLL скопированы
|
||||
в директорию сборки (`cmake --build` делает это автоматически через
|
||||
post-build commands в CMakeLists.txt).
|
||||
| Проблема | Решение |
|
||||
|---|---|
|
||||
| `python` не найден | Запустите `setup.ps1` — установит автоматически. Или используйте скрипты `gen_transactions.ps1` / `run_server.ps1` — в них путь уже зашит |
|
||||
| Порт 8888 занят | `setup.ps1` убивает старый процесс автоматически. Вручную: `netstat -ano \| findstr :8888` -> `taskkill /PID <pid> /F` |
|
||||
| `curl` нет | `Invoke-WebRequest` (PowerShell) или `winget install curl` |
|
||||
| Firewall блокирует | Разрешить `python.exe` в Windows Firewall. Мок слушает только `127.0.0.1` |
|
||||
| ARC не видит устройства | Проверить USB-драйверы, включить USB debugging, подтвердить RSA-ключ |
|
||||
| Кракозябры в консоли | `chcp 65001` перед запуском (UTF-8). Скрипты `.ps1` делают это автоматически |
|
||||
| BOM в config.ini | `setup.ps1` пишет без BOM. Если правили вручную — пересохраните в UTF-8 без BOM (VS Code) |
|
||||
| No active materials | БД пустая. Запустите ARC с боевым сервером, залогиньтесь, добавьте устройства |
|
||||
| Пул транзакций пуст | Сгенерируйте: [раздел 3](#3-генерация-fetch_transactions-задач) |
|
||||
| `No ADB devices match` | Устройства не подключены или serial не совпадает с БД. Проверьте `adb devices` |
|
||||
| SSL ошибки при сборке | OpenSSL DLL копируются автоматически через CMake post-build |
|
||||
|
||||
---
|
||||
|
||||
## 9. Быстрый чеклист
|
||||
|
||||
```
|
||||
[ ] Python 3.9+ установлен, pip install flask
|
||||
[ ] config.ini: apiBase=http://127.0.0.1:8888
|
||||
[ ] Устройства в adb devices → device
|
||||
[ ] Терминал 1: python mock_server.py --port 8888
|
||||
[ ] Терминал 2: ARC.exe
|
||||
[ ] powershell -ExecutionPolicy Bypass -File setup.ps1
|
||||
[ ] Устройства в adb devices -> device
|
||||
[ ] (опционально) powershell -ExecutionPolicy Bypass -File gen_transactions.ps1
|
||||
[ ] ARC.exe запущен (отдельный терминал)
|
||||
[ ] Браузер: http://127.0.0.1:8888/_admin/report/html
|
||||
[ ] Дождаться completed = 12 в отчёте
|
||||
[ ] Дождаться completed = всего в отчёте
|
||||
[ ] Вернуть 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)
|
||||
|
||||
|
||||
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:
|
||||
adb(serial, "shell", "uiautomator", "dump", "/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):
|
||||
xml = dump_xml(serial)
|
||||
is_ozon = bank == "ozon" or is_ozon_list(xml)
|
||||
is_dush = "dushanbe" in bank or is_dushanbe_list(xml)
|
||||
# Detect bank by foreground app package first, fall back to XML heuristics
|
||||
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:
|
||||
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
|
||||
|
||||
On Windows:
|
||||
python tests\mock_server\gen_mock_data.py
|
||||
python tests\\mock_server\\gen_mock_data.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@ -30,7 +30,7 @@ from pathlib import Path
|
||||
|
||||
def resolve_db_path(config_path: Path) -> Path:
|
||||
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="")
|
||||
if not raw:
|
||||
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}")
|
||||
|
||||
# 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(
|
||||
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():
|
||||
print(f"[mock] no {p.name} found — profile starts empty")
|
||||
return
|
||||
data = json.loads(p.read_text())
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
_bank_profiles = data.get("bank_profiles", [])
|
||||
mats = sum(len(bp.get("materials", [])) for bp in _bank_profiles)
|
||||
|
||||
@ -117,7 +117,7 @@ def load_pool():
|
||||
count = 0
|
||||
for f in sorted(pool_dir.glob("*.json")):
|
||||
try:
|
||||
task = json.loads(f.read_text())
|
||||
task = json.loads(f.read_text(encoding="utf-8"))
|
||||
except Exception as e:
|
||||
print(f"[mock] skipping {f.name}: {e}")
|
||||
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:
|
||||
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-total {{ color: var(--muted); }}
|
||||
.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>
|
||||
</head>
|
||||
<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 unsolicited"><div class="n">{summary['unsolicited']}</div>незапрошенные</div>
|
||||
</div>
|
||||
{render_profile_section()}
|
||||
<div id="report-body">
|
||||
{body_rows}
|
||||
</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