import json import os 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) if __name__ == "__main__": unittest.main()