mirror of
https://github.com/ClassicWasTaken/HaloCE-SteamFrame.git
synced 2026-10-23 14:00:33 +02:00
459 lines
22 KiB
Python
459 lines
22 KiB
Python
"""Storage selection must bind every remote step to one verified destination."""
|
|
import json
|
|
import threading
|
|
import contextlib
|
|
import io
|
|
import types
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from halo_frame_installer.install import (
|
|
EXPECTED_MAPS, Installer, validate_storage_destination,
|
|
)
|
|
from halo_frame_installer.ssh import CancelledError, Settings, SSHError
|
|
|
|
RESOURCES = Path(__file__).resolve().parents[1] / "resources"
|
|
SD_ID = "sd:" + "a" * 32
|
|
MOUNT = "/run/media/steamos/Halo SD"
|
|
|
|
|
|
def destination(identifier=SD_ID):
|
|
internal = identifier == "internal"
|
|
root = "/home/steamos" if internal else MOUNT
|
|
return {"id": identifier, "kind": "internal" if internal else "sd",
|
|
"label": "Internal storage" if internal else "SD card — Halo SD",
|
|
"mountPath": root, "gamePath": root + "/Games/HaloCENativeVR",
|
|
"cachePath": root + ("/.cache/halo-frame-installer" if internal else "/.halo-frame-installer"),
|
|
"freeBytes": 32 * 1024 ** 3, "installed": False,
|
|
"identity": {"kind": "internal", "root": root} if internal else {
|
|
"kind": "sd", "mountPath": root, "mountId": 42, "device": "179:1",
|
|
"filesystem": "ext4", "rootDevice": 45825, "rootInode": 2,
|
|
"cardIdentity": "b" * 64, "filesystemUuid": "filesystem-123",
|
|
"blockDevice": "/dev/mmcblk0p1"}}
|
|
|
|
|
|
class StorageConnection:
|
|
def __init__(self, settings, *, existing=False):
|
|
self.settings, self.existing = settings, existing
|
|
self.host_fingerprint = "SHA256:public"
|
|
self.commands, self.uploads = [], []
|
|
self.command_inputs = []
|
|
self.closed = False
|
|
self.failure = None
|
|
self.bad_history = False
|
|
self.live_changes = {}
|
|
|
|
def connect(self):
|
|
pass
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
def put(self, local, remote, **kwargs):
|
|
self.uploads.append((Path(local).name, remote))
|
|
|
|
def run(self, argv, **kwargs):
|
|
self.commands.append(argv)
|
|
self.command_inputs.append(kwargs.get("input_data"))
|
|
selected = destination(self.settings.storage_id)
|
|
if argv[1] == "-c" and len(argv) == 3:
|
|
response = {"home": "/home/steamos", "destinations": [destination("internal"), destination()]}
|
|
elif argv[1] == "-c":
|
|
if argv[3] == "--storage-id":
|
|
selected.update(self.live_changes)
|
|
response = selected
|
|
else:
|
|
response = {"home": "/home/steamos", "storage": selected,
|
|
"cachePath": selected["cachePath"], "gamePath": selected["gamePath"],
|
|
"existing": {"gamePath": selected["gamePath"], "reused": True,
|
|
"mapsVerified": True} if self.existing else None,
|
|
"uninstallSupported": True}
|
|
elif argv[2] == "prepare":
|
|
run = argv[argv.index("--run-id") + 1]
|
|
response = {"uploadPath": selected["cachePath"] + "/runs/" + run + "/upload"}
|
|
elif argv[2] == "build":
|
|
if self.failure:
|
|
raise self.failure
|
|
response = {"built": True}
|
|
elif argv[2] == "finalize":
|
|
response = {"gamePath": selected["gamePath"], "repaired": self.existing}
|
|
elif argv[2] == "shortcut":
|
|
response = {"status": "added", "appid": 3000000042}
|
|
elif argv[2] == "cancel":
|
|
response = {"cancelled": True}
|
|
elif argv[2] == "uninstall":
|
|
run = argv[argv.index("--run-id") + 1]
|
|
parent = selected["gamePath"].rsplit("/", 1)[0]
|
|
response = {"gamePath": selected["gamePath"], "uninstalled": True,
|
|
"steam": {"status": "removed"},
|
|
"savedBackupPath": parent + "/HaloCENativeVR-saves-" + run,
|
|
"recoveredSaveBackupPaths": [
|
|
("/home/steamos/Games" if self.bad_history else parent)
|
|
+ "/HaloCENativeVR-saves-" + "b" * 32]}
|
|
else:
|
|
raise AssertionError(argv)
|
|
return "HFI_RESULT " + json.dumps(response)
|
|
|
|
|
|
def maps_fixture(tmp_path):
|
|
maps = tmp_path / "maps"
|
|
maps.mkdir()
|
|
for name in EXPECTED_MAPS:
|
|
(maps / name).write_bytes(b"synthetic Xbox map")
|
|
return maps
|
|
|
|
|
|
@pytest.mark.parametrize("extra", [
|
|
{"id": "sd:../../other"}, {"kind": "internal"},
|
|
{"mountPath": "/home/steamos"}, {"mountPath": "/run/media/steamos"},
|
|
{"mountPath": "/run/media/steamos/../other"}, {"mountPath": '/run/media/steamos/evil"'},
|
|
{"mountPath": "/run/media/steamos/evil$(command)"},
|
|
{"gamePath": MOUNT + "/Games/other"}, {"cachePath": "/home/steamos/.cache/halo-frame-installer"},
|
|
{"freeBytes": True}, {"installed": "yes"}, {"label": "bad\nlabel"},
|
|
])
|
|
def test_remote_storage_paths_and_details_fail_closed(extra):
|
|
with pytest.raises(SSHError):
|
|
validate_storage_destination(dict(destination(), **extra), SD_ID)
|
|
|
|
|
|
def test_sd_card_path_accepts_spaces_and_unicode_without_shell_syntax():
|
|
item = destination()
|
|
root = "/run/media/steamos/Halo 卡"
|
|
item.update(mountPath=root, gamePath=root + "/Games/HaloCENativeVR", cachePath=root + "/.halo-frame-installer")
|
|
item["identity"]["mountPath"] = root
|
|
assert validate_storage_destination(item, SD_ID)["mountPath"] == root
|
|
|
|
|
|
def test_discovery_closes_connection_before_returning_verified_choices():
|
|
settings = Settings("frame", "private")
|
|
connection = StorageConnection(settings)
|
|
result = Installer(lambda _: connection, RESOURCES).discover_storage(settings)
|
|
assert connection.closed and not connection.uploads
|
|
assert [item["id"] for item in result.destinations] == ["internal", SD_ID]
|
|
assert result.host_fingerprint == "SHA256:public" and result.cleanup_warning is None
|
|
|
|
|
|
def test_discovery_cleanup_warning_is_redacted_and_not_ignored():
|
|
settings = Settings("frame", "private", transport="usb")
|
|
connection = StorageConnection(settings)
|
|
def close():
|
|
raise SSHError("private USB forward remains")
|
|
connection.close = close
|
|
result = Installer(lambda _: connection, RESOURCES).discover_storage(settings)
|
|
assert result.cleanup_warning and "private" not in result.cleanup_warning
|
|
assert "Unplug the USB cable" in result.cleanup_warning
|
|
|
|
|
|
def test_discovery_preserves_bounded_unavailable_card_diagnostics():
|
|
settings = Settings("frame", "private")
|
|
connection = StorageConnection(settings)
|
|
original = connection.run
|
|
detail = {"mountPath": MOUNT, "reason": "Card is mounted read-only."}
|
|
def run(argv, **kwargs):
|
|
response = json.loads(original(argv, **kwargs).split(" ", 1)[1])
|
|
response["unavailable"] = [detail]
|
|
return "HFI_RESULT " + json.dumps(response)
|
|
connection.run = run
|
|
result = Installer(lambda _: connection, RESOURCES).discover_storage(settings)
|
|
assert result.unavailable == (detail,) and connection.closed
|
|
|
|
|
|
@pytest.mark.parametrize("existing", [False, True])
|
|
def test_sd_install_and_repair_bind_all_steps_uploads_and_library_to_card(tmp_path, existing):
|
|
settings = Settings("frame", "private", storage_id=SD_ID, reinstall_existing=existing)
|
|
connection = StorageConnection(settings, existing=existing)
|
|
result = Installer(lambda _: connection, RESOURCES).run(settings, None if existing else maps_fixture(tmp_path))
|
|
assert result.game_path == destination()["gamePath"] and result.storage_id == SD_ID
|
|
assert connection.closed and result.repaired == existing
|
|
assert all(command[-2:] == ["--storage-id", SD_ID] for command in connection.commands)
|
|
assert all(path.startswith(destination()["cachePath"] + "/resources/") or
|
|
path.startswith(destination()["cachePath"] + "/runs/") for _, path in connection.uploads)
|
|
assert sum(path.endswith(".map") for _, path in connection.uploads) == (0 if existing else 24)
|
|
assert any(name == "frame_storage.py" for name, _ in connection.uploads)
|
|
assert any(command[2] == "shortcut" for command in connection.commands if command[1] != "-c")
|
|
|
|
|
|
def test_sd_library_retry_does_not_rebuild_or_upload_maps():
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings, existing=True)
|
|
result = Installer(lambda _: connection, RESOURCES).add_to_steam(settings)
|
|
assert result.storage_id == SD_ID and result.reused and connection.closed
|
|
assert all(command[-2:] == ["--storage-id", SD_ID] for command in connection.commands)
|
|
assert not any(path.endswith(".map") for _, path in connection.uploads)
|
|
assert not any(command[2] in ("build", "prepare") for command in connection.commands if command[1] != "-c")
|
|
|
|
|
|
def test_sd_build_failure_cancels_selected_run_and_does_not_register(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
connection.failure = SSHError("Build failed")
|
|
with pytest.raises(SSHError, match="Build failed"):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
steps = [command[2] for command in connection.commands if command[1] != "-c"]
|
|
assert steps == ["prepare", "build", "cancel"] and connection.closed
|
|
assert connection.commands[-1][-2:] == ["--storage-id", SD_ID]
|
|
|
|
|
|
def test_sd_uninstall_uses_selected_parent_for_backup_and_recovery_history():
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
result = Installer(lambda _: connection, RESOURCES).uninstall(settings)
|
|
assert result.storage_id == SD_ID and result.uninstalled and connection.closed
|
|
assert result.saved_backup_path.startswith(MOUNT + "/Games/HaloCENativeVR-saves-")
|
|
assert result.recovered_save_backup_paths == (MOUNT + "/Games/HaloCENativeVR-saves-" + "b" * 32,)
|
|
assert all("--storage-id" in command for command in connection.commands)
|
|
|
|
|
|
def test_sd_uninstall_rejects_history_from_internal_storage():
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
connection.bad_history = True
|
|
with pytest.raises(SSHError, match="invalid uninstall recovery"):
|
|
Installer(lambda _: connection, RESOURCES).uninstall(settings)
|
|
assert connection.closed
|
|
|
|
|
|
def test_storage_settings_reject_arbitrary_target_paths():
|
|
with pytest.raises(ValueError, match="detected SD"):
|
|
Settings("frame", "private", storage_id="/run/media/steamos/card")
|
|
|
|
|
|
def test_discovery_cancel_before_connect_is_noop():
|
|
settings = Settings("frame", "private")
|
|
connection = StorageConnection(settings)
|
|
cancel = threading.Event()
|
|
cancel.set()
|
|
with pytest.raises(CancelledError):
|
|
Installer(lambda _: connection, RESOURCES).discover_storage(settings, cancel)
|
|
assert connection.closed and not connection.commands
|
|
|
|
|
|
@pytest.mark.parametrize("field,value", [
|
|
("kind", "internal"), ("mountPath", "/run/media/steamos/other"),
|
|
("mountId", True), ("device", "bad"), ("filesystem", "vfat"),
|
|
("rootDevice", 0), ("rootInode", -1), ("cardIdentity", "missing"),
|
|
("filesystemUuid", "bad/uuid"), ("blockDevice", "/dev/sda1"),
|
|
])
|
|
def test_sd_identity_fields_fail_closed(field, value):
|
|
item = destination()
|
|
item["identity"][field] = value
|
|
with pytest.raises(SSHError, match="invalid SD card identity"):
|
|
validate_storage_destination(item, SD_ID)
|
|
|
|
|
|
@pytest.mark.parametrize("field", [
|
|
"kind", "mountPath", "mountId", "device", "filesystem", "rootDevice",
|
|
"rootInode", "cardIdentity", "filesystemUuid",
|
|
])
|
|
def test_sd_identity_missing_fields_fail_closed(field):
|
|
item = destination()
|
|
del item["identity"][field]
|
|
with pytest.raises(SSHError, match="invalid SD card identity"):
|
|
validate_storage_destination(item, SD_ID)
|
|
|
|
|
|
def storage_checks(connection):
|
|
return [command for command in connection.commands
|
|
if command[1] == "-c" and len(command) > 3 and command[3] == "--storage-id"]
|
|
|
|
|
|
def remote_steps(connection):
|
|
return [command[2] for command in connection.commands if command[1] != "-c"]
|
|
|
|
|
|
def test_sd_each_put_has_independent_bundled_before_and_after_checks(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
checks = storage_checks(connection)
|
|
assert len(checks) == len(connection.uploads) * 2
|
|
assert all(command[-2:] == ["--storage-id", SD_ID] for command in checks)
|
|
payloads = [payload.decode("utf-8") for command, payload in zip(connection.commands, connection.command_inputs)
|
|
if command[1] == "-c" and len(command) > 3 and command[3] == "--storage-id"]
|
|
assert len(payloads) == len(checks)
|
|
assert all("types.ModuleType" in source and "resolve_storage(sys.argv[-1])" in source
|
|
and "def _sd_identity(" in source for source in payloads)
|
|
assert all("synthetic Xbox map" not in source and "xbox-data-manifest" not in source
|
|
for source in payloads)
|
|
|
|
|
|
@pytest.mark.parametrize("field,value", [
|
|
("mountId", 43), ("device", "179:2"), ("rootDevice", 45826),
|
|
("rootInode", 3), ("cardIdentity", "c" * 64),
|
|
("filesystemUuid", "new-filesystem"), ("filesystem", "f2fs"),
|
|
("blockDevice", "/dev/mmcblk0p2"),
|
|
])
|
|
def test_card_identity_changed_between_puts_stops_before_next_write(tmp_path, field, value):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
original = connection.put
|
|
def put(local, remote, **kwargs):
|
|
original(local, remote, **kwargs)
|
|
if Path(local).name == "frame_storage.py":
|
|
# The first upload's post-check still sees the original card.
|
|
# Its next call models the card being swapped between files.
|
|
original_run = connection.run
|
|
def run(argv, **options):
|
|
response = original_run(argv, **options)
|
|
if argv[1] == "-c" and argv[3] == "--storage-id":
|
|
identity = dict(destination()["identity"], **{field: value})
|
|
connection.live_changes = {"identity": identity}
|
|
connection.run = original_run
|
|
return response
|
|
connection.run = run
|
|
connection.put = put
|
|
with pytest.raises(SSHError, match="SD card changed during transfer"):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
assert connection.closed and len(connection.uploads) == 1
|
|
assert not remote_steps(connection)
|
|
|
|
|
|
def test_card_swapped_during_large_map_is_checked_before_upload_continues(tmp_path, monkeypatch):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
original = connection.put
|
|
clock = [0.0]
|
|
monkeypatch.setattr("halo_frame_installer.install.time.monotonic", lambda: clock[0])
|
|
callbacks = []
|
|
def put(local, remote, **kwargs):
|
|
original(local, remote, **kwargs)
|
|
if Path(local).suffix == ".map":
|
|
callback = kwargs["callback"]
|
|
clock[0] = 0.5; callback(1, 8); callbacks.append(1)
|
|
connection.live_changes = {"identity": dict(destination()["identity"], mountId=99)}
|
|
clock[0] = 1.1; callback(2, 8)
|
|
callbacks.append(2) # Must never reach the next SFTP write callback.
|
|
connection.put = put
|
|
activity = []
|
|
with pytest.raises(SSHError, match="SD card changed during transfer"):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path),
|
|
lambda *update: activity.append(update))
|
|
assert callbacks == [1] and connection.closed
|
|
assert sum(name.endswith(".map") for name, _ in connection.uploads) == 1
|
|
assert remote_steps(connection) == ["prepare", "cancel"]
|
|
assert any("cancellation confirmed" in message for _, message, _ in activity)
|
|
|
|
|
|
def test_sd_post_upload_guard_stops_swapped_card_before_build(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
original = connection.put
|
|
def put(local, remote, **kwargs):
|
|
original(local, remote, **kwargs)
|
|
if Path(local).suffix == ".map":
|
|
connection.live_changes = {"identity": dict(destination()["identity"], rootInode=99)}
|
|
connection.put = put
|
|
with pytest.raises(SSHError, match="SD card changed during transfer"):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
assert remote_steps(connection) == ["prepare", "cancel"] and connection.closed
|
|
|
|
|
|
def test_sd_guard_ignores_changing_free_space_and_install_metadata(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
connection.live_changes = {"freeBytes": 1, "installed": True, "label": "Halo SD renamed"}
|
|
result = Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
assert result.storage_id == SD_ID and connection.closed and "shortcut" in remote_steps(connection)
|
|
|
|
|
|
def test_sd_uninstall_upload_guard_prevents_removal_on_changed_card():
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
connection.live_changes = {"identity": dict(destination()["identity"], mountId=99)}
|
|
with pytest.raises(SSHError, match="SD card changed during transfer"):
|
|
Installer(lambda _: connection, RESOURCES).uninstall(settings)
|
|
assert not connection.uploads and not remote_steps(connection) and connection.closed
|
|
|
|
|
|
def test_sd_callback_cancel_stops_run_and_closes_connection(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
original = connection.put
|
|
cancel = threading.Event()
|
|
def put(local, remote, **kwargs):
|
|
original(local, remote, **kwargs)
|
|
if Path(local).suffix == ".map":
|
|
cancel.set()
|
|
kwargs["callback"](1, 8)
|
|
connection.put = put
|
|
with pytest.raises(CancelledError):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path), cancel_event=cancel)
|
|
assert remote_steps(connection) == ["prepare", "cancel"] and connection.closed
|
|
|
|
|
|
def test_internal_uploads_keep_original_transfer_behavior_without_sd_checks(tmp_path):
|
|
settings = Settings("frame", "private")
|
|
connection = StorageConnection(settings)
|
|
result = Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
assert result.storage_id == "internal" and not storage_checks(connection) and connection.closed
|
|
|
|
|
|
def test_sd_preflight_requires_complete_identity_before_any_write(tmp_path):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
original = connection.run
|
|
def run(argv, **kwargs):
|
|
value = json.loads(original(argv, **kwargs).split(" ", 1)[1])
|
|
if argv[1] == "-c" and argv[3] == "preflight":
|
|
del value["storage"]["identity"]
|
|
return "HFI_RESULT " + json.dumps(value)
|
|
connection.run = run
|
|
with pytest.raises(SSHError, match="invalid SD card identity"):
|
|
Installer(lambda _: connection, RESOURCES).run(settings, maps_fixture(tmp_path))
|
|
assert not connection.uploads and not remote_steps(connection) and connection.closed
|
|
|
|
|
|
def test_sd_guard_requires_original_fixed_paths_even_with_same_selected_id():
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings, existing=True)
|
|
mount = "/run/media/steamos/Card moved"
|
|
connection.live_changes = {"mountPath": mount, "gamePath": mount + "/Games/HaloCENativeVR",
|
|
"cachePath": mount + "/.halo-frame-installer",
|
|
"identity": dict(destination()["identity"], mountPath=mount)}
|
|
with pytest.raises(SSHError, match="SD card changed during transfer"):
|
|
Installer(lambda _: connection, RESOURCES).add_to_steam(settings)
|
|
assert not connection.uploads and not remote_steps(connection) and connection.closed
|
|
|
|
|
|
def test_sd_periodic_guard_throttles_checks_and_preserves_progress(tmp_path, monkeypatch):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
clock = [0.0]
|
|
monkeypatch.setattr("halo_frame_installer.install.time.monotonic", lambda: clock[0])
|
|
progress = []
|
|
def put(local, remote, **kwargs):
|
|
for done, moment in enumerate((0.2, 0.4, 1.1, 1.3, 2.2), 1):
|
|
clock[0] = moment
|
|
kwargs["callback"](done, 5)
|
|
connection.put = put
|
|
upload = Installer(resource_dir=RESOURCES).guarded_upload(connection, settings, destination(), threading.Event())
|
|
upload(tmp_path / "example.map", MOUNT + "/.halo-frame-installer/example.map",
|
|
callback=lambda done, total: progress.append((done, total)))
|
|
assert progress == [(done, 5) for done in range(1, 6)]
|
|
assert len(storage_checks(connection)) == 4 # before, two elapsed seconds, after
|
|
|
|
|
|
def test_missing_card_inline_check_emits_bounded_error_without_traceback(tmp_path, monkeypatch):
|
|
settings = Settings("frame", "private", storage_id=SD_ID)
|
|
connection = StorageConnection(settings)
|
|
(tmp_path / "frame_storage.py").write_text(
|
|
"def resolve_storage(selected):\n raise ValueError('card missing\\n' + 'x' * 1000)\n")
|
|
messages = []
|
|
def run(argv, **kwargs):
|
|
output = io.StringIO()
|
|
monkeypatch.setattr("sys.argv", ["-c", *argv[3:]])
|
|
monkeypatch.setattr("sys.stdin", types.SimpleNamespace(buffer=io.BytesIO(kwargs["input_data"])))
|
|
with contextlib.redirect_stdout(output), pytest.raises(SystemExit) as stopped:
|
|
exec(compile(argv[2], "<inline SD check>", "exec"), {})
|
|
assert stopped.value.code == 1
|
|
messages.append(output.getvalue())
|
|
raise SSHError(output.getvalue())
|
|
connection.run = run
|
|
upload = Installer(resource_dir=tmp_path).guarded_upload(connection, settings, destination(), threading.Event())
|
|
with pytest.raises(SSHError, match="HFI_ERROR card missing"):
|
|
upload(tmp_path / "file", MOUNT + "/.halo-frame-installer/file")
|
|
assert not connection.uploads and len(messages) == 1
|
|
assert len(messages[0]) <= len("HFI_ERROR ") + 600 + 1
|
|
assert messages[0].count("\n") == 1 and "Traceback" not in messages[0]
|