#!/usr/bin/python3 # SPDX-License-Identifier: BSD-3-Clause import os import pytest import struct import subprocess from datetime import datetime from datetime import timedelta # Defined in include/commonlib/bsd/elog.h ELOG_TYPE_SYSTEM_BOOT = 0x17 ELOG_TYPE_EOL = 0xff ELOG_EVENT_HEADER_SIZE = 8 ELOG_EVENT_CHECKSUM_SIZE = 1 def convert_to_event(s: str) -> dict: fields = s.split("|") assert len(fields) == 3 or len(fields) == 4 return { "index": int(fields[0]), "timestamp": datetime.strptime(fields[1].strip(), "%Y-%m-%d %H:%M:%S"), "desc": fields[2].strip(), "data": fields[3].strip() if len(fields) == 4 else None, } def compare_event(expected: dict, got: dict) -> None: # Ignore the keys that might be in "got", but not in "expected". # In particular "timestamp" might not want to be tested. for key in expected: assert key in got.keys() assert expected[key] == got[key] @pytest.fixture(scope="session") def elogtool_path(request): exe = request.config.option.elogtool_path assert os.path.exists(exe) return exe @pytest.fixture(scope="function") def elogfile(tmp_path): header_size = 8 tail_size = 512 - header_size # Elog header: # Magic (4 bytes) = "ELOG" # Version (1 byte) = 1 # Size (1 byte) = 8 # Reserved (2 bytes) = 0xffff header = struct.pack("4sBBH", bytes("ELOG", "utf-8"), 1, 8, 0xffff) # Fill the tail with EOL events. tail = bytes([ELOG_TYPE_EOL] * tail_size) buf = header + tail buf_path = tmp_path / "elog_empty.bin" with buf_path.open("wb") as fd: fd.write(buf) fd.flush() return str(buf_path) assert False def elog_list(elogtool_path: str, path: str) -> list: output = subprocess.run([elogtool_path, 'list', '-f', path], capture_output=True, check=True) log = output.stdout.decode("utf-8").strip() lines = log.splitlines() lines = [convert_to_event(s.strip()) for s in lines] return lines def elog_clear(elogtool_path: str, path: str) -> None: subprocess.run([elogtool_path, 'clear', '-f', path], check=True) def elog_add(elogtool_path: str, path: str, typ: int, data: bytearray) -> None: subprocess.run([elogtool_path, 'add', '-f', path, hex(typ), data.hex()], check=True) def test_list_empty(elogtool_path, elogfile): events = elog_list(elogtool_path, elogfile) assert len(events) == 0 def test_clear_empty(elogtool_path, elogfile): elog_clear(elogtool_path, elogfile) events = elog_list(elogtool_path, elogfile) # Must have one event, the "Log area cleared" event. assert len(events) == 1 expected = {"index": 0, "desc": "Log area cleared", # "0", since it was an empty elog buffer. No bytes were cleared. "data": "0"} compare_event(expected, events[0]) def test_clear_not_empty(elogtool_path, elogfile): tot_events = 10 data_size = 4 event_size = ELOG_EVENT_HEADER_SIZE + data_size + ELOG_EVENT_CHECKSUM_SIZE written_bytes = tot_events * event_size for i in range(tot_events): # Adding boot_count for completeness. But it is ignored in this test. boot_count = i.to_bytes(data_size, "little") elog_add(elogtool_path, elogfile, ELOG_TYPE_SYSTEM_BOOT, boot_count) elog_clear(elogtool_path, elogfile) events = elog_list(elogtool_path, elogfile) # Must have one event, the "Log area cleared" event. assert len(events) == 1 expected = {"index": 0, "desc": "Log area cleared", "data": str(written_bytes) } compare_event(expected, events[0]) def test_add_single_event(elogtool_path, elogfile): # "before - one second" is needed because datetime.now() fills the # microsecond variable. But eventlog doesn't use it, and has it hardcoded to # zero. before = datetime.now() - timedelta(seconds=1) boot_count = 128 elog_add(elogtool_path, elogfile, ELOG_TYPE_SYSTEM_BOOT, boot_count.to_bytes(4, "little")) after = datetime.now() events = elog_list(elogtool_path, elogfile) assert len(events) == 1 ev = events[0] expected = {"index": 0, "desc": "System boot", "data": str(boot_count) } compare_event(expected, ev) assert before < ev["timestamp"] < after