mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 19:06:11 +00:00
A check on the releases page once a day: at start, on a timer while Dikte runs, from the General tab on demand, and from `dikte update` at a terminal. What it finds goes in the tray menu and in one notification per version, and opens the release page. Nothing is downloaded and nothing is installed. The four downloads are installed four different ways and three of those belong to the platform: a Mac bundle cannot rewrite itself while it is running, the Windows setup has an uninstall entry of its own, an AppImage is a file kept wherever its owner keeps it, and a checkout is updated with git. Being wrong about any one of them means an installation somebody has to repair by hand. Versions are compared by their numbers alone. A build off master carries the released number with its commit after it, and that build is ahead of the release it names rather than behind it; read as a version suffix, every nightly would be told to go back to a release it had already passed. The clock lives in its own file rather than in the settings, since a check runs while the settings window may be open and a background write into config.json is what would overwrite whatever it holds.
176 lines
6.9 KiB
Python
176 lines
6.9 KiB
Python
"""Whether a newer release is one worth telling somebody about.
|
|
|
|
Two things carry the weight here. A version is compared by its numbers alone,
|
|
because a build off master carries the released number with its commit after
|
|
it and is ahead of that release rather than behind it. And the clock lives in a
|
|
file, so a day of asking nobody has to survive a restart.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
|
|
from dikte import hub
|
|
from dikte import update
|
|
from tests.support import DikteTest, fake_urlopen, url_error
|
|
|
|
RELEASE = {
|
|
"tag_name": "v1.4.0",
|
|
"html_url": "https://github.com/yusufipk/dikte/releases/tag/v1.4.0",
|
|
"published_at": "2026-08-01T10:00:00Z",
|
|
}
|
|
|
|
|
|
class Numbers(DikteTest):
|
|
def test_a_tag_and_a_bare_number_read_the_same(self):
|
|
self.assertEqual(update._numbers("v1.4.0"), (1, 4, 0))
|
|
self.assertEqual(update._numbers("1.4.0"), (1, 4, 0))
|
|
|
|
def test_a_short_number_is_filled_out(self):
|
|
self.assertEqual(update._numbers("2"), (2, 0, 0))
|
|
self.assertEqual(update._numbers("2.1"), (2, 1, 0))
|
|
|
|
def test_what_follows_the_number_is_dropped(self):
|
|
self.assertEqual(update._numbers("1.0.1-dev.abc1234"), (1, 0, 1))
|
|
self.assertEqual(update._numbers("1.0.1+build7"), (1, 0, 1))
|
|
|
|
def test_something_that_is_not_a_version_is_no_version(self):
|
|
self.assertEqual(update._numbers("latest"), ())
|
|
self.assertEqual(update._numbers(""), ())
|
|
self.assertEqual(update._numbers(None), ())
|
|
|
|
|
|
class Newer(DikteTest):
|
|
def test_a_higher_number_is_newer(self):
|
|
self.assertTrue(update.newer("1.4.0", "1.3.9"))
|
|
self.assertTrue(update.newer("v2.0.0", "1.9.9"))
|
|
|
|
def test_the_same_number_is_not(self):
|
|
self.assertFalse(update.newer("1.4.0", "1.4.0"))
|
|
self.assertFalse(update.newer("1.3.0", "1.4.0"))
|
|
|
|
def test_a_build_off_master_is_ahead_of_the_release_it_names(self):
|
|
"""1.0.1-dev.abc1234 was built after 1.0.1 went out, not before it.
|
|
Read as a version suffix it would be older, and every nightly would be
|
|
told to go back to the release it had already passed."""
|
|
self.assertFalse(update.newer("1.0.1", "1.0.1-dev.abc1234"))
|
|
self.assertTrue(update.newer("1.0.2", "1.0.1-dev.abc1234"))
|
|
|
|
def test_a_tag_that_is_not_a_version_is_never_newer(self):
|
|
self.assertFalse(update.newer("nightly", "1.0.0"))
|
|
|
|
|
|
class Asking(DikteTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.patch_attr(hub, "CACHE_DIR", self.path("cache"))
|
|
self.patch_attr(update, "__version__", "1.0.0")
|
|
|
|
def test_the_newest_release_comes_back_with_its_page(self):
|
|
with fake_urlopen(RELEASE) as calls:
|
|
release = update.latest()
|
|
self.assertEqual(release.version, "1.4.0")
|
|
self.assertEqual(release.url, RELEASE["html_url"])
|
|
self.assertEqual(
|
|
calls[0].full_url,
|
|
"https://api.github.com/repos/yusufipk/dikte/releases/latest")
|
|
|
|
def test_a_release_with_no_page_falls_back_to_the_redirect(self):
|
|
with fake_urlopen({"tag_name": "v1.4.0"}):
|
|
release = update.latest()
|
|
self.assertEqual(release.url, update.RELEASES_PAGE)
|
|
|
|
def test_a_repository_with_no_release_is_an_error(self):
|
|
with fake_urlopen({"message": "Not Found"}):
|
|
with self.assertRaises(hub.HubError):
|
|
update.latest()
|
|
|
|
def test_a_check_answers_with_the_newer_release(self):
|
|
with fake_urlopen(RELEASE):
|
|
release = update.check()
|
|
self.assertEqual(release.version, "1.4.0")
|
|
|
|
def test_a_check_that_finds_nothing_new_answers_with_nothing(self):
|
|
self.patch_attr(update, "__version__", "1.4.0")
|
|
with fake_urlopen(RELEASE):
|
|
self.assertIsNone(update.check())
|
|
|
|
def test_a_second_check_the_same_day_asks_nobody(self):
|
|
with fake_urlopen(RELEASE) as calls:
|
|
update.check()
|
|
release = update.check()
|
|
self.assertEqual(len(calls), 1)
|
|
# And still says what the first one found, since it is still true.
|
|
self.assertEqual(release.version, "1.4.0")
|
|
|
|
def test_a_day_later_it_asks_again(self):
|
|
with fake_urlopen(RELEASE) as calls:
|
|
update.check()
|
|
update._store(checked=time.time() - update.INTERVAL - 60)
|
|
update.check()
|
|
self.assertEqual(len(calls), 2)
|
|
|
|
def test_the_button_asks_whatever_the_clock_says(self):
|
|
with fake_urlopen(RELEASE) as calls:
|
|
update.check()
|
|
update.check(force=True)
|
|
self.assertEqual(len(calls), 2)
|
|
|
|
def test_a_check_that_cannot_reach_github_says_so(self):
|
|
with fake_urlopen(url_error("no route to host")):
|
|
with self.assertRaises(hub.HubError):
|
|
update.check()
|
|
|
|
|
|
class Remembering(DikteTest):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.patch_attr(hub, "CACHE_DIR", self.path("cache"))
|
|
self.patch_attr(update, "__version__", "1.0.0")
|
|
|
|
def test_what_the_last_check_found_survives_a_restart(self):
|
|
with fake_urlopen(RELEASE):
|
|
update.check()
|
|
release = update.pending()
|
|
self.assertEqual(release.version, "1.4.0")
|
|
self.assertEqual(release.url, RELEASE["html_url"])
|
|
|
|
def test_nothing_was_ever_checked(self):
|
|
self.assertIsNone(update.pending())
|
|
self.assertEqual(update.state(), {})
|
|
self.assertTrue(update.due())
|
|
|
|
def test_a_release_that_is_no_longer_newer_is_not_pending(self):
|
|
"""The state file outlives the build that wrote it: an update that was
|
|
found and then installed must not still be waiting afterwards."""
|
|
update._store(version="1.4.0")
|
|
self.patch_attr(update, "__version__", "1.4.0")
|
|
self.assertIsNone(update.pending())
|
|
|
|
def test_a_version_is_announced_once(self):
|
|
self.assertEqual(update.announced(), "")
|
|
update.mark_announced("1.4.0")
|
|
self.assertEqual(update.announced(), "1.4.0")
|
|
|
|
def test_a_state_file_that_is_rubbish_is_no_state_at_all(self):
|
|
update.STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
update.STATE_FILE.write_text("half a {", encoding="utf-8")
|
|
self.assertEqual(update.state(), {})
|
|
self.assertIsNone(update.pending())
|
|
|
|
def test_a_state_file_that_cannot_be_written_is_not_a_failure(self):
|
|
self.patch_attr(update, "STATE_FILE",
|
|
self.path("nope") / "deeper" / "update.json")
|
|
self.path("nope").write_text("a file where a directory would go")
|
|
with fake_urlopen(RELEASE):
|
|
release = update.check()
|
|
self.assertEqual(release.version, "1.4.0")
|
|
|
|
def test_the_clock_is_kept_out_of_the_settings(self):
|
|
"""A background check writes while the settings window may be open, and
|
|
a write into config.json there would undo whatever it holds."""
|
|
with fake_urlopen(RELEASE):
|
|
update.check()
|
|
stored = json.loads(update.STATE_FILE.read_text(encoding="utf-8"))
|
|
self.assertEqual(stored["version"], "1.4.0")
|
|
self.assertGreater(stored["checked"], 0)
|