xref: /aosp_15_r20/external/coreboot/util/cbfstool/tests/elogtool_test.py (revision b9411a12aaaa7e1e6a6fb7c5e057f44ee179a49c)
1#!/usr/bin/python3
2# SPDX-License-Identifier: BSD-3-Clause
3
4import os
5import pytest
6import struct
7import subprocess
8from datetime import datetime
9from datetime import timedelta
10
11# Defined in include/commonlib/bsd/elog.h
12ELOG_TYPE_SYSTEM_BOOT = 0x17
13ELOG_TYPE_EOL = 0xff
14ELOG_EVENT_HEADER_SIZE = 8
15ELOG_EVENT_CHECKSUM_SIZE = 1
16
17
18def convert_to_event(s: str) -> dict:
19    fields = s.split("|")
20    assert len(fields) == 3 or len(fields) == 4
21
22    return {
23        "index": int(fields[0]),
24        "timestamp": datetime.strptime(fields[1].strip(), "%Y-%m-%d %H:%M:%S"),
25        "desc": fields[2].strip(),
26        "data": fields[3].strip() if len(fields) == 4 else None,
27    }
28
29
30def compare_event(expected: dict, got: dict) -> None:
31    # Ignore the keys that might be in "got", but not in "expected".
32    # In particular "timestamp" might not want to be tested.
33    for key in expected:
34        assert key in got.keys()
35        assert expected[key] == got[key]
36
37
38@pytest.fixture(scope="session")
39def elogtool_path(request):
40    exe = request.config.option.elogtool_path
41    assert os.path.exists(exe)
42    return exe
43
44
45@pytest.fixture(scope="function")
46def elogfile(tmp_path):
47    header_size = 8
48    tail_size = 512 - header_size
49
50    # Elog header:
51    #  Magic (4 bytes) = "ELOG"
52    #  Version (1 byte) = 1
53    #  Size (1 byte) = 8
54    #  Reserved (2 bytes) = 0xffff
55    header = struct.pack("4sBBH", bytes("ELOG", "utf-8"), 1, 8, 0xffff)
56
57    # Fill the tail with EOL events.
58    tail = bytes([ELOG_TYPE_EOL] * tail_size)
59    buf = header + tail
60
61    buf_path = tmp_path / "elog_empty.bin"
62    with buf_path.open("wb") as fd:
63        fd.write(buf)
64        fd.flush()
65        return str(buf_path)
66    assert False
67
68
69def elog_list(elogtool_path: str, path: str) -> list:
70    output = subprocess.run([elogtool_path, 'list', '-f', path],
71                            capture_output=True, check=True)
72    log = output.stdout.decode("utf-8").strip()
73
74    lines = log.splitlines()
75    lines = [convert_to_event(s.strip()) for s in lines]
76    return lines
77
78
79def elog_clear(elogtool_path: str, path: str) -> None:
80    subprocess.run([elogtool_path, 'clear', '-f', path], check=True)
81
82
83def elog_add(elogtool_path: str, path: str, typ: int, data: bytearray) -> None:
84    subprocess.run([elogtool_path, 'add', '-f', path,
85                   hex(typ), data.hex()], check=True)
86
87
88def test_list_empty(elogtool_path, elogfile):
89    events = elog_list(elogtool_path, elogfile)
90    assert len(events) == 0
91
92
93def test_clear_empty(elogtool_path, elogfile):
94    elog_clear(elogtool_path, elogfile)
95    events = elog_list(elogtool_path, elogfile)
96
97    # Must have one event, the "Log area cleared" event.
98    assert len(events) == 1
99
100    expected = {"index": 0,
101                "desc": "Log area cleared",
102                # "0", since it was an empty elog buffer. No bytes were cleared.
103                "data": "0"}
104    compare_event(expected, events[0])
105
106
107def test_clear_not_empty(elogtool_path, elogfile):
108    tot_events = 10
109    data_size = 4
110    event_size = ELOG_EVENT_HEADER_SIZE + data_size + ELOG_EVENT_CHECKSUM_SIZE
111    written_bytes = tot_events * event_size
112
113    for i in range(tot_events):
114        # Adding boot_count for completeness. But it is ignored in this test.
115        boot_count = i.to_bytes(data_size, "little")
116        elog_add(elogtool_path, elogfile, ELOG_TYPE_SYSTEM_BOOT, boot_count)
117    elog_clear(elogtool_path, elogfile)
118    events = elog_list(elogtool_path, elogfile)
119
120    # Must have one event, the "Log area cleared" event.
121    assert len(events) == 1
122
123    expected = {"index": 0,
124                "desc": "Log area cleared",
125                "data": str(written_bytes)
126                }
127    compare_event(expected, events[0])
128
129
130def test_add_single_event(elogtool_path, elogfile):
131    # "before - one second" is needed because datetime.now() fills the
132    # microsecond variable. But eventlog doesn't use it, and has it hardcoded to
133    # zero.
134    before = datetime.now() - timedelta(seconds=1)
135    boot_count = 128
136    elog_add(elogtool_path, elogfile, ELOG_TYPE_SYSTEM_BOOT,
137             boot_count.to_bytes(4, "little"))
138    after = datetime.now()
139
140    events = elog_list(elogtool_path, elogfile)
141    assert len(events) == 1
142
143    ev = events[0]
144    expected = {"index": 0,
145                "desc": "System boot",
146                "data": str(boot_count)
147                }
148    compare_event(expected, ev)
149
150    assert before < ev["timestamp"] < after
151