15 Commits

Author SHA1 Message Date
13890fbbfc fix(release): create annotated tags on current commit
All checks were successful
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Successful in 3m30s
- create release tag as annotated (-a) with explicit gitea.sha target

- pass target_commitish to Gitea release action for stable ordering
2026-02-15 23:18:49 +03:00
d7494c1092 feat(update): add setup fallback action and bump 2.1.2
All checks were successful
Desktop CI / tests (push) Successful in 14s
Desktop Release / release (push) Successful in 3m29s
- add installer asset detection in update service

- add 'Download and install (setup)' action in update dialog

- bump app version to 2.1.2 and extend update service test
2026-02-15 23:11:15 +03:00
67f6910435 feat(installer): restore Russian UI and setup icon
All checks were successful
Desktop CI / tests (push) Successful in 17s
Desktop Release / release (push) Successful in 3m31s
- re-enable Russian language entry and Russian labels in Inno script

- restore SetupIconFile via MyIconFile define

- pass MyIconFile from build.py with explicit file existence check
2026-02-15 23:03:57 +03:00
2c502fe3bf fix(installer): remove Russian language dependency in ISCC
All checks were successful
Desktop CI / tests (push) Successful in 14s
Desktop Release / release (push) Successful in 3m43s
- keep English-only MessagesFile to avoid compiler language path issues on runner

- switch post-install run description to English ASCII
2026-02-15 22:48:57 +03:00
02078282bc fix(ci-installer): remove dynamic path defines for ISCC
Some checks failed
Desktop CI / tests (push) Successful in 12s
Desktop Release / release (push) Failing after 2m35s
- use static relative paths in .iss for source/output

- pass only version and /O override to ISCC

- add explicit source/script path diagnostics in build.py
2026-02-15 22:42:39 +03:00
b1ed97a826 fix(ci): write build log to runner temp and enforce repo cwd
Some checks failed
Desktop CI / tests (push) Successful in 15s
Desktop Release / release (push) Failing after 2m34s
- run build step from git toplevel directory

- store build.log under %RUNNER_TEMP% to avoid missing dist path

- guard log dump when file is absent
2026-02-15 22:38:27 +03:00
5be8ab9af7 fix(ci): improve release build diagnostics and encoding
Some checks failed
Desktop CI / tests (push) Successful in 17s
Desktop Release / release (push) Failing after 1m27s
- run build.py with UTF-8 env in release workflow

- capture full build output to dist/build.log and print it on failure

- extend ISCC output decoding in build.py with UTF-16 fallbacks
2026-02-15 22:34:03 +03:00
0f07fe250c fix(ci-installer): remove setup icon dependency for ISCC
Some checks failed
Desktop CI / tests (push) Successful in 12s
Desktop Release / release (push) Failing after 2m32s
- build Inno installer without SetupIconFile to avoid code 2 failures in runner

- drop MyIconFile define and pass only essential defines

- make task description ASCII-only to avoid encoding issues
2026-02-15 22:22:32 +03:00
fc0c98ee49 fix(installer): stabilize ISCC script and output decoding
Some checks failed
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Failing after 2m35s
- remove fragile preprocessor icon block in .iss

- decode ISCC stdout/stderr with cp1251/cp866 fallbacks for readable diagnostics
2026-02-15 22:16:13 +03:00
e22eac6de3 fix(ci): force PySide6 stubs in updater test
Some checks are pending
Desktop CI / tests (push) Successful in 13s
Desktop Release / release (push) Has started running
- replace any preloaded PySide6 modules with local stubs before importing updater_gui

- prevent headless linux failures from libGL dependency during unit tests
2026-02-15 22:14:48 +03:00
bf7e5e599e fix(installer): improve ISCC diagnostics and icon handling
Some checks failed
Desktop CI / tests (push) Failing after 13s
Desktop Release / release (push) Failing after 2m34s
- make SetupIconFile optional in Inno script

- print full ISCC stdout/stderr on build failure

- copy icon.ico into dist artifacts explicitly
2026-02-15 22:11:24 +03:00
d1714a86c7 fix(ci): avoid non-zero exit from iscc help probe
Some checks failed
Desktop CI / tests (push) Failing after 12s
Desktop Release / release (push) Failing after 2m35s
- stop calling iscc /? in Ensure Inno Setup step

- resolve compiler path via Get-Command/Test-Path and finish with exit 0
2026-02-15 22:04:54 +03:00
781bf679ff chore(version): bump to 2.1.1
Some checks failed
Desktop CI / tests (push) Failing after 12s
Desktop Release / release (push) Failing after 12s
- set APP_VERSION to 2.1.1
2026-02-15 22:03:46 +03:00
813dafd6b8 fix(updater,ci): headless tests and immediate app shutdown
Some checks failed
Desktop CI / tests (push) Failing after 14s
Desktop Release / release (push) Failing after 15s
- stub PySide6 in test_updater_gui to run on linux runner without libGL

- close main app immediately after launching updater, without blocking OK dialog
2026-02-15 22:01:51 +03:00
965d09d47c feat(installer): add Inno Setup packaging to release
- add installer/AnabasisManager.iss for per-user install without admin rights

- extend build.py to produce setup.exe via ISCC

- publish setup.exe and checksums in release workflow
2026-02-15 22:01:47 +03:00
8 changed files with 335 additions and 34 deletions

View File

@@ -38,6 +38,28 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt pyinstaller
- name: Ensure Inno Setup 6
shell: powershell
run: |
$isccPath = ""
$inPath = Get-Command iscc.exe -ErrorAction SilentlyContinue
if ($inPath) {
$isccPath = $inPath.Source
Write-Host "Inno Setup compiler found in PATH."
} elseif (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
"C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
$isccPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
Write-Host "Inno Setup compiler found in Program Files (x86)."
} elseif (Test-Path "C:\Program Files\Inno Setup 6\ISCC.exe") {
"C:\Program Files\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
$isccPath = "C:\Program Files\Inno Setup 6\ISCC.exe"
Write-Host "Inno Setup compiler found in Program Files."
} else {
throw "Inno Setup 6 is not installed on runner. Install Inno Setup and restart runner service."
}
Write-Host "Using ISCC: $isccPath"
exit 0
- name: Extract app version
id: extract_version
shell: powershell
@@ -92,18 +114,43 @@ jobs:
- name: Build release zip
if: env.CONTINUE == 'true'
shell: powershell
env:
PYTHONUTF8: "1"
PYTHONIOENCODING: "utf-8"
run: |
python build.py
$ErrorActionPreference = "Continue"
$repoRoot = (git rev-parse --show-toplevel).Trim()
if (-not [string]::IsNullOrWhiteSpace($repoRoot)) {
Set-Location $repoRoot
}
$logDir = Join-Path $env:RUNNER_TEMP "anabasis-build"
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
$buildLog = Join-Path $logDir "build.log"
python build.py *>&1 | Tee-Object -FilePath $buildLog
$code = $LASTEXITCODE
if ($code -ne 0) {
Write-Host "Build failed with exit code $code. Dumping build log:"
if (Test-Path $buildLog) {
Get-Content -Path $buildLog -Raw
} else {
Write-Host "Build log was not created: $buildLog"
}
exit $code
}
- name: Ensure archive exists
- name: Ensure artifacts exist
if: env.CONTINUE == 'true'
shell: powershell
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$archivePath = "dist/AnabasisManager-$version.zip"
$installerPath = "dist/AnabasisManager-setup-$version.exe"
if (-not (Test-Path $archivePath)) {
throw "Archive not found: $archivePath"
}
if (-not (Test-Path $installerPath)) {
throw "Installer not found: $installerPath"
}
- name: Generate SHA256 checksum
if: env.CONTINUE == 'true'
@@ -111,11 +158,14 @@ jobs:
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$archiveName = "AnabasisManager-$version.zip"
$archivePath = "dist/$archiveName"
$checksumPath = "dist/$archiveName.sha256"
$hash = (Get-FileHash -Path $archivePath -Algorithm SHA256).Hash.ToLower()
"$hash $archiveName" | Set-Content -Path $checksumPath -Encoding UTF8
Write-Host "Checksum created: $checksumPath"
$installerName = "AnabasisManager-setup-$version.exe"
foreach ($name in @($archiveName, $installerName)) {
$path = "dist/$name"
$checksumPath = "dist/$name.sha256"
$hash = (Get-FileHash -Path $path -Algorithm SHA256).Hash.ToLower()
"$hash $name" | Set-Content -Path $checksumPath -Encoding UTF8
Write-Host "Checksum created: $checksumPath"
}
- name: Configure git identity
if: env.CONTINUE == 'true'
@@ -130,9 +180,10 @@ jobs:
run: |
$version = "${{ steps.extract_version.outputs.version }}"
$tag = "v$version"
$sha = "${{ gitea.sha }}"
$tagLine = (git ls-remote --tags origin "refs/tags/$tag" | Select-Object -First 1)
if ([string]::IsNullOrWhiteSpace($tagLine)) {
git tag "$tag"
git tag -a "$tag" -m "Release $tag" "$sha"
git push origin "$tag"
} else {
Write-Host "Tag $tag already exists on origin, skipping tag push."
@@ -146,9 +197,12 @@ jobs:
repository: ${{ gitea.repository }}
token: ${{ secrets.API_TOKEN }}
tag_name: v${{ steps.extract_version.outputs.version }}
target_commitish: ${{ gitea.sha }}
name: Anabasis Manager ${{ steps.extract_version.outputs.version }}
body: |
Desktop release v${{ steps.extract_version.outputs.version }}
files: |
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip
dist/AnabasisManager-${{ steps.extract_version.outputs.version }}.zip.sha256
dist/AnabasisManager-setup-${{ steps.extract_version.outputs.version }}.exe
dist/AnabasisManager-setup-${{ steps.extract_version.outputs.version }}.exe.sha256

View File

@@ -1 +1 @@
APP_VERSION = "2.1.0"
APP_VERSION = "2.1.2"

154
build.py
View File

@@ -4,15 +4,17 @@ import subprocess
import sys
from app_version import APP_VERSION
# --- Конфигурация ---
# --- Configuration ---
APP_NAME = "AnabasisManager"
UPDATER_NAME = "AnabasisUpdater"
VERSION = APP_VERSION # Единая версия приложения
MAIN_SCRIPT = "main.py"
UPDATER_SCRIPT = "updater_gui.py"
ICON_PATH = "icon.ico"
INSTALLER_SCRIPT = os.path.join("installer", "AnabasisManager.iss")
DIST_DIR = os.path.join("dist", APP_NAME)
ARCHIVE_NAME = f"{APP_NAME}-{VERSION}" # Формат Название-Версия
INSTALLER_NAME = f"{APP_NAME}-setup-{VERSION}.exe"
SAFE_CLEAN_ROOT_FILES = {"main.py", "updater_gui.py", "requirements.txt", "build.py"}
REMOVE_LIST = [
"Qt6Pdf.dll", "Qt6PdfQuick.dll", "Qt6PdfWidgets.dll",
@@ -30,22 +32,38 @@ def write_version_marker():
os.makedirs(DIST_DIR, exist_ok=True)
with open(marker_path, "w", encoding="utf-8") as f:
f.write(str(VERSION).strip() + "\n")
print(f"[OK] Обновлен маркер версии: {marker_path}")
print(f"[OK] Version marker written: {marker_path}")
except Exception as e:
print(f"[ERROR] Не удалось записать version.txt: {e}")
print(f"[ERROR] Failed to write version.txt: {e}")
sys.exit(1)
def copy_icon_to_dist():
icon_abs_path = os.path.abspath(ICON_PATH)
if not os.path.exists(icon_abs_path):
print("[WARN] icon.ico not found, skipping icon copy into dist.")
return
try:
os.makedirs("dist", exist_ok=True)
os.makedirs(DIST_DIR, exist_ok=True)
shutil.copy2(icon_abs_path, os.path.join("dist", "icon.ico"))
shutil.copy2(icon_abs_path, os.path.join(DIST_DIR, "icon.ico"))
print("[OK] Icon copied to dist/icon.ico and dist/AnabasisManager/icon.ico")
except Exception as e:
print(f"[ERROR] Failed to copy icon.ico into dist: {e}")
sys.exit(1)
def ensure_project_root():
missing = [name for name in SAFE_CLEAN_ROOT_FILES if not os.path.exists(name)]
if missing:
print("[ERROR] Скрипт нужно запускать из корня проекта.")
print(f"[ERROR] Не найдены: {', '.join(missing)}")
print("[ERROR] Run this script from the project root.")
print(f"[ERROR] Missing files: {', '.join(missing)}")
sys.exit(1)
def run_build():
print(f"--- 1. Запуск PyInstaller для {APP_NAME} v{VERSION} ---")
print(f"--- 1. Running PyInstaller for {APP_NAME} v{VERSION} ---")
icon_abs_path = os.path.abspath(ICON_PATH)
has_icon = os.path.exists(icon_abs_path)
@@ -68,14 +86,14 @@ def run_build():
try:
subprocess.check_call(command)
print("\n[OK] Сборка PyInstaller завершена.")
print("\n[OK] PyInstaller build completed.")
except subprocess.CalledProcessError as e:
print(f"\n[ERROR] Ошибка при сборке: {e}")
print(f"\n[ERROR] Build failed: {e}")
sys.exit(1)
def run_updater_build():
print(f"\n--- 1.2 Сборка {UPDATER_NAME} ---")
print(f"\n--- 1.2 Building {UPDATER_NAME} ---")
icon_abs_path = os.path.abspath(ICON_PATH)
has_icon = os.path.exists(icon_abs_path)
updater_spec_dir = os.path.join("build", "updater_spec")
@@ -98,14 +116,14 @@ def run_updater_build():
command = [arg for arg in command if arg]
try:
subprocess.check_call(command)
print(f"[OK] {UPDATER_NAME} собран.")
print(f"[OK] {UPDATER_NAME} built.")
except subprocess.CalledProcessError as e:
print(f"[ERROR] Ошибка при сборке {UPDATER_NAME}: {e}")
print(f"[ERROR] Failed to build {UPDATER_NAME}: {e}")
sys.exit(1)
def run_cleanup():
print(f"\n--- 2. Оптимизация папки {APP_NAME} ---")
print(f"\n--- 2. Optimizing {APP_NAME} folder ---")
# Пытаемся найти папку PySide6 внутри сборки
pyside_path = os.path.join(DIST_DIR, "PySide6")
@@ -120,21 +138,116 @@ def run_cleanup():
shutil.rmtree(path)
else:
os.remove(path)
print(f"Удалено: {item}")
print(f"Removed: {item}")
except Exception as e:
print(f"Пропуск {item}: {e}")
print(f"Skipped {item}: {e}")
def create_archive():
print(f"\n--- 3. Создание архива {ARCHIVE_NAME}.zip ---")
print(f"\n--- 3. Creating archive {ARCHIVE_NAME}.zip ---")
try:
# Создаем zip-архив из папки DIST_DIR
# base_name - имя файла без расширения, format - 'zip', root_dir - что упаковываем
shutil.make_archive(os.path.join("dist", ARCHIVE_NAME), 'zip', DIST_DIR)
print(f"[OK] Архив создан: dist/{ARCHIVE_NAME}.zip")
print(f"[OK] Archive created: dist/{ARCHIVE_NAME}.zip")
except Exception as e:
print(f"[ERROR] Не удалось создать архив: {e}")
print(f"[ERROR] Failed to create archive: {e}")
def _find_iscc():
candidates = []
iscc_env = os.getenv("ISCC_PATH", "").strip()
if iscc_env:
candidates.append(iscc_env)
candidates.append(shutil.which("iscc"))
candidates.append(shutil.which("ISCC.exe"))
candidates.append(r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe")
candidates.append(r"C:\Program Files\Inno Setup 6\ISCC.exe")
for candidate in candidates:
if candidate and os.path.exists(candidate):
return candidate
return ""
def _decode_process_output(raw_bytes):
if raw_bytes is None:
return ""
if isinstance(raw_bytes, str):
return raw_bytes
for enc in ("utf-8-sig", "utf-16", "utf-16-le", "cp1251", "cp866", "latin-1"):
try:
return raw_bytes.decode(enc)
except Exception:
continue
return raw_bytes.decode("utf-8", errors="replace")
def build_installer():
print(f"\n--- 4. Building installer {INSTALLER_NAME} ---")
if os.name != "nt":
print("[INFO] Inno Setup installer is built only on Windows. Step skipped.")
return
if not os.path.exists(INSTALLER_SCRIPT):
print(f"[ERROR] Installer script not found: {INSTALLER_SCRIPT}")
sys.exit(1)
if not os.path.exists(DIST_DIR):
print(f"[ERROR] Build output folder not found: {DIST_DIR}")
sys.exit(1)
iscc_path = _find_iscc()
if not iscc_path:
print("[ERROR] Inno Setup Compiler (ISCC.exe) not found.")
print("[ERROR] Install Inno Setup 6 or set ISCC_PATH environment variable.")
sys.exit(1)
project_root = os.path.abspath(".")
source_dir = os.path.abspath(DIST_DIR)
output_dir = os.path.abspath("dist")
iss_path = os.path.abspath(INSTALLER_SCRIPT)
icon_path = os.path.abspath(ICON_PATH)
print(f"[INFO] ISCC source dir: {source_dir}")
print(f"[INFO] ISCC output dir: {output_dir}")
print(f"[INFO] ISCC script: {iss_path}")
print(f"[INFO] ISCC icon path: {icon_path}")
if not os.path.exists(source_dir):
print(f"[ERROR] Source dir does not exist: {source_dir}")
sys.exit(1)
if not os.path.exists(iss_path):
print(f"[ERROR] Installer script does not exist: {iss_path}")
sys.exit(1)
if not os.path.exists(icon_path):
print(f"[ERROR] Icon file does not exist: {icon_path}")
sys.exit(1)
command = [
iscc_path,
f"/DMyAppVersion={VERSION}",
f"/DMyIconFile={icon_path}",
f"/O{output_dir}",
iss_path,
]
try:
completed = subprocess.run(
command,
capture_output=True,
cwd=project_root,
check=False,
)
stdout_text = _decode_process_output(completed.stdout)
stderr_text = _decode_process_output(completed.stderr)
if stdout_text:
print(stdout_text.rstrip())
if stderr_text:
print(stderr_text.rstrip())
if completed.returncode != 0:
raise RuntimeError(f"ISCC exited with code {completed.returncode}")
installer_path = os.path.join("dist", INSTALLER_NAME)
if not os.path.exists(installer_path):
print(f"[ERROR] Installer was not created: {installer_path}")
sys.exit(1)
print(f"[OK] Installer created: {installer_path}")
except Exception as e:
print(f"[ERROR] Failed to build installer: {e}")
sys.exit(1)
if __name__ == "__main__":
@@ -147,10 +260,13 @@ if __name__ == "__main__":
run_build()
run_updater_build()
run_cleanup()
copy_icon_to_dist()
write_version_marker()
create_archive()
build_installer()
print("\n" + "=" * 30)
print("ПРОЦЕСС ЗАВЕРШЕН")
print(f"Файл для отправки: dist/{ARCHIVE_NAME}.zip")
print("BUILD COMPLETED")
print(f"Release archive: dist/{ARCHIVE_NAME}.zip")
print(f"Installer: dist/{INSTALLER_NAME}")
print("=" * 30)

View File

@@ -0,0 +1,42 @@
#define MyAppName "Anabasis Manager"
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#ifndef MyIconFile
#define MyIconFile "..\icon.ico"
#endif
[Setup]
AppId={{6CD9D6F2-4B95-4E9C-A8D8-2A9C8F6AA741}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher=Benya
DefaultDirName={localappdata}\Programs\Anabasis Manager
DefaultGroupName=Anabasis Manager
DisableProgramGroupPage=yes
PrivilegesRequired=lowest
OutputDir=..\dist
OutputBaseFilename=AnabasisManager-setup-{#MyAppVersion}
Compression=lzma2
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64compatible
UninstallDisplayIcon={app}\AnabasisManager.exe
SetupIconFile={#MyIconFile}
[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "Создать ярлык на рабочем столе"; GroupDescription: "Дополнительные задачи:"
[Files]
Source: "..\dist\AnabasisManager\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\Anabasis Manager"; Filename: "{app}\AnabasisManager.exe"
Name: "{autodesktop}\Anabasis Manager"; Filename: "{app}\AnabasisManager.exe"; Tasks: desktopicon
[Run]
Filename: "{app}\AnabasisManager.exe"; Description: "Запустить Anabasis Manager"; Flags: nowait postinstall skipifsilent

16
main.py
View File

@@ -407,6 +407,10 @@ class VkChatManager(QMainWindow):
)
update_now_button = message_box.addButton("Обновить сейчас", QMessageBox.ButtonRole.AcceptRole)
download_button = message_box.addButton("Скачать", QMessageBox.ButtonRole.AcceptRole)
setup_button = None
installer_url = result.get("installer_url")
if installer_url:
setup_button = message_box.addButton("Скачать и установить (setup)", QMessageBox.ButtonRole.AcceptRole)
releases_button = message_box.addButton("Релизы", QMessageBox.ButtonRole.ActionRole)
cancel_button = message_box.addButton("Позже", QMessageBox.ButtonRole.RejectRole)
message_box.setDefaultButton(update_now_button)
@@ -422,6 +426,9 @@ class VkChatManager(QMainWindow):
if release_url:
QDesktopServices.openUrl(QUrl(release_url))
return
if setup_button is not None and clicked is setup_button and installer_url:
QDesktopServices.openUrl(QUrl(installer_url))
return
if clicked is download_button and download_url:
QDesktopServices.openUrl(QUrl(download_url))
elif clicked in (download_button, releases_button) and release_url:
@@ -1212,12 +1219,9 @@ class VkChatManager(QMainWindow):
version=latest_version,
)
self._log_event("auto_update", f"Update {latest_version} started from {download_url}")
QMessageBox.information(
self,
"Обновление запущено",
"Обновление скачано. Открылось окно обновления.",
)
QApplication.instance().quit()
self.status_label.setText("Статус: обновление запущено, закрываю приложение...")
self.close()
QTimer.singleShot(0, QApplication.instance().quit)
return True
except Exception as e:
self._log_event("auto_update_failed", str(e), level="ERROR")

View File

@@ -71,6 +71,8 @@ def _extract_release_payload(release_data, repository_url, current_version):
download_url = ""
download_name = ""
checksum_url = ""
installer_url = ""
installer_name = ""
for asset in assets:
url = asset.get("browser_download_url", "")
if url.lower().endswith(".zip"):
@@ -81,6 +83,16 @@ def _extract_release_payload(release_data, repository_url, current_version):
download_url = assets[0].get("browser_download_url", "")
download_name = assets[0].get("name", "")
for asset in assets:
url = asset.get("browser_download_url", "")
name = asset.get("name", "")
name_lower = name.lower()
if installer_url:
break
if url.lower().endswith(".exe") and ("setup" in name_lower or "installer" in name_lower):
installer_url = url
installer_name = name
for asset in assets:
name = asset.get("name", "").lower()
if not name:
@@ -102,6 +114,8 @@ def _extract_release_payload(release_data, repository_url, current_version):
"release_url": html_url,
"download_url": download_url,
"download_name": download_name,
"installer_url": installer_url,
"installer_name": installer_name,
"checksum_url": checksum_url,
"has_update": _is_newer_version(latest_version, current_version),
}

View File

@@ -33,6 +33,7 @@ class UpdateServiceTests(unittest.TestCase):
"assets": [
{"name": "notes.txt", "browser_download_url": "https://example.com/notes.txt"},
{"name": "AnabasisManager-win64.zip", "browser_download_url": "https://example.com/app.zip"},
{"name": "AnabasisManager-setup-1.7.2.exe", "browser_download_url": "https://example.com/setup.exe"},
{"name": "AnabasisManager-win64.zip.sha256", "browser_download_url": "https://example.com/app.zip.sha256"},
],
}
@@ -43,6 +44,7 @@ class UpdateServiceTests(unittest.TestCase):
)
self.assertEqual(payload["latest_version"], "1.7.2")
self.assertEqual(payload["download_url"], "https://example.com/app.zip")
self.assertEqual(payload["installer_url"], "https://example.com/setup.exe")
self.assertEqual(payload["checksum_url"], "https://example.com/app.zip.sha256")
self.assertTrue(payload["has_update"])

View File

@@ -1,10 +1,79 @@
import importlib.util
import sys
import tempfile
import unittest
from pathlib import Path
import types
def _install_pyside6_stubs():
pyside6_module = types.ModuleType("PySide6")
pyside6_module.__path__ = [] # treat as package
qtcore_module = types.ModuleType("PySide6.QtCore")
qtgui_module = types.ModuleType("PySide6.QtGui")
qtwidgets_module = types.ModuleType("PySide6.QtWidgets")
class _Signal:
def __init__(self, *args, **kwargs):
pass
def connect(self, *args, **kwargs):
pass
class _QObject:
pass
class _QThread:
def __init__(self, *args, **kwargs):
pass
class _QTimer:
@staticmethod
def singleShot(*args, **kwargs):
pass
class _QUrl:
@staticmethod
def fromLocalFile(path):
return path
class _QDesktopServices:
@staticmethod
def openUrl(*args, **kwargs):
return True
class _Widget:
def __init__(self, *args, **kwargs):
pass
qtcore_module.QObject = _QObject
qtcore_module.Qt = type("Qt", (), {})
qtcore_module.QThread = _QThread
qtcore_module.Signal = _Signal
qtcore_module.QTimer = _QTimer
qtcore_module.QUrl = _QUrl
qtgui_module.QDesktopServices = _QDesktopServices
qtwidgets_module.QApplication = _Widget
qtwidgets_module.QLabel = _Widget
qtwidgets_module.QProgressBar = _Widget
qtwidgets_module.QVBoxLayout = _Widget
qtwidgets_module.QWidget = _Widget
qtwidgets_module.QPushButton = _Widget
qtwidgets_module.QHBoxLayout = _Widget
# Force stubs even if real PySide6 was imported earlier in the process.
for mod_name in list(sys.modules.keys()):
if mod_name == "PySide6" or mod_name.startswith("PySide6."):
del sys.modules[mod_name]
sys.modules["PySide6"] = pyside6_module
sys.modules["PySide6.QtCore"] = qtcore_module
sys.modules["PySide6.QtGui"] = qtgui_module
sys.modules["PySide6.QtWidgets"] = qtwidgets_module
MODULE_PATH = Path("updater_gui.py")
_install_pyside6_stubs()
SPEC = importlib.util.spec_from_file_location("updater_gui_under_test", MODULE_PATH)
updater_gui = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(updater_gui)