155 lines
6.6 KiB
Python
155 lines
6.6 KiB
Python
import json
|
|
import os
|
|
from datetime import datetime, timezone
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from _common import agent
|
|
|
|
|
|
class JournalTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self.dir = tempfile.TemporaryDirectory()
|
|
self.path = os.path.join(self.dir.name, "journal.jsonl")
|
|
self.j = agent.Journal(self.path)
|
|
patcher = mock.patch.object(agent, "log")
|
|
self.log = patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def tearDown(self):
|
|
self.dir.cleanup()
|
|
|
|
def test_empty(self):
|
|
self.assertEqual(self.j.records(), [])
|
|
self.assertEqual(self.j.unacked_finished(), [])
|
|
self.assertEqual(self.j.unfinished_started(), [])
|
|
|
|
def test_lifecycle(self):
|
|
self.j.started(1)
|
|
self.assertEqual(self.j.unfinished_started(), [1])
|
|
self.j.finished(1, "done", {"key": "A-1"}, None, 2, 0)
|
|
self.assertEqual(self.j.unfinished_started(), [])
|
|
pending = self.j.unacked_finished()
|
|
self.assertEqual(len(pending), 1)
|
|
self.assertEqual(pending[0]["command_id"], 1)
|
|
self.assertEqual(pending[0]["result"], {"key": "A-1"})
|
|
self.assertEqual(pending[0]["jira_calls"], 2)
|
|
self.j.acked(1)
|
|
self.assertEqual(self.j.unacked_finished(), [])
|
|
self.j.started(2)
|
|
self.j.started(3)
|
|
self.j.finished(3, "failed", None, "HTTP 400", 1, 0)
|
|
self.assertEqual(self.j.unfinished_started(), [2])
|
|
self.assertEqual([r["command_id"] for r in self.j.unacked_finished()], [3])
|
|
|
|
def test_broken_line_is_skipped(self):
|
|
self.j.started(1)
|
|
with open(self.path, "a", encoding="utf-8") as f:
|
|
f.write("{не json\n")
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
self.assertEqual(len(self.j.records()), 2)
|
|
self.log.assert_called_with("журнал: строка 2 не читается, пропускаю")
|
|
|
|
def test_prune_keeps_unacked_and_recent(self):
|
|
self.j.started(1)
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
self.j.append({"kind": "acked", "command_id": 1, "at": "2020-01-01T00:00:00+00:00"})
|
|
self.j.started(2)
|
|
self.j.finished(2, "done", None, None, 1, 0)
|
|
self.j.acked(2)
|
|
self.j.started(3)
|
|
removed = self.j.prune(days=30)
|
|
self.assertEqual(removed, 3)
|
|
ids = [(r["kind"], r["command_id"]) for r in self.j.records()]
|
|
self.assertEqual(ids, [("started", 2), ("finished", 2), ("acked", 2), ("started", 3)])
|
|
self.assertEqual(self.j.prune(days=30), 0)
|
|
|
|
def test_no_secrets_in_records(self):
|
|
self.j.finished(1, "done", {"key": "A-1"}, None, 1, 0)
|
|
with open(self.path, encoding="utf-8") as f:
|
|
text = f.read()
|
|
self.assertNotIn("password", text)
|
|
self.assertEqual(json.loads(text.splitlines()[0])["kind"], "finished")
|
|
|
|
|
|
def test_torn_tail_is_not_glued(self):
|
|
with open(self.path, "wb") as f:
|
|
f.write('{"kind": "star'.encode("utf-8"))
|
|
self.j.started(1)
|
|
recs = self.j.records()
|
|
self.assertEqual([(r["kind"], r["command_id"]) for r in recs], [("started", 1)])
|
|
self.assertEqual(self.j.unfinished_started(), [1])
|
|
|
|
def test_restart_after_acked_reopens(self):
|
|
self.j.started(1)
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
self.j.acked(1)
|
|
self.assertEqual(self.j.unfinished_started(), [])
|
|
self.j.started(1)
|
|
self.assertEqual(self.j.unfinished_started(), [1])
|
|
|
|
def test_duplicate_started_is_deduplicated_in_first_open_order(self):
|
|
self.j.started(2)
|
|
self.j.started(1)
|
|
self.j.started(1)
|
|
self.assertEqual(self.j.unfinished_started(), [2, 1])
|
|
self.j.finished(2, "done", None, None, 1, 0)
|
|
self.assertEqual(self.j.unfinished_started(), [1])
|
|
|
|
def test_prune_keeps_id_with_newer_records(self):
|
|
self.j.started(1)
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
self.j.append({"kind": "acked", "command_id": 1, "at": "2020-01-01T00:00:00+00:00"})
|
|
self.j.started(1)
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
before = self.j.records()
|
|
self.assertEqual(self.j.prune(days=30), 0)
|
|
self.assertEqual(self.j.records(), before)
|
|
|
|
def test_non_dict_and_no_command_id_lines_are_skipped(self):
|
|
with open(self.path, "wb") as f:
|
|
f.write(b'[1, 2]\n5\n{"kind": "started"}\n')
|
|
self.j.started(1)
|
|
self.j.finished(1, "done", None, None, 1, 0)
|
|
self.j.acked(1)
|
|
self.assertEqual(len(self.j.records()), 3)
|
|
self.assertEqual(self.j.unacked_finished(), [])
|
|
self.assertEqual(self.j.unfinished_started(), [])
|
|
self.assertEqual(self.j.prune(days=30), 0)
|
|
self.log.assert_any_call("журнал: удаляю битых строк 3")
|
|
self.assertEqual(len(self.j.records()), 3)
|
|
with open(self.path, encoding="utf-8") as f:
|
|
self.assertNotIn("[1, 2]", f.read())
|
|
|
|
def test_naive_and_invalid_at_in_prune(self):
|
|
self.j.started(1)
|
|
self.j.append({"kind": "acked", "command_id": 1, "at": "2020-01-01T00:00:00"})
|
|
self.j.started(2)
|
|
self.j.append({"kind": "acked", "command_id": 2, "at": "не дата"})
|
|
self.assertEqual(self.j.prune(days=30), 2)
|
|
self.assertEqual([(r["kind"], r["command_id"]) for r in self.j.records()], [("started", 2), ("acked", 2)])
|
|
|
|
def test_journal_file_is_private(self):
|
|
self.j.started(1)
|
|
mode = os.stat(self.path).st_mode & 0o777
|
|
self.assertEqual(mode & 0o077, 0)
|
|
|
|
def test_last_at_is_latest_record_time(self):
|
|
self.assertIsNone(self.j.last_at())
|
|
self.j.append({"kind": "claimed", "command_id": 1, "at": "2026-10-13T10:00:00+00:00"})
|
|
self.j.append({"kind": "finished", "command_id": 1, "at": "2026-10-13T10:05:00+00:00"})
|
|
self.j.append({"kind": "acked", "command_id": 1, "at": "2026-10-13T10:04:00+00:00"})
|
|
self.assertEqual(self.j.last_at(), datetime(2026, 10, 13, 10, 5, tzinfo=timezone.utc))
|
|
|
|
def test_last_at_skips_bad_and_missing_times(self):
|
|
self.j.append({"kind": "claimed", "command_id": 1, "at": "2026-10-13T10:00:00+00:00"})
|
|
self.j.append({"kind": "started", "command_id": 1, "at": "не время"})
|
|
self.j.append({"kind": "started", "command_id": 1, "at": "2026-10-13T11:00:00"}) # без зоны — пропуск
|
|
with open(self.path, "a", encoding="utf-8") as f:
|
|
f.write("битая строка\n")
|
|
self.assertEqual(self.j.last_at(), datetime(2026, 10, 13, 10, 0, tzinfo=timezone.utc))
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|