mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-19 14:01:42 +02:00
Flet 1.0 keeps toasts in the same stack as dialogs and pop_dialog() closes the topmost open entry. An install link's job shows a "waiting for your answer" toast just as its question appears; when the toast came second, "Download and install" closed the toast, the download ran, and the question stayed on screen. pop_dialog now skips toasts (reproduced and checked with real clicks in the web build). Also a too-long test docstring line.
438 lines
23 KiB
Python
438 lines
23 KiB
Python
"""Self-update (frameport.updates): version logic, release parsing, checks, staging and the swap script."""
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import tarfile
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from frameport import __version__, updates
|
|
from frameport.core import cache, library
|
|
|
|
|
|
def release(tag="v9.9.9", assets=("FramePort-windows-x64.zip", "FramePort-macos-arm64.zip",
|
|
"FramePort-linux-x64.tar.gz", "SHA256SUMS.txt", "frameport-9.9.9-py3-none-any.whl"),
|
|
**kw):
|
|
return {"tag_name": tag, "body": "What's new: things", "html_url": f"https://example.invalid/releases/{tag}",
|
|
"draft": False, "prerelease": False, "published_at": "2026-10-02T00:00:00Z",
|
|
"assets": [{"name": n, "browser_download_url": f"https://example.invalid/dl/{n}"} for n in assets], **kw}
|
|
|
|
|
|
def test_versions():
|
|
assert updates.parse_version("v0.10.1") > updates.parse_version("0.9.9")
|
|
assert updates.parse_version("1.0.0") > updates.parse_version("1.0.0-rc1")
|
|
assert updates.is_newer("0.2.1", "0.2.0") and not updates.is_newer("0.2.0", "0.2.0")
|
|
assert not updates.is_newer("garbage", "0.1.0")
|
|
|
|
|
|
def test_release_parsing_picks_this_platform_and_skips_drafts():
|
|
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
|
assert (up.version == "9.9.9" and up.asset_url.endswith("linux-x64.tar.gz")
|
|
and up.sums_url.endswith("SHA256SUMS.txt"))
|
|
assert up.wheel_url.endswith(".whl") and "What's new" in up.notes
|
|
assert updates.update_from_release(release(draft=True)) is None
|
|
assert updates.update_from_release(release(prerelease=True)) is None
|
|
assert updates.update_from_release(release(assets=("SHA256SUMS.txt",)), "FramePort-linux-x64.tar.gz").asset is None
|
|
|
|
|
|
def test_check_skip_and_cached_hint(monkeypatch):
|
|
calls = []
|
|
monkeypatch.setattr(cache, "http_get", lambda url, **kw: calls.append(url) or type(
|
|
"R", (), {"text": json.dumps(release()), "raise_for_status": lambda self: None})())
|
|
up = updates.check(force=True)
|
|
assert up and up.version == "9.9.9" and calls == [updates.LATEST_API]
|
|
assert library.setting("update.last_check")
|
|
assert updates.cached_update().version == "9.9.9" # read from the cache, no network
|
|
updates.skip("9.9.9")
|
|
assert updates.check() is None and updates.cached_update() is None # "Skip this version" hides it
|
|
assert updates.check(force=True).version == "9.9.9" # "Check now" still finds it
|
|
monkeypatch.setattr(cache, "http_get", lambda url, **kw: (_ for _ in ()).throw(OSError("offline")))
|
|
(cache.cache_dir() / "app-release.json").unlink()
|
|
assert updates.check(force=True) is None # offline: no update, no error
|
|
|
|
|
|
def test_snooze_hides_a_version_until_it_runs_out(monkeypatch):
|
|
monkeypatch.setattr(cache, "cached_json", lambda *a, **k: release())
|
|
cache.cache_dir().mkdir(parents=True, exist_ok=True)
|
|
(cache.cache_dir() / "app-release.json").write_text(json.dumps(release()), encoding="utf-8")
|
|
now = [1_000_000.0]
|
|
monkeypatch.setattr(updates.time, "time", lambda: now[0])
|
|
updates.snooze("9.9.9", hours=24)
|
|
assert updates.check() is None and updates.cached_update() is None # "Later": hidden for a day
|
|
assert updates.check(force=True).version == "9.9.9" # "Check now" still finds it
|
|
now[0] += 23 * 3600
|
|
assert updates.check() is None
|
|
now[0] += 2 * 3600 # a day later it is offered again
|
|
assert updates.check().version == "9.9.9" and updates.cached_update().version == "9.9.9"
|
|
updates.snooze("9.9.8") # a snooze for another version doesn't hide it
|
|
assert updates.check().version == "9.9.9"
|
|
assert not updates.hidden("9.9.9")
|
|
|
|
|
|
def test_older_release_is_no_update(monkeypatch):
|
|
monkeypatch.setattr(cache, "cached_json", lambda *a, **k: release(tag="v0.0.1"))
|
|
assert updates.check(force=True) is None
|
|
|
|
|
|
def test_bundle_layouts():
|
|
assert updates.bundle_root(Path("C:/Games/FramePort/FramePort.exe"), "win32") == Path("C:/Games/FramePort")
|
|
assert updates.bundle_root(Path("/Applications/FramePort.app/Contents/MacOS/FramePort"), "darwin") == \
|
|
Path("/Applications/FramePort.app")
|
|
assert updates.bundle_root(Path("/home/u/FramePort/FramePort"), "linux") == Path("/home/u/FramePort")
|
|
assert updates.bundle_root(Path("/usr/bin/python3"), "linux") is None
|
|
assert updates.bundle_root(Path("/usr/bin/python3.12"), "darwin") is None
|
|
|
|
|
|
def test_install_kind_from_source_checkout():
|
|
# the test suite runs from the repo: a git checkout
|
|
assert updates.install_kind() == "source"
|
|
cmds = updates.upgrade_commands(updates.update_from_release(release(), None), "source")
|
|
assert cmds[0][:2] == ["git", "-C"] and cmds[0][-2:] == ["pull", "--ff-only"] and cmds[1][:2] == ["uv", "sync"]
|
|
|
|
|
|
def test_wheel_commands(monkeypatch):
|
|
up = updates.update_from_release(release(), None)
|
|
monkeypatch.setattr(sys, "prefix", "/home/u/.local/share/uv/tools/frameport")
|
|
assert updates.upgrade_commands(up, "wheel") == [["uv", "tool", "install", "--force", up.wheel_url]]
|
|
monkeypatch.setattr(sys, "prefix", "/home/u/venv")
|
|
assert updates.upgrade_commands(up, "wheel")[0][1:4] == ["-m", "pip", "install"]
|
|
|
|
|
|
def test_sums_parsing():
|
|
sums = updates.parse_sums("a" * 64 + " FramePort-linux-x64.tar.gz\n"
|
|
+ "B" * 64 + " *FramePort-windows-x64.zip\njunk\n")
|
|
assert sums == {"FramePort-linux-x64.tar.gz": "a" * 64, "FramePort-windows-x64.zip": "b" * 64}
|
|
|
|
|
|
def _fake_downloads(monkeypatch, files: dict[str, bytes], sums: dict[str, str] | None = None):
|
|
sums = sums or {name: hashlib.sha256(data).hexdigest() for name, data in files.items()}
|
|
text = "".join(f"{h} {n}\n" for n, h in sums.items())
|
|
monkeypatch.setattr(cache, "http_get", lambda url, **kw: type("R", (), {"text": text})())
|
|
|
|
def download(url, dest, progress=None, expected_sha256=None, expected_sha1=None):
|
|
data = files[url.rsplit("/", 1)[1]]
|
|
if expected_sha256 and hashlib.sha256(data).hexdigest() != expected_sha256:
|
|
raise ValueError("checksum mismatch")
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.write_bytes(data)
|
|
return dest
|
|
monkeypatch.setattr(cache, "download", download)
|
|
|
|
|
|
def _linux_archive(version: str) -> bytes:
|
|
buf = io.BytesIO()
|
|
with tarfile.open(fileobj=buf, mode="w:gz") as t:
|
|
for name, data, mode in (("FramePort/FramePort", b"#!/bin/sh\necho " + version.encode() + b"\n", 0o755),
|
|
("FramePort/data/version.txt", version.encode(), 0o644)):
|
|
info = tarfile.TarInfo(name)
|
|
info.size, info.mode = len(data), mode
|
|
t.addfile(info, io.BytesIO(data))
|
|
return buf.getvalue()
|
|
|
|
|
|
def test_prepare_verifies_and_unpacks(monkeypatch):
|
|
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
|
_fake_downloads(monkeypatch, {"FramePort-linux-x64.tar.gz": _linux_archive("9.9.9")})
|
|
app = updates.prepare(up, platform="linux")
|
|
assert (app / "FramePort").is_file() and (app / "data/version.txt").read_text() == "9.9.9"
|
|
assert os.access(app / "FramePort", os.X_OK) or sys.platform == "win32"
|
|
assert updates.ready_update() == ("9.9.9", app)
|
|
|
|
|
|
def test_prepare_rejects_a_bad_checksum(monkeypatch):
|
|
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
|
_fake_downloads(monkeypatch, {"FramePort-linux-x64.tar.gz": _linux_archive("9.9.9")},
|
|
sums={"FramePort-linux-x64.tar.gz": "0" * 64})
|
|
with pytest.raises(ValueError):
|
|
updates.prepare(up, platform="linux")
|
|
assert updates.ready_update() is None
|
|
|
|
|
|
def test_prepare_rejects_wrong_layout(monkeypatch):
|
|
up = updates.update_from_release(release(), "FramePort-windows-x64.zip")
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as z:
|
|
z.writestr("NotFramePort.exe", b"x")
|
|
_fake_downloads(monkeypatch, {"FramePort-windows-x64.zip": buf.getvalue()})
|
|
with pytest.raises(updates.UpdateError):
|
|
updates.prepare(up, platform="win32", current=Path("/nonexistent"))
|
|
|
|
|
|
def test_release_without_sums_is_refused():
|
|
up = updates.update_from_release(release(assets=("FramePort-linux-x64.tar.gz",)), "FramePort-linux-x64.tar.gz")
|
|
with pytest.raises(updates.UpdateError):
|
|
updates.prepare(up, platform="linux")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX swap script")
|
|
def test_swap_script_replaces_the_installed_folder(monkeypatch, tmp_path):
|
|
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
|
_fake_downloads(monkeypatch, {"FramePort-linux-x64.tar.gz": _linux_archive("9.9.9")})
|
|
app = updates.prepare(up, platform="linux")
|
|
installed = tmp_path / "apps/FramePort"
|
|
installed.mkdir(parents=True)
|
|
(installed / "FramePort").write_text("old")
|
|
(installed / "stale.txt").write_text("old file")
|
|
script = updates.apply(app, installed, relaunch=False, pid=2 ** 22 + 1, platform="linux", wait=True)
|
|
assert script.exists()
|
|
assert (installed / "data/version.txt").read_text() == "9.9.9" and not (installed / "stale.txt").exists()
|
|
assert not (tmp_path / "apps/FramePort.old").exists()
|
|
log = (updates.user_data_dir() / "logs/update.log").read_text()
|
|
assert "update: installed" in log and "not relaunching" in log
|
|
|
|
|
|
def test_windows_script_backs_up_and_relaunches():
|
|
text = updates.swap_script(Path("C:/data/updates/9.9.9/staged"), Path("C:/Games/FramePort"), 1234, "win32", True,
|
|
Path("C:/data/logs/update.log"))
|
|
assert "Get-Process -Id 1234" in text and "Copy-Item -Path (Join-Path $src '*') -Destination $dst" in text
|
|
assert "previous" in text and "Start-Process -FilePath 'C:" in text and "FramePort.exe" in text
|
|
|
|
|
|
def test_macos_script_clears_quarantine_and_opens():
|
|
text = updates.swap_script(Path("/d/staged/FramePort.app"), Path("/Applications/FramePort.app"), 7, "darwin",
|
|
True, Path("/d/update.log"))
|
|
assert "xattr -dr com.apple.quarantine '/Applications/FramePort.app'" in text
|
|
assert "open '/Applications/FramePort.app'" in text
|
|
|
|
|
|
def test_version_is_single_sourced():
|
|
text = (Path(__file__).resolve().parents[1] / "src/frameport/_version.py").read_text()
|
|
assert f'"{__version__}"' in text
|
|
assert 'dynamic = ["version"]' in (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text()
|
|
|
|
|
|
def test_cli_version_and_update_check(monkeypatch):
|
|
from typer.testing import CliRunner
|
|
|
|
from frameport.cli import app
|
|
|
|
runner = CliRunner()
|
|
assert runner.invoke(app, ["--version"]).output.strip() == f"FramePort {__version__}"
|
|
monkeypatch.setattr(cache, "cached_json", lambda *a, **k: release())
|
|
res = runner.invoke(app, ["update", "--check"])
|
|
assert res.exit_code == 10 and "FramePort 9.9.9 is available" in res.output and "What's new" in res.output
|
|
monkeypatch.setattr(cache, "cached_json", lambda *a, **k: release(tag="v0.0.1"))
|
|
res = runner.invoke(app, ["update", "--check"])
|
|
assert res.exit_code == 0 and "is the latest version" in res.output
|
|
|
|
|
|
def test_cli_hint_once_a_day_without_waiting(monkeypatch):
|
|
from typer.testing import CliRunner
|
|
|
|
from frameport.cli import app
|
|
|
|
(cache.cache_dir() / "app-release.json").write_text(json.dumps(release()))
|
|
monkeypatch.setattr(updates, "refresh_cache", lambda: None) # the cache is fresh anyway: no network
|
|
runner = CliRunner()
|
|
first = runner.invoke(app, ["list"])
|
|
assert "FramePort 9.9.9 is available" in first.output
|
|
assert "FramePort 9.9.9 is available" not in runner.invoke(app, ["list"]).output # once a day
|
|
monkeypatch.setenv("FRAMEPORT_NO_UPDATE_CHECK", "1")
|
|
library.set_setting("update.cli_hint", None)
|
|
assert "is available" not in runner.invoke(app, ["list"]).output
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX swap script")
|
|
def test_apply_detached_checks_the_script_started(monkeypatch, tmp_path):
|
|
import time
|
|
|
|
up = updates.update_from_release(release(), "FramePort-linux-x64.tar.gz")
|
|
_fake_downloads(monkeypatch, {"FramePort-linux-x64.tar.gz": _linux_archive("9.9.9")})
|
|
app = updates.prepare(up, platform="linux")
|
|
installed = tmp_path / "apps/FramePort"
|
|
installed.mkdir(parents=True)
|
|
(installed / "FramePort").write_text("old")
|
|
updates.apply(app, installed, relaunch=False, pid=2 ** 22 + 2, platform="linux") # returns once the script runs
|
|
log = updates.user_data_dir() / "logs/update.log"
|
|
for _ in range(100):
|
|
if "not relaunching" in log.read_text():
|
|
break
|
|
time.sleep(0.1)
|
|
assert (installed / "data/version.txt").read_text() == "9.9.9"
|
|
|
|
|
|
def test_apply_reports_a_script_that_never_starts(monkeypatch, tmp_path):
|
|
installed = tmp_path / "FramePort"
|
|
installed.mkdir()
|
|
staged = tmp_path / "staged"
|
|
staged.mkdir()
|
|
monkeypatch.setattr(updates, "START_TIMEOUT", 0.5)
|
|
monkeypatch.setattr(updates.subprocess, "Popen", lambda *a, **k: type("P", (), {"poll": lambda self: None})())
|
|
with pytest.raises(updates.UpdateError):
|
|
updates.apply(staged, installed, relaunch=False, pid=1, platform="linux")
|
|
|
|
|
|
def test_platform_asset_picks_the_arm64_linux_bundle(monkeypatch):
|
|
monkeypatch.setattr(updates.sys, "platform", "linux")
|
|
monkeypatch.setattr(updates.platform, "machine", lambda: "aarch64")
|
|
assert updates.platform_asset() == "FramePort-linux-arm64.tar.gz"
|
|
monkeypatch.setattr(updates.platform, "machine", lambda: "x86_64")
|
|
assert updates.platform_asset() == "FramePort-linux-x64.tar.gz"
|
|
|
|
|
|
def test_dev_build_versions_sort_between_releases():
|
|
import importlib.util
|
|
from pathlib import Path
|
|
|
|
spec = importlib.util.spec_from_file_location("dev_version", Path(__file__).parents[1] / "scripts/dev_version.py")
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
assert mod.dev_version("0.9.0", 57) == "0.9.1.dev57"
|
|
pv = updates.parse_version
|
|
assert pv("0.9.0") < pv("0.9.1.dev57") < pv("0.9.1.dev58") < pv("0.9.1")
|
|
assert updates.is_newer("0.9.1", "0.9.1.dev58") and not updates.is_newer("0.9.0", "0.9.1.dev58")
|
|
|
|
|
|
def test_dev_release_is_only_read_on_request():
|
|
release = {"tag_name": "dev", "prerelease": True, "html_url": "https://example/dev", "body": "try X",
|
|
"assets": [{"name": "frameport-0.9.1.dev57-py3-none-any.whl", "browser_download_url": "w"},
|
|
{"name": "FramePort-windows-x64.zip", "browser_download_url": "z"},
|
|
{"name": "SHA256SUMS.txt", "browser_download_url": "s"}]}
|
|
assert updates.update_from_release(release, "FramePort-windows-x64.zip") is None # automatic checks skip it
|
|
up = updates.update_from_release(release, "FramePort-windows-x64.zip", dev=True)
|
|
assert up.version == "0.9.1.dev57" and up.asset_url == "z" and up.sums_url == "s" and up.notes == "try X"
|
|
assert updates.update_from_release({**release, "assets": []}, dev=True) is None # no wheel: no version
|
|
|
|
|
|
# ------------------------------------------------------------------------------------------------- changelog
|
|
FOOTER = (Path(__file__).parents[1] / "packaging" / "release-footer.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def ci_notes(tag, bullets):
|
|
"""Release notes as CI writes them (.github/workflows/build.yml)."""
|
|
return (f"FramePort {tag}: Windows (x64), macOS (Apple Silicon) and Linux (x64, ARM64).\n\n## What's new\n\n"
|
|
f"{bullets}\n\nAlready have FramePort? It offers this update itself (Library → **Update now**).\n\n"
|
|
+ FOOTER)
|
|
|
|
|
|
def rel(tag, bullets="- a change", date="2026-10-01", **kw):
|
|
return {"tag_name": tag, "name": f"FramePort {tag}", "body": ci_notes(tag, bullets), "draft": False,
|
|
"prerelease": False, "published_at": f"{date}T12:00:00Z",
|
|
"html_url": f"https://example.invalid/releases/{tag}", "assets": [], **kw}
|
|
|
|
|
|
DEV = {"tag_name": "dev", "name": "FramePort dev build 0.9.3.dev7", "prerelease": True, "draft": False,
|
|
"published_at": "2026-10-07T00:00:00Z", "html_url": "https://example.invalid/releases/dev",
|
|
"body": "Dev build **0.9.3.dev7** from commit abc1234 (2026-10-07). For testing fixes before\nthe next release; "
|
|
"you get that release as a normal update.\n\n## Please test\n\nthe changelog\n\n"
|
|
"## Changes since v0.9.2\n\n- one commit\n",
|
|
"assets": [{"name": "frameport-0.9.3.dev7-py3-none-any.whl", "browser_download_url": "w"}]}
|
|
|
|
RELEASES = [DEV, rel("v0.9.2", "- newest", "2026-10-05"), rel("v0.9.1", "- middle", "2026-10-03"),
|
|
rel("v0.9.1-rc1", prerelease=True), rel("v0.9.0", "- old", "2026-10-01"), rel("v0.8.9", draft=True),
|
|
"garbage", {"tag_name": "nightly"}]
|
|
|
|
|
|
def test_whats_new_keeps_only_the_changes():
|
|
body = updates.whats_new(ci_notes("v0.9.2", "- **Bold** change\n- [link](https://x.invalid)"))
|
|
assert body == "- **Bold** change\n- [link](https://x.invalid)"
|
|
# notes without a "What's new" heading: CI's intro, the update hint and the install footer go
|
|
plain = updates.whats_new("FramePort v0.3.1: Windows (x64).\n\nFixed things.\n\nAlready have FramePort? It "
|
|
"offers this update itself.\n\n" + FOOTER)
|
|
assert plain == "Fixed things."
|
|
dev = updates.whats_new(DEV["body"])
|
|
assert "Please test" in dev and "one commit" in dev and "Dev build" not in dev
|
|
assert updates.whats_new("") == "" and updates.whats_new(None) == ""
|
|
|
|
|
|
def test_changelog_entries_skip_drafts_prereleases_and_bad_data():
|
|
entries = updates.changelog_entries(RELEASES)
|
|
assert [e.version for e in entries] == ["0.9.3.dev7", "0.9.2", "0.9.1", "0.9.0"]
|
|
newest = entries[1]
|
|
assert (newest.date, newest.title, newest.body) == ("2026-10-05", "FramePort v0.9.2", "- newest")
|
|
assert entries[0].dev and not newest.dev
|
|
assert updates.changelog_entries(None) == [] and updates.changelog_entries({"message": "rate limited"}) == []
|
|
|
|
|
|
def test_changelog_ranges_stable_and_dev():
|
|
entries = updates.changelog_entries(RELEASES)
|
|
|
|
def versions(es):
|
|
return [e.version for e in es]
|
|
assert versions(updates.between(entries, "0.9.0", "0.9.2")) == ["0.9.2", "0.9.1"] # newest first
|
|
assert versions(updates.between(entries, "0.9.2", "0.9.2")) == []
|
|
assert versions(updates.between(entries, "0.9.1.dev3", "0.9.1")) == ["0.9.1"] # dev → its release
|
|
assert versions(updates.between(entries, "0.9.1", "0.9.3.dev7")) == ["0.9.3.dev7", "0.9.2"] # choosing dev
|
|
assert versions(updates.between(entries, "0.9.1", "0.9.3")) == ["0.9.2"] # stable: no dev entry
|
|
up = updates.update_from_release(rel("v0.9.2", "- newest"), "")
|
|
assert versions(updates.changelog_for(up, "0.9.0", entries)) == ["0.9.2", "0.9.1"]
|
|
updates.skip("0.9.1") # a skipped version's changes still reach whoever updates past it
|
|
assert versions(updates.changelog_for(up, "0.9.0", entries)) == ["0.9.2", "0.9.1"]
|
|
# the offered release isn't in the (stale) list yet: its own notes come first
|
|
new = updates.update_from_release(rel("v0.9.4", "- brand new"), "")
|
|
out = updates.changelog_for(new, "0.9.1", entries)
|
|
assert versions(out) == ["0.9.4", "0.9.2"] and out[0].body == "- brand new"
|
|
assert versions(updates.recent_history(entries, "0.9.2", n=2)) == ["0.9.2", "0.9.1"]
|
|
assert versions(updates.recent_history(entries, "0.9.3.dev7")) == ["0.9.3.dev7", "0.9.2", "0.9.1", "0.9.0"]
|
|
|
|
|
|
def test_fetch_changelog_caches_and_survives_offline(monkeypatch):
|
|
calls = []
|
|
|
|
def online(url, **kw):
|
|
calls.append(url)
|
|
return type("R", (), {"text": json.dumps(RELEASES), "raise_for_status": lambda self: None})()
|
|
monkeypatch.setattr(cache, "http_get", online)
|
|
assert [e.version for e in updates.fetch_changelog()][:2] == ["0.9.3.dev7", "0.9.2"]
|
|
assert calls == [updates.RELEASES_API]
|
|
updates.fetch_changelog(need="0.9.1") # cached and fresh: no second request
|
|
assert len(calls) == 1
|
|
updates.fetch_changelog(need="0.9.5") # newer than everything cached: asks GitHub again
|
|
assert len(calls) == 2
|
|
monkeypatch.setattr(cache, "http_get", lambda url, **kw: (_ for _ in ()).throw(OSError("offline")))
|
|
assert updates.fetch_changelog(force=True)[1].version == "0.9.2" # offline: the cached copy
|
|
(cache.cache_dir() / "app-releases.json").unlink()
|
|
assert updates.fetch_changelog(force=True) == [] # offline, nothing cached: empty, no error
|
|
|
|
|
|
def test_news_range():
|
|
assert updates.news_range(None, "0.9.2") == (False, None) # first start ever
|
|
assert updates.news_range(None, "0.9.2", used_before=True) == (True, None) # from a version without the setting
|
|
assert updates.news_range("0.9.0", "0.9.2") == (True, "0.9.0")
|
|
assert updates.news_range("0.9.2", "0.9.2") == (False, None)
|
|
assert updates.news_range("0.9.3", "0.9.2") == (False, None) # went back
|
|
assert updates.news_range("0.9.1.dev4", "0.9.1") == (True, "0.9.1.dev4")
|
|
|
|
|
|
def test_pending_news_shows_once_and_waits_while_offline():
|
|
entries = updates.changelog_entries(RELEASES)
|
|
assert updates.pending_news("0.9.2", entries) == [] # first start: just recorded
|
|
assert library.setting("update.last_seen_version") == "0.9.2"
|
|
library.set_setting("update.last_seen_version", "0.9.0")
|
|
assert updates.pending_news("0.9.2", []) == [] # offline: try again next start
|
|
assert library.setting("update.last_seen_version") == "0.9.0"
|
|
assert [e.version for e in updates.pending_news("0.9.2", entries)] == ["0.9.2", "0.9.1"]
|
|
assert library.setting("update.last_seen_version") == "0.9.2"
|
|
assert updates.pending_news("0.9.2", entries) == [] # only once
|
|
library.set_setting("update.last_seen_version", None)
|
|
library.set_setting("update.last_check", 1.0) # updated from an older FramePort
|
|
assert [e.version for e in updates.pending_news("0.9.2", entries)] == ["0.9.2"]
|
|
|
|
|
|
def test_relaunch_environment_drops_the_running_apps_own_variables():
|
|
"""GitHub #195: the relaunched app inherited the old app's Flet/embedded-Python variables and stayed black."""
|
|
from frameport import updates
|
|
|
|
env = updates.clean_env({"PATH": "/bin", "FLET_SERVER_UDS_PATH": "/tmp/old.sock", "flet_platform": "windows",
|
|
"SERIOUS_PYTHON_SITE_PACKAGES": "x", "PYTHONHOME": "C:/Temp/serious_python_temp",
|
|
"PYTHONPATH": "y", "USERPROFILE": "C:/Users/u", "PSModulePath": "z"})
|
|
assert env == {"PATH": "/bin", "USERPROFILE": "C:/Users/u", "PSModulePath": "z"}
|
|
|
|
|
|
def test_source_restart_keeps_a_hidden_console_on_windows(monkeypatch):
|
|
"""DETACHED_PROCESS gave every console child (ssh) of a restarted dev install its own visible terminal."""
|
|
from frameport.ui import updater
|
|
|
|
calls = []
|
|
monkeypatch.setattr(updater.sys, "platform", "win32")
|
|
monkeypatch.setattr(updater.subprocess, "Popen", lambda cmd, **k: calls.append(k))
|
|
u = updater.Updater.__new__(updater.Updater)
|
|
monkeypatch.setattr(u, "_quit", lambda: None, raising=False)
|
|
u._restart_process()
|
|
flags = calls[0]["creationflags"]
|
|
assert flags & 0x08000000 and not flags & 0x00000008 # CREATE_NO_WINDOW, not DETACHED_PROCESS
|