From ebdb7616b84fdb6a238f8b0ec508f18ea554c04c Mon Sep 17 00:00:00 2001 From: uslstenn Date: Wed, 24 Jun 2026 18:29:55 +0300 Subject: [PATCH] [tests] New unit tests added --- tests/conftest.py | 131 +++++++++++++++++++++++++++++++ tests/test_config.py | 16 ++++ tests/test_main.py | 179 +++++++++++++++++++++++++++++++++++++++++++ tests/test_parser.py | 44 +++++++++++ 4 files changed, 370 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index fdd1bed..3bf65e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -244,3 +244,134 @@ def trace_with_unordered(tmp_path: Path) -> Path: trace_file = tmp_path.joinpath("trace_with_unordered.out") trace_file.write_text(content) return trace_file + + +@pytest.fixture +def trace_with_malformed_fetch(tmp_path: Path) -> Path: + content = """\ +O3PipeView:fetch:1000:0x1000:0:add x1, x2, x3:IntAlu +O3PipeView:decode:1100 +O3PipeView:fetch:2000:0x2000:0:2:lw a0, 0(a1):MemRead +O3PipeView:decode:2100 +O3PipeView:retire:2500:store:0 +""" + trace_file = tmp_path.joinpath("trace_malformed.out") + trace_file.write_text(content) + return trace_file + + +@pytest.fixture +def trace_with_store_completion(tmp_path: Path) -> Path: + content = """\ +O3PipeView:fetch:1000:0x1000:0:1:sw a0, 0(a1):MemWrite +O3PipeView:decode:1100 +O3PipeView:rename:1150 +O3PipeView:dispatch:1200 +O3PipeView:issue:1300 +O3PipeView:complete:1400 +O3PipeView:retire:1500:store:2000 +""" + trace_file = tmp_path.joinpath("trace_store.out") + trace_file.write_text(content) + return trace_file + + +@pytest.fixture +def trace_with_invalid_issue(tmp_path: Path) -> Path: + content = """\ +O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu +O3PipeView:decode:1100 +O3PipeView:rename:1150 +O3PipeView:dispatch:1200 +O3PipeView:issue:0 +O3PipeView:complete:1400 +O3PipeView:retire:1500:store:0 +""" + trace_file = tmp_path.joinpath("trace_invalid_issue.out") + trace_file.write_text(content) + return trace_file + + +@pytest.fixture +def trace_with_empty_disasm(tmp_path: Path) -> Path: + content = """\ +O3PipeView:fetch:1000:0x1000:0:1::No_OpClass +O3PipeView:decode:1100 +O3PipeView:rename:1150 +O3PipeView:dispatch:1200 +O3PipeView:issue:1300 +O3PipeView:complete:1400 +O3PipeView:retire:1500:store:0 +""" + trace_file = tmp_path.joinpath("trace_empty_disasm.out") + trace_file.write_text(content) + return trace_file + + +@pytest.fixture +def trace_empty(tmp_path: Path) -> Path: + trace_file = tmp_path.joinpath("trace_empty.out") + trace_file.write_text("") + return trace_file + + +@pytest.fixture +def squashed_parser() -> PipeViewParser: + parser = PipeViewParser() + instr = Instruction( + seq_num=2, + pc="0x2000", + disasm="lw a0, 0(a1)", + opclass="MemRead", + stages={ + PipelineStage.FETCH: 1000, + PipelineStage.DECODE: 1100, + PipelineStage.RENAME: 0, + PipelineStage.DISPATCH: 0, + PipelineStage.ISSUE: 0, + PipelineStage.COMPLETE: 0, + PipelineStage.RETIRE: 0, + }, + stage_order=[ + PipelineStage.FETCH, + PipelineStage.DECODE, + PipelineStage.RENAME, + PipelineStage.DISPATCH, + PipelineStage.ISSUE, + PipelineStage.COMPLETE, + PipelineStage.RETIRE, + ], + ) + parser.instructions = {2: instr} + return parser + + +@pytest.fixture +def parser_with_unknown_opclass() -> PipeViewParser: + parser = PipeViewParser() + instr = Instruction( + seq_num=3, + pc="0x3000", + disasm="fence", + opclass="SomeUnknownOpClass", + stages={ + PipelineStage.FETCH: 1000, + PipelineStage.DECODE: 1100, + PipelineStage.RENAME: 1150, + PipelineStage.DISPATCH: 1200, + PipelineStage.ISSUE: 1300, + PipelineStage.COMPLETE: 1400, + PipelineStage.RETIRE: 1500, + }, + stage_order=[ + PipelineStage.FETCH, + PipelineStage.DECODE, + PipelineStage.RENAME, + PipelineStage.DISPATCH, + PipelineStage.ISSUE, + PipelineStage.COMPLETE, + PipelineStage.RETIRE, + ], + ) + parser.instructions = {3: instr} + return parser diff --git a/tests/test_config.py b/tests/test_config.py index 89aa40a..fdf414c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -35,3 +35,19 @@ def test_config_load_from_nonexist_dir(tmp_path: Path): nonexist = tmp_path.joinpath("nonexist") config = load_config(nonexist) assert config.get_stage_name(PipelineStage.FETCH) is not None + + +def test_config_getattr_missing_key(config: Config): + with pytest.raises(AttributeError): + _ = config._no_such_key + + +def test_config_path_not_directory(tmp_path: Path, caplog): + config_file = tmp_path.joinpath("colors.json") + config_file.write_text(json.dumps({"default": ["test"]})) + + import logging + caplog.set_level(logging.WARNING) + config = load_config(config_file) + assert config.get_stage_name(PipelineStage.FETCH) is not None + assert "not a directory" in caplog.text diff --git a/tests/test_main.py b/tests/test_main.py index 810b461..0f0ff80 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,7 @@ import pytest import sys import json +import logging from pathlib import Path from uScope.main import main @@ -32,3 +33,181 @@ def test_main_basic(tmp_path: Path, monkeypatch): assert output_file.exists() data = json.loads(output_file.read_text()) assert isinstance(data, list) + + +def test_main_verbose(tmp_path: Path, monkeypatch, caplog): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + output_file = tmp_path.joinpath("output.json") + + caplog.set_level(logging.DEBUG) + monkeypatch.setattr( + sys, "argv", + ["uscope", "-v", "--input-file", str(input_file), "--output-file", str(output_file)], + ) + main() + assert output_file.exists() + + +def test_main_quiet(tmp_path: Path, monkeypatch, caplog): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + output_file = tmp_path.joinpath("output.json") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(input_file), "--output-file", str(output_file)], + ) + main() + assert output_file.exists() + + +def test_main_gzip(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + output_file = tmp_path.joinpath("output.json.gz") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "-z", "--input-file", str(input_file), "--output-file", str(output_file)], + ) + main() + assert output_file.exists() + import gzip + data = json.loads(gzip.open(output_file, 'rt').read()) + assert isinstance(data, list) + + +def test_main_empty_trace(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text("") + output_file = tmp_path.joinpath("output.json") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(input_file), "--output-file", str(output_file)], + ) + with pytest.raises(SystemExit) as exc: + main() + assert exc.value.code == 1 + + +def test_main_file_not_found(tmp_path: Path, monkeypatch): + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(tmp_path / "nonexistent.out"), "--output-file", str(tmp_path / "out.json")], + ) + with pytest.raises(SystemExit) as exc: + main() + assert exc.value.code == 2 + + +def test_main_non_out_extension(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.txt") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + output_file = tmp_path.joinpath("output.json") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(input_file), "--output-file", str(output_file)], + ) + main() + assert output_file.exists() + + +def test_main_auto_output_name(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + expected_output = tmp_path.joinpath("trace.json") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(input_file)], + ) + main() + assert expected_output.exists() + + +def test_main_gzip_auto_extension(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.out") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + expected_output = tmp_path.joinpath("trace.json.gz") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "-z", "--input-file", str(input_file)], + ) + main() + assert expected_output.exists() + import gzip + data = json.loads(gzip.open(expected_output, 'rt').read()) + assert isinstance(data, list) + + +def test_main_auto_output_txt_extension(tmp_path: Path, monkeypatch): + input_file = tmp_path.joinpath("trace.txt") + input_file.write_text( + "O3PipeView:fetch:1000:0x1000:0:1:add x1, x2, x3:IntAlu\n" + "O3PipeView:decode:1100\n" + "O3PipeView:rename:1150\n" + "O3PipeView:dispatch:1200\n" + "O3PipeView:issue:1300\n" + "O3PipeView:complete:1400\n" + "O3PipeView:retire:1500:store:0\n" + ) + expected_output = tmp_path.joinpath("trace.txt.json") + + monkeypatch.setattr( + sys, "argv", + ["uscope", "-q", "--input-file", str(input_file)], + ) + main() + assert expected_output.exists() diff --git a/tests/test_parser.py b/tests/test_parser.py index dffc25e..26768db 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -387,3 +387,47 @@ def test_parse_unordered(trace_with_unordered): ), } assert_instructions(parser, expected, all_stages) + + +def test_parse_malformed_fetch(trace_with_malformed_fetch): + parser = PipeViewParser() + parser.parse_file(str(trace_with_malformed_fetch)) + + all_stages = PipelineStage.order() + expected = { + 2: ( + "0x2000", + "lw a0, 0(a1)", + "MemRead", + { + PipelineStage.FETCH: 2000, + PipelineStage.DECODE: 2100, + PipelineStage.RENAME: 0, + PipelineStage.DISPATCH: 0, + PipelineStage.ISSUE: 0, + PipelineStage.COMPLETE: 0, + PipelineStage.RETIRE: 2500, + }, + ), + } + assert len(parser.instructions) == len(expected) + assert parser.instructions[2].disasm == "lw a0, 0(a1)" + + +def test_empty_disasm_mnemonic(): + from uScope.O3 import Instruction + + instr = Instruction( + seq_num=1, pc="0x0", disasm="", opclass="No_OpClass", + stages={}, stage_order=[] + ) + assert instr.mnemonic == Instruction.UNKNOWN + + +def test_store_completion_parsed(trace_with_store_completion): + parser = PipeViewParser() + parser.parse_file(str(trace_with_store_completion)) + + instr = parser.instructions[1] + assert instr.store_tick == 2000 + assert instr.stages[PipelineStage.RETIRE] == 1500