Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35c9693740 |
11
README.md
11
README.md
@ -61,8 +61,10 @@
|
||||
|
||||
Приложение скачивает ZIP-пакет текущей платформы, проверяет его целостность и
|
||||
безопасность, завершает старый процесс, заменяет бинарник и все каталоги из
|
||||
пакета, а затем запускается заново. Перед заменой создаётся резервная копия для
|
||||
автоматического восстановления при ошибке. Файл `settings.json` и другие файлы,
|
||||
пакета, а затем запускается заново. Перед заменой создаётся резервная копия;
|
||||
она удаляется только после подтверждения запуска новой версии. Если запуск
|
||||
не удался, предыдущая версия восстанавливается, а сообщение записывается
|
||||
в `update-error.log`. Файл `settings.json` и другие файлы,
|
||||
которых нет в архиве, сохраняются. Для работы проверки релизы репозитория должны
|
||||
быть доступны без авторизации.
|
||||
|
||||
@ -71,6 +73,11 @@
|
||||
- Windows x86_64: `plate_config_app-windows-x86_64.zip`;
|
||||
- Linux x86_64: `plate_config_app-linux-x86_64.zip`.
|
||||
|
||||
Для перехода с уже установленной v2.0.1 на v2.0.2 обновите приложение вручную:
|
||||
закройте его, скачайте ZIP-пакет своей платформы и распакуйте содержимое в
|
||||
каталог приложения с заменой прежних файлов. `settings.json` удалять не нужно.
|
||||
Начиная с v2.0.2 последующие обновления можно устанавливать кнопкой в приложении.
|
||||
|
||||
---
|
||||
|
||||
## Интерфейс приложения
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"""Plate Config App package."""
|
||||
|
||||
__version__ = "2.0.1"
|
||||
__version__ = "2.0.2"
|
||||
|
||||
@ -2,11 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from .main_window import MainWindow
|
||||
from .services import settings_service
|
||||
from .services.update_service import signal_update_ready
|
||||
from .ui.icons import ICON_COLOR_PRIMARY, icon
|
||||
from .ui.theme import build_stylesheet, get_palette, create_qpalette
|
||||
|
||||
@ -27,4 +29,5 @@ def main() -> None:
|
||||
if not app_icon.isNull():
|
||||
window.setWindowIcon(app_icon)
|
||||
window.show()
|
||||
QTimer.singleShot(0, signal_update_ready)
|
||||
sys.exit(app.exec())
|
||||
|
||||
@ -31,6 +31,7 @@ DOWNLOAD_CHUNK_SIZE = 1024 * 1024
|
||||
MAX_DOWNLOAD_BYTES = 300 * 1024 * 1024
|
||||
MAX_UNPACKED_BYTES = 600 * 1024 * 1024
|
||||
MAX_ARCHIVE_MEMBERS = 10_000
|
||||
UPDATE_READY_ENV = "PLATE_CONFIG_UPDATE_READY_FILE"
|
||||
VERSION_PATTERN = re.compile(
|
||||
r"[vV]?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)"
|
||||
)
|
||||
@ -353,18 +354,29 @@ def current_executable() -> Path:
|
||||
return Path(sys.executable).resolve()
|
||||
|
||||
|
||||
def signal_update_ready() -> None:
|
||||
ready_file = os.environ.pop(UPDATE_READY_ENV, None)
|
||||
if ready_file:
|
||||
try:
|
||||
Path(ready_file).write_text(str(os.getpid()), encoding="ascii")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _write_windows_updater(script_path: Path) -> None:
|
||||
script = r'''param(
|
||||
[int]$ApplicationProcessId,
|
||||
[string]$Staging,
|
||||
[string]$Target,
|
||||
[string]$Backup
|
||||
[string]$Backup,
|
||||
[string]$ReadyFile
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
$applicationDirectory = Split-Path -Parent $Target
|
||||
$errorLog = Join-Path $applicationDirectory "update-error.log"
|
||||
$installed = [System.Collections.Generic.List[string]]::new()
|
||||
$backedUp = [System.Collections.Generic.List[string]]::new()
|
||||
$restarted = $null
|
||||
try {
|
||||
Wait-Process -Id $ApplicationProcessId -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||
@ -373,35 +385,81 @@ try {
|
||||
foreach ($entry in $entries) {
|
||||
$destination = Join-Path $applicationDirectory $entry.Name
|
||||
if (Test-Path -LiteralPath $destination) {
|
||||
Move-Item -LiteralPath $destination -Destination $Backup -Force
|
||||
$deadline = (Get-Date).AddSeconds(30)
|
||||
while ($true) {
|
||||
try {
|
||||
Move-Item -LiteralPath $destination -Destination $Backup -Force -ErrorAction Stop
|
||||
break
|
||||
} catch {
|
||||
if ((Get-Date) -ge $deadline) { throw }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
}
|
||||
$backedUp.Add($entry.Name)
|
||||
}
|
||||
Move-Item -LiteralPath $entry.FullName -Destination $destination -Force
|
||||
Move-Item -LiteralPath $entry.FullName -Destination $destination -Force -ErrorAction Stop
|
||||
$installed.Add($entry.Name)
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $Target -PathType Leaf)) {
|
||||
throw "В пакете обновления отсутствует исполняемый файл"
|
||||
}
|
||||
Start-Process -FilePath $Target -WorkingDirectory $applicationDirectory
|
||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
||||
$previousReadyFile = $env:PLATE_CONFIG_UPDATE_READY_FILE
|
||||
$previousResetEnvironment = $env:PYINSTALLER_RESET_ENVIRONMENT
|
||||
try {
|
||||
$env:PLATE_CONFIG_UPDATE_READY_FILE = $ReadyFile
|
||||
$env:PYINSTALLER_RESET_ENVIRONMENT = "1"
|
||||
$restarted = Start-Process -FilePath $Target -WorkingDirectory $applicationDirectory -PassThru
|
||||
} finally {
|
||||
$env:PLATE_CONFIG_UPDATE_READY_FILE = $previousReadyFile
|
||||
$env:PYINSTALLER_RESET_ENVIRONMENT = $previousResetEnvironment
|
||||
}
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
while (-not (Test-Path -LiteralPath $ReadyFile -PathType Leaf)) {
|
||||
if ($restarted.HasExited) { throw "Новая версия завершилась до запуска интерфейса" }
|
||||
if ((Get-Date) -ge $deadline) { throw "Новая версия не подтвердила запуск" }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
if ($restarted.HasExited) { throw "Новая версия завершилась сразу после запуска" }
|
||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force
|
||||
} catch {
|
||||
$failure = $_
|
||||
if ($restarted -and -not $restarted.HasExited) {
|
||||
& taskkill.exe /PID $restarted.Id /T /F *> $null
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
$rollbackErrors = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($name in $installed) {
|
||||
$destination = Join-Path $applicationDirectory $name
|
||||
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path -LiteralPath $destination) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction Stop
|
||||
} catch {
|
||||
$rollbackErrors.Add($_.ToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($name in $backedUp) {
|
||||
$backupItem = Join-Path $Backup $name
|
||||
$destination = Join-Path $applicationDirectory $name
|
||||
Remove-Item -LiteralPath $destination -Recurse -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path -LiteralPath $backupItem) {
|
||||
Move-Item -LiteralPath $backupItem -Destination $destination -Force
|
||||
try {
|
||||
Move-Item -LiteralPath $backupItem -Destination $destination -Force -ErrorAction Stop
|
||||
} catch {
|
||||
$rollbackErrors.Add($_.ToString())
|
||||
}
|
||||
}
|
||||
}
|
||||
Remove-Item -LiteralPath $ReadyFile -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $Staging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||
$_ | Out-File -LiteralPath $errorLog -Encoding utf8
|
||||
if ($rollbackErrors.Count -eq 0) {
|
||||
Remove-Item -LiteralPath $Backup -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@($failure.ToString()) + @($rollbackErrors) | Out-File -LiteralPath $errorLog -Encoding utf8
|
||||
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
'''
|
||||
@ -415,28 +473,40 @@ application_pid="$1"
|
||||
staging_path="$2"
|
||||
target_path="$3"
|
||||
backup_path="$4"
|
||||
ready_path="$5"
|
||||
application_dir="$(dirname -- "$target_path")"
|
||||
error_log="$application_dir/update-error.log"
|
||||
installed_file="$backup_path/.installed"
|
||||
backed_up_file="$backup_path/.backed_up"
|
||||
new_pid=""
|
||||
rollback() {
|
||||
exit_code="$?"
|
||||
trap - EXIT HUP INT TERM
|
||||
set +e
|
||||
if [ -n "$new_pid" ]; then
|
||||
kill "$new_pid" 2>/dev/null
|
||||
wait "$new_pid" 2>/dev/null
|
||||
fi
|
||||
rollback_failed=0
|
||||
if [ -f "$installed_file" ]; then
|
||||
while IFS= read -r name; do
|
||||
rm -rf -- "$application_dir/$name"
|
||||
rm -rf -- "$application_dir/$name" || rollback_failed=1
|
||||
done < "$installed_file"
|
||||
fi
|
||||
if [ -f "$backed_up_file" ]; then
|
||||
while IFS= read -r name; do
|
||||
rm -rf -- "$application_dir/$name"
|
||||
if [ -e "$backup_path/$name" ]; then
|
||||
mv -f -- "$backup_path/$name" "$application_dir/$name"
|
||||
mv -f -- "$backup_path/$name" "$application_dir/$name" || rollback_failed=1
|
||||
fi
|
||||
done < "$backed_up_file"
|
||||
fi
|
||||
rm -rf -- "$staging_path" "$backup_path"
|
||||
rm -f -- "$ready_path"
|
||||
rm -rf -- "$staging_path"
|
||||
if [ "$rollback_failed" -eq 0 ]; then
|
||||
rm -rf -- "$backup_path"
|
||||
fi
|
||||
printf '%s\n' "Не удалось заменить приложение" >"$error_log"
|
||||
rm -f -- "$0"
|
||||
if [ "$exit_code" -eq 0 ]; then
|
||||
exit_code=1
|
||||
fi
|
||||
@ -462,8 +532,25 @@ for source_path in "$staging_path"/*; do
|
||||
done
|
||||
test -f "$target_path"
|
||||
chmod 755 -- "$target_path"
|
||||
"$target_path" >/dev/null 2>&1 &
|
||||
rm -f -- "$ready_path"
|
||||
PLATE_CONFIG_UPDATE_READY_FILE="$ready_path" PYINSTALLER_RESET_ENVIRONMENT=1 "$target_path" >/dev/null 2>&1 &
|
||||
new_pid=$!
|
||||
attempts=0
|
||||
while [ ! -f "$ready_path" ]; do
|
||||
if ! kill -0 "$new_pid" 2>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
attempts=$((attempts + 1))
|
||||
if [ "$attempts" -ge 240 ]; then
|
||||
exit 1
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
if ! kill -0 "$new_pid" 2>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
trap - EXIT HUP INT TERM
|
||||
rm -f -- "$ready_path"
|
||||
rm -rf -- "$backup_path" "$staging_path"
|
||||
rm -f -- "$0"
|
||||
'''
|
||||
@ -494,6 +581,7 @@ def launch_update(
|
||||
|
||||
staging_path = target.parent / f".plate_config_update-{updater_id}"
|
||||
backup_path = target.parent / f".plate_config_backup-{updater_id}"
|
||||
ready_path = target.parent / f".plate_config_ready-{updater_id}"
|
||||
script_path: Path | None = None
|
||||
try:
|
||||
_extract_update_archive(archive_path, staging_path, target.name)
|
||||
@ -522,6 +610,7 @@ def launch_update(
|
||||
str(staging_path),
|
||||
str(target),
|
||||
str(backup_path),
|
||||
str(ready_path),
|
||||
],
|
||||
close_fds=True,
|
||||
creationflags=creation_flags,
|
||||
@ -543,6 +632,7 @@ def launch_update(
|
||||
str(staging_path),
|
||||
str(target),
|
||||
str(backup_path),
|
||||
str(ready_path),
|
||||
],
|
||||
cwd=str(target.parent),
|
||||
close_fds=True,
|
||||
|
||||
@ -5,7 +5,6 @@ from pathlib import Path
|
||||
from PySide6.QtCore import QThread, QTimer, Qt, Signal
|
||||
from PySide6.QtGui import QColor
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QGraphicsDropShadowEffect,
|
||||
QHBoxLayout,
|
||||
@ -230,9 +229,7 @@ class StartTab(QWidget):
|
||||
path.unlink(missing_ok=True)
|
||||
self._on_update_failed(str(exc))
|
||||
return
|
||||
app = QApplication.instance()
|
||||
if app is not None:
|
||||
app.quit()
|
||||
self.window().close()
|
||||
|
||||
def _on_update_failed(self, detail: str) -> None:
|
||||
self._update_btn.setText("Скачать обновление")
|
||||
|
||||
@ -54,10 +54,28 @@ if ($InstallBuildDeps) {
|
||||
# ── Сборка ─────────────────────────────────────────────────────────
|
||||
Write-Host ""
|
||||
Write-Host "=== Сборка EXE ==="
|
||||
& $venvPython -m PyInstaller --noconfirm --clean plate_config_app.spec
|
||||
$pythonHome = (& $venvPython -c "import sys; print(sys.base_prefix)").Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $pythonHome -PathType Container)) {
|
||||
throw "Не удалось определить каталог Python для сборки."
|
||||
}
|
||||
$buildPath = @(
|
||||
(Split-Path -Parent $venvPython),
|
||||
$pythonHome,
|
||||
(Join-Path $pythonHome "DLLs"),
|
||||
(Join-Path $env:SystemRoot "System32"),
|
||||
$env:SystemRoot
|
||||
) -join ";"
|
||||
$originalPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = $buildPath
|
||||
& $venvPython -m PyInstaller --noconfirm --clean plate_config_app.spec
|
||||
$buildExitCode = $LASTEXITCODE
|
||||
} finally {
|
||||
$env:PATH = $originalPath
|
||||
}
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "PyInstaller завершился с ошибкой (код $LASTEXITCODE)."
|
||||
if ($buildExitCode -ne 0) {
|
||||
throw "PyInstaller завершился с ошибкой (код $buildExitCode)."
|
||||
}
|
||||
|
||||
$builtExe = Join-Path $projectRoot "dist\plate_config_app.exe"
|
||||
|
||||
@ -4,7 +4,7 @@ import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
@ -98,20 +98,18 @@ class StartTabUpdateTests(unittest.TestCase):
|
||||
warning.assert_called_once()
|
||||
tab.shutdown()
|
||||
|
||||
def test_successful_download_launches_update_and_quits(self) -> None:
|
||||
def test_successful_download_launches_update_and_closes_window(self) -> None:
|
||||
tab = StartTab(lambda: None)
|
||||
downloaded = Path("downloaded-update")
|
||||
application = Mock()
|
||||
|
||||
with (
|
||||
patch("app.widgets.start_tab.launch_update") as launcher,
|
||||
patch("app.widgets.start_tab.QApplication") as application_class,
|
||||
patch.object(tab, "close") as close_window,
|
||||
):
|
||||
application_class.instance.return_value = application
|
||||
tab._on_update_downloaded(downloaded)
|
||||
|
||||
launcher.assert_called_once_with(downloaded)
|
||||
application.quit.assert_called_once_with()
|
||||
close_window.assert_called_once_with()
|
||||
self.assertEqual("Перезапуск…", tab._update_btn.text())
|
||||
tab.shutdown()
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ from app.services.update_service import (
|
||||
parse_version,
|
||||
platform_asset_name,
|
||||
platform_executable_name,
|
||||
signal_update_ready,
|
||||
)
|
||||
|
||||
|
||||
@ -199,6 +200,17 @@ class UpdateServiceTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(UpdateError, "Неизвестное"):
|
||||
archive_executable_name("update.zip")
|
||||
|
||||
def test_signals_restart_only_when_ready_file_is_requested(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
marker = Path(directory) / "ready"
|
||||
with patch.dict(os.environ, {update_service.UPDATE_READY_ENV: str(marker)}):
|
||||
signal_update_ready()
|
||||
self.assertEqual(str(os.getpid()), marker.read_text(encoding="ascii"))
|
||||
self.assertNotIn(update_service.UPDATE_READY_ENV, os.environ)
|
||||
marker.unlink()
|
||||
signal_update_ready()
|
||||
self.assertFalse(marker.exists())
|
||||
|
||||
def test_downloads_and_validates_windows_zip(self) -> None:
|
||||
info = self._update_info()
|
||||
data = make_update_archive(
|
||||
@ -412,6 +424,10 @@ class UpdateServiceTests(unittest.TestCase):
|
||||
script = scripts[0].read_text(encoding="utf-8-sig")
|
||||
self.assertIn("$Staging", script)
|
||||
self.assertIn("$backedUp", script)
|
||||
self.assertIn("AddSeconds(30)", script)
|
||||
self.assertIn("$restarted.HasExited", script)
|
||||
self.assertIn("$ReadyFile", script)
|
||||
self.assertIn('$env:PYINSTALLER_RESET_ENVIRONMENT = "1"', script)
|
||||
popen.assert_called_once()
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
||||
@ -445,6 +461,8 @@ class UpdateServiceTests(unittest.TestCase):
|
||||
script = scripts[0].read_text(encoding="utf-8")
|
||||
self.assertIn("staging_path", script)
|
||||
self.assertIn("rollback", script)
|
||||
self.assertIn("PLATE_CONFIG_UPDATE_READY_FILE", script)
|
||||
self.assertIn("PYINSTALLER_RESET_ENVIRONMENT=1", script)
|
||||
popen.assert_called_once()
|
||||
|
||||
def test_extracts_external_archive_next_to_executable(self) -> None:
|
||||
@ -561,7 +579,7 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _wait_for_replacement(
|
||||
def _wait_for_rollback(
|
||||
target: Path,
|
||||
expected_digest: str,
|
||||
replaced_resource: Path,
|
||||
@ -571,17 +589,19 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
||||
if (
|
||||
target.is_file()
|
||||
and ReplacementIntegrationTests._digest(target) == expected_digest
|
||||
and replaced_resource.read_text(encoding="utf-8") == "new"
|
||||
and replaced_resource.is_file()
|
||||
and replaced_resource.read_text(encoding="utf-8") == "old"
|
||||
and not list(target.parent.glob(".plate_config_updater-*"))
|
||||
and not list(target.parent.glob(".plate_config_update-*"))
|
||||
and not list(target.parent.glob(".plate_config_backup-*"))
|
||||
and (target.parent / "update-error.log").is_file()
|
||||
):
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise AssertionError("Внешний helper не заменил пакет за 20 секунд")
|
||||
raise AssertionError("Внешний helper не откатил пакет за 20 секунд")
|
||||
|
||||
@unittest.skipUnless(sys.platform == "win32", "Тест предназначен для Windows")
|
||||
def test_windows_helper_replaces_full_package(self) -> None:
|
||||
def test_windows_helper_rolls_back_when_new_app_exits_early(self) -> None:
|
||||
system_root = Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
@ -597,20 +617,20 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
||||
extra_files={"commands/version.txt": b"new"},
|
||||
)
|
||||
)
|
||||
expected_digest = hashlib.sha256(new_executable).hexdigest()
|
||||
expected_digest = self._digest(target)
|
||||
processes = self._launch_and_capture(archive, target, "Windows")
|
||||
|
||||
self._wait_for_replacement(
|
||||
self._wait_for_rollback(
|
||||
target,
|
||||
expected_digest,
|
||||
root / "commands/version.txt",
|
||||
)
|
||||
processes[0].wait(timeout=5)
|
||||
self._assert_package_replaced(root)
|
||||
self._assert_package_preserved(root)
|
||||
self._wait_for_windows_unlock(target)
|
||||
|
||||
@unittest.skipUnless(sys.platform.startswith("linux"), "Тест предназначен для Linux")
|
||||
def test_linux_helper_replaces_full_package(self) -> None:
|
||||
def test_linux_helper_rolls_back_when_new_app_exits_early(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "plate_config_app-linux-x86_64"
|
||||
@ -625,16 +645,16 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
||||
extra_files={"commands/version.txt": b"new"},
|
||||
)
|
||||
)
|
||||
expected_digest = hashlib.sha256(new_executable).hexdigest()
|
||||
expected_digest = self._digest(target)
|
||||
processes = self._launch_and_capture(archive, target, "Linux")
|
||||
|
||||
self._wait_for_replacement(
|
||||
self._wait_for_rollback(
|
||||
target,
|
||||
expected_digest,
|
||||
root / "commands/version.txt",
|
||||
)
|
||||
processes[0].wait(timeout=5)
|
||||
self._assert_package_replaced(root)
|
||||
self._assert_package_preserved(root)
|
||||
self.assertTrue(target.stat().st_mode & stat.S_IXUSR)
|
||||
|
||||
@staticmethod
|
||||
@ -646,12 +666,12 @@ class ReplacementIntegrationTests(unittest.TestCase):
|
||||
(root / "settings.json").write_text("preserve", encoding="utf-8")
|
||||
|
||||
@staticmethod
|
||||
def _assert_package_replaced(root: Path) -> None:
|
||||
def _assert_package_preserved(root: Path) -> None:
|
||||
self_settings = (root / "settings.json").read_text(encoding="utf-8")
|
||||
if self_settings != "preserve":
|
||||
raise AssertionError("settings.json не сохранён")
|
||||
if (root / "commands/obsolete.txt").exists():
|
||||
raise AssertionError("Старое содержимое каталога не удалено")
|
||||
if not (root / "commands/obsolete.txt").exists():
|
||||
raise AssertionError("Старое содержимое каталога не восстановлено")
|
||||
|
||||
@staticmethod
|
||||
def _launch_and_capture(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user