jira-agent/tests/test_update.py
2026-10-15 00:36:03 +04:00

285 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import hashlib
import json
import os
import tempfile
import unittest
from _common import SETTINGS, agent
from fake_hub import FakeHub
CUR = agent.AGENT_VERSION
NEXT = CUR + 1
class UpdateStateTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.path = os.path.join(self.dir.name, "update.json")
def tearDown(self):
self.dir.cleanup()
def test_write_and_read(self):
d = agent.write_update_state(self.path, "staged", 5, 6, "abc")
self.assertEqual(agent.read_update_state(self.path), d)
self.assertEqual((d["state"], d["from_version"], d["to_version"], d["sha256"], d["error"]), ("staged", 5, 6, "abc", None))
self.assertTrue(d["at"])
self.assertEqual(os.listdir(self.dir.name), ["update.json"]) # временный файл не остался
def test_read_missing(self):
self.assertIsNone(agent.read_update_state(self.path))
def test_read_update_state_garbage(self):
for text in ('{"state": "tri', "[1, 2]", ""):
with open(self.path, "w", encoding="utf-8") as f:
f.write(text)
self.assertIsNone(agent.read_update_state(self.path))
def test_confirm_trial_of_this_version(self):
u = agent.Updater(None, self.dir.name, 1)
agent.write_update_state(u.state_path, "trial", 4, agent.AGENT_VERSION, "s")
u.confirm()
st = agent.read_update_state(u.state_path)
self.assertEqual((st["state"], st["from_version"], st["to_version"], st["sha256"]), ("ok", 4, agent.AGENT_VERSION, "s"))
def test_confirm_ignores_other_states(self):
u = agent.Updater(None, self.dir.name, 1)
u.confirm()
self.assertIsNone(u.state()) # нет файла — не создаётся
agent.write_update_state(u.state_path, "trial", 4, agent.AGENT_VERSION + 1, "s")
u.confirm()
self.assertEqual(u.state()["state"], "trial") # пробная другая версия — не наша
agent.write_update_state(u.state_path, "rolled_back", 5, 6, "s", "код 1 через 3 с")
u.confirm()
self.assertEqual(u.state()["state"], "rolled_back")
def make_release(version, body="print('ok')\n"):
content = "AGENT_VERSION = %d\n%s" % (version, body)
return {"version": version, "sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(), "content": content}
class UpdaterCheckTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.hub = FakeHub(SETTINGS)
cfg = {"hub_url": self.hub.base_url, "hub_token": self.hub.token, "timeout": 5}
self.u = agent.Updater(agent.HubClient(cfg), self.dir.name, 1)
self.selftests = []
self.code = 0
self.u.run_selftest = lambda path: (self.selftests.append(path), (self.code, "вывод selftest"))[1]
self.now = [1000.0]
self.u.clock = lambda: self.now[0]
def tearDown(self):
self.hub.stop()
self.dir.cleanup()
def ref(self, rel):
return {"version": rel["version"], "sha256": rel["sha256"]}
def capture_logs(self):
logs = []
orig = agent.log
agent.log = logs.append
self.addCleanup(setattr, agent, "log", orig)
return logs
def test_incomplete_release_ignored_with_hourly_hint(self):
logs = self.capture_logs()
for ref in ({"version": NEXT, "sha256": None}, {"version": str(NEXT), "sha256": "a" * 64},
{"version": True, "sha256": "a" * 64}, {"version": NEXT, "sha256": "A" * 64},
{"version": NEXT, "sha256": "a" * 63}, {"version": NEXT, "sha256": "a" * 64 + "\n"}):
self.assertFalse(self.u.check(ref))
self.now[0] += 3600
self.assertFalse(self.u.check({"version": NEXT, "sha256": None}))
self.assertEqual(sum(1 for l in logs if l.startswith("хаб прислал неполную публикацию")), 2)
self.assertEqual(self.hub.release_requests, 0)
self.assertIsNone(self.u.state())
def test_incomplete_release_hint_is_bounded(self):
"""M-4: подсказка не печатает произвольно длинный мусор от хаба целиком."""
logs = self.capture_logs()
self.assertFalse(self.u.check({"version": NEXT, "sha256": "x" * 10000}))
hints = [l for l in logs if l.startswith("хаб прислал неполную публикацию")]
self.assertEqual(len(hints), 1)
self.assertLess(len(hints[0]), 300)
def test_hints_have_separate_timers(self):
logs = self.capture_logs()
self.u.launcher_version = None
self.hub.release = make_release(NEXT)
self.assertFalse(self.u.check({"version": NEXT, "sha256": None}))
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertEqual(sum(1 for l in logs if l.startswith("хаб прислал неполную публикацию")), 1)
self.assertEqual(logs.count("доступна v%d; для самообновления запускайте через run.py" % NEXT), 1)
def test_write_error_is_transient(self):
self.hub.release = make_release(NEXT)
ref = self.ref(self.hub.release)
self.u.new_path = os.path.join(self.dir.name, "нет-каталога", "agent.py.new") # open() → FileNotFoundError
self.assertFalse(self.u.check(ref))
self.assertIsNone(self.u.state())
self.assertEqual(self.selftests, [])
self.now[0] += 299
self.assertFalse(self.u.check(ref))
self.assertEqual(self.hub.release_requests, 1) # раньше 5 минут не повторяет
def test_selftest_os_error_is_transient(self):
self.hub.release = make_release(NEXT)
def boom(path):
raise OSError(28, "No space left on device")
self.u.run_selftest = boom
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertIsNone(self.u.state())
self.assertFalse(os.path.exists(self.u.new_path))
def test_staged_write_error_is_transient(self):
self.hub.release = make_release(NEXT)
self.u.state_path = os.path.join(self.dir.name, "нет-каталога", "update.json")
self.assertFalse(self.u.check(self.ref(self.hub.release))) # без staged выходить с 75 нельзя
self.assertFalse(os.path.exists(self.u.new_path))
def test_reject_write_error_only_logged(self):
rel = make_release(NEXT)
self.hub.release = dict(rel, content=rel["content"] + "# подмена\n")
self.u.state_path = os.path.join(self.dir.name, "нет-каталога", "update.json")
logs = self.capture_logs()
self.assertFalse(self.u.check(self.ref(rel))) # не бросает
self.assertTrue(any(l.startswith("не удалось записать update.json") for l in logs), logs)
def test_good_version_is_staged(self):
rel = make_release(NEXT)
self.hub.release = rel
self.assertTrue(self.u.check(self.ref(rel)))
with open(self.u.new_path, encoding="utf-8") as f:
self.assertEqual(f.read(), rel["content"])
st = self.u.state()
self.assertEqual((st["state"], st["from_version"], st["to_version"], st["sha256"]), ("staged", CUR, NEXT, rel["sha256"]))
self.assertEqual(self.selftests, [self.u.new_path])
def test_not_newer_or_disabled_or_no_release(self):
self.hub.release = make_release(CUR)
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertFalse(self.u.check(None))
self.hub.release = make_release(NEXT)
self.u.enabled = False
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertEqual(self.hub.release_requests, 0)
def test_sha_mismatch_rejected_once(self):
rel = make_release(NEXT)
self.hub.release = dict(rel, content=rel["content"] + "# подмена\n")
ref = self.ref(rel)
self.hub.release["sha256"] = rel["sha256"]
self.assertFalse(self.u.check(ref))
st = self.u.state()
self.assertEqual((st["state"], st["to_version"], st["error"]), ("rejected", NEXT, "sha256 не сходится"))
self.assertFalse(os.path.exists(self.u.new_path))
self.assertFalse(self.u.check(ref))
self.assertEqual(self.hub.release_requests, 1) # отвергнутую версию больше не качает
def test_syntax_error_and_wrong_version_rejected(self):
bad = make_release(NEXT, body="def (:\n")
self.hub.release = bad
self.assertFalse(self.u.check(self.ref(bad)))
self.assertIn("не компилируется", self.u.state()["error"])
wrong = make_release(NEXT + 1)
wrong = dict(wrong, version=NEXT + 2)
self.hub.release = wrong
self.assertFalse(self.u.check(self.ref(wrong)))
self.assertIn("AGENT_VERSION", self.u.state()["error"])
self.assertEqual(self.selftests, [])
def test_selftest_bad_code_rejected(self):
self.hub.release = make_release(NEXT)
self.code = 1
self.assertFalse(self.u.check(self.ref(self.hub.release)))
st = self.u.state()
self.assertEqual(st["state"], "rejected")
self.assertTrue(st["error"].startswith("selftest: код 1"))
self.assertIn("вывод selftest", st["error"])
self.assertFalse(os.path.exists(self.u.new_path))
def test_selftest_unavailable_is_transient(self):
self.hub.release = make_release(NEXT)
self.code = 3
ref = self.ref(self.hub.release)
self.assertFalse(self.u.check(ref))
self.assertIsNone(self.u.state()) # update.json не тронут
self.assertFalse(os.path.exists(self.u.new_path))
self.now[0] += 299
self.assertFalse(self.u.check(ref))
self.assertEqual(len(self.selftests), 1) # раньше 5 минут не повторяет
self.now[0] += 2
self.code = 0
self.assertTrue(self.u.check(ref))
def test_selftest_timeout_is_transient(self):
self.hub.release = make_release(NEXT)
self.code = None
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertIsNone(self.u.state())
def test_download_failure_is_transient(self):
rel = make_release(NEXT) # в claim есть, а /agent/release — 404
self.assertFalse(self.u.check(self.ref(rel)))
self.assertIsNone(self.u.state())
def test_release_changed_between_requests_is_transient(self):
old, new = make_release(NEXT), make_release(NEXT + 1)
self.hub.release = new
self.assertFalse(self.u.check(self.ref(old)))
self.assertIsNone(self.u.state())
self.assertEqual(self.selftests, [])
def test_rolled_back_same_sha_not_retried(self):
rel = make_release(NEXT)
self.hub.release = rel
agent.write_update_state(self.u.state_path, "rolled_back", CUR, NEXT, rel["sha256"], "код 1 через 3 с")
self.assertFalse(self.u.check(self.ref(rel)))
fixed = make_release(NEXT, body="print('fixed')\n")
self.hub.release = fixed
self.assertTrue(self.u.check(self.ref(fixed))) # тот же номер, другой sha — пробуем
def test_without_launcher_only_hint(self):
self.u.launcher_version = None
self.hub.release = make_release(NEXT)
logs = []
orig = agent.log
agent.log = logs.append
try:
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.now[0] += 3600
self.assertFalse(self.u.check(self.ref(self.hub.release)))
finally:
agent.log = orig
self.assertEqual(logs.count("доступна v%d; для самообновления запускайте через run.py" % NEXT), 2)
self.assertEqual(self.hub.release_requests, 0)
def test_stop_during_selftest_is_not_reject(self):
self.hub.release = make_release(NEXT)
stop = [False]
self.u.should_stop = lambda: stop[0]
def selftest(path):
stop[0] = True # Ctrl-C пришёл и selftest упал с 1
return 1, "KeyboardInterrupt"
self.u.run_selftest = selftest
self.assertFalse(self.u.check(self.ref(self.hub.release)))
self.assertIsNone(self.u.state())
self.assertFalse(os.path.exists(self.u.new_path))
def test_real_selftest_runs_new_file(self):
u = agent.Updater(None, self.dir.name, 1, config_path=os.path.join(self.dir.name, "agent.json"))
path = os.path.join(self.dir.name, "agent.py.new")
with open(path, "w", encoding="utf-8") as f:
f.write("import sys\nprint('args', sys.argv[1:])\nsys.exit(3)\n")
code, out = u._run_selftest(path)
self.assertEqual(code, 3)
self.assertIn("'--config', '%s', 'selftest'" % os.path.join(self.dir.name, "agent.json"), out)
if __name__ == "__main__":
unittest.main()