diff options
-rw-r--r-- | util/cbfstool/tests/README.md | 26 | ||||
-rw-r--r-- | util/cbfstool/tests/conftest.py | 12 | ||||
-rw-r--r-- | util/cbfstool/tests/elogtool_test.py | 150 |
3 files changed, 188 insertions, 0 deletions
diff --git a/util/cbfstool/tests/README.md b/util/cbfstool/tests/README.md new file mode 100644 index 0000000000..8ba69744cd --- /dev/null +++ b/util/cbfstool/tests/README.md @@ -0,0 +1,26 @@ +# CBFSTool tests + +To run the tests do `pytest name_of_the_file.py`. E.g: + +```shell +$ pytest elogtool_test.py +``` + +## Dependencies + +### Pytest + +Requires `pytest`. To install it do: + +```shell +$ pip install --user pytest +``` + +### Binaries + +Make sure that you have compiled the cbfstool binaries before running the test. e.g: + +```shell +$ cd $COREBOOT_SRC/util/cbfstool +$ make +``` diff --git a/util/cbfstool/tests/conftest.py b/util/cbfstool/tests/conftest.py new file mode 100644 index 0000000000..4e779245df --- /dev/null +++ b/util/cbfstool/tests/conftest.py @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: BSD-3-Clause + +import pathlib + + +def pytest_addoption(parser): + here = pathlib.Path(__file__).parent + parser.addoption( + "--elogtool-path", + type=pathlib.Path, + default=(here / ".." / "elogtool").resolve(), + ) diff --git a/util/cbfstool/tests/elogtool_test.py b/util/cbfstool/tests/elogtool_test.py new file mode 100644 index 0000000000..a798a4c16c --- /dev/null +++ b/util/cbfstool/tests/elogtool_test.py @@ -0,0 +1,150 @@ +#!/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 |