From 4b87556135063c1757900cc82f7777ecc2307400 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:26:03 -0700 Subject: [PATCH] Add comprehensive test suite for PeakRDL-BusDecoder with sub-block and integration tests (#6) * Initial plan * Add comprehensive test suite for PeakRDL-BusDecoder - Added tests for utility functions (clog2, is_pow2, roundup_pow2, get_indexed_path) - Added tests for body classes (Body, ForLoopBody, IfBody, CombinationalBody, StructBody) - Added tests for code generators (DecodeLogicGenerator, StructGenerator) - Added tests for DesignState configuration - Added integration tests for BusDecoderExporter - Added tests for APB4 interface generation - Fixed conftest to properly handle RDLCompiler and temporary files - All 56 tests passing Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> * Format test files with ruff Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arnavsacheti <36746504+arnavsacheti@users.noreply.github.com> --- tests/unit/conftest.py | 10 +- tests/unit/test_body.py | 285 +++++++++++++++++++++++++++++++ tests/unit/test_exporter.py | 258 ++++++++++++++++++++++++++++ tests/unit/test_generators.py | 312 ++++++++++++++++++++++++++++++++++ tests/unit/test_utils.py | 262 ++++++++++++++++++++++++++++ 5 files changed, 1124 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_body.py create mode 100644 tests/unit/test_exporter.py create mode 100644 tests/unit/test_generators.py create mode 100644 tests/unit/test_utils.py diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 100283b..4ec6582 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -43,10 +43,14 @@ def compile_rdl(tmp_path: Path): try: compiler.compile_file(tmp_file.name) if top is not None: - return compiler.elaborate(top) - return compiler.elaborate() + root = compiler.elaborate(top) + return root.top + root = compiler.elaborate() + return root.top except RDLCompileError: - compiler.print_messages() + # Print error messages if available + if hasattr(compiler, "print_messages"): + compiler.print_messages() raise return _compile diff --git a/tests/unit/test_body.py b/tests/unit/test_body.py new file mode 100644 index 0000000..4a2e4d8 --- /dev/null +++ b/tests/unit/test_body.py @@ -0,0 +1,285 @@ +"""Tests for body classes used in code generation.""" + +from __future__ import annotations + +import pytest + +from peakrdl_busdecoder.body import ( + Body, + CombinationalBody, + ForLoopBody, + IfBody, + StructBody, +) + + +class TestBody: + """Test the base Body class.""" + + def test_empty_body(self): + """Test empty body returns empty string.""" + body = Body() + assert str(body) == "" + assert not body # Should be falsy when empty + + def test_add_single_line(self): + """Test adding a single line to body.""" + body = Body() + body += "line1" + assert str(body) == "line1" + assert body # Should be truthy when not empty + + def test_add_multiple_lines(self): + """Test adding multiple lines to body.""" + body = Body() + body += "line1" + body += "line2" + body += "line3" + expected = "line1\nline2\nline3" + assert str(body) == expected + + def test_add_returns_self(self): + """Test that add operation returns self for chaining.""" + body = Body() + body += "line1" + body += "line2" + # Chaining works because += returns self + assert len(body.lines) == 2 + + def test_add_nested_body(self): + """Test adding another body as a line.""" + outer = Body() + inner = Body() + inner += "inner1" + inner += "inner2" + outer += "outer1" + outer += inner + outer += "outer2" + expected = "outer1\ninner1\ninner2\nouter2" + assert str(outer) == expected + + +class TestForLoopBody: + """Test the ForLoopBody class.""" + + def test_genvar_for_loop(self): + """Test genvar-style for loop.""" + body = ForLoopBody("genvar", "i", 4) + body += "statement1;" + body += "statement2;" + + result = str(body) + assert "for (genvar i = 0; i < 4; i++)" in result + assert "statement1;" in result + assert "statement2;" in result + assert "end" in result + + def test_int_for_loop(self): + """Test int-style for loop.""" + body = ForLoopBody("int", "j", 8) + body += "assignment = value;" + + result = str(body) + assert "for (int j = 0; j < 8; j++)" in result + assert "assignment = value;" in result + assert "end" in result + + def test_empty_for_loop(self): + """Test empty for loop.""" + body = ForLoopBody("genvar", "k", 2) + result = str(body) + # Empty for loop should still have structure + assert "for (genvar k = 0; k < 2; k++)" in result + + def test_nested_for_loops(self): + """Test nested for loops.""" + outer = ForLoopBody("genvar", "i", 3) + inner = ForLoopBody("genvar", "j", 2) + inner += "nested_statement;" + outer += inner + + result = str(outer) + assert "for (genvar i = 0; i < 3; i++)" in result + assert "for (genvar j = 0; j < 2; j++)" in result + assert "nested_statement;" in result + + +class TestIfBody: + """Test the IfBody class.""" + + def test_simple_if(self): + """Test simple if statement.""" + body = IfBody() + with body.cm("condition1") as b: + b += "statement1;" + + result = str(body) + assert "if (condition1)" in result + assert "statement1;" in result + assert "end" in result + + def test_if_else(self): + """Test if-else statement.""" + body = IfBody() + with body.cm("condition1") as b: + b += "if_statement;" + with body.cm(None) as b: # None for else + b += "else_statement;" + + result = str(body) + assert "if (condition1)" in result + assert "if_statement;" in result + assert "else" in result + assert "else_statement;" in result + + def test_if_elif_else(self): + """Test if-elif-else chain.""" + body = IfBody() + with body.cm("condition1") as b: + b += "statement1;" + with body.cm("condition2") as b: + b += "statement2;" + with body.cm(None) as b: # None for else + b += "statement3;" + + result = str(body) + assert "if (condition1)" in result + assert "statement1;" in result + assert "else if (condition2)" in result + assert "statement2;" in result + assert "else" in result + assert "statement3;" in result + + def test_multiple_elif(self): + """Test multiple elif statements.""" + body = IfBody() + with body.cm("cond1") as b: + b += "stmt1;" + with body.cm("cond2") as b: + b += "stmt2;" + with body.cm("cond3") as b: + b += "stmt3;" + + result = str(body) + assert "if (cond1)" in result + assert "else if (cond2)" in result + assert "else if (cond3)" in result + + def test_empty_if_branches(self): + """Test if statement with empty branches.""" + body = IfBody() + with body.cm("condition"): + pass + + result = str(body) + assert "if (condition)" in result + + def test_nested_if(self): + """Test nested if statements.""" + outer = IfBody() + with outer.cm("outer_cond") as outer_body: + inner = IfBody() + with inner.cm("inner_cond") as inner_body: + inner_body += "nested_statement;" + outer_body += inner + + result = str(outer) + assert "if (outer_cond)" in result + assert "if (inner_cond)" in result + assert "nested_statement;" in result + + +class TestCombinationalBody: + """Test the CombinationalBody class.""" + + def test_simple_combinational_block(self): + """Test simple combinational block.""" + body = CombinationalBody() + body += "assign1 = value1;" + body += "assign2 = value2;" + + result = str(body) + assert "always_comb" in result + assert "begin" in result + assert "assign1 = value1;" in result + assert "assign2 = value2;" in result + assert "end" in result + + def test_empty_combinational_block(self): + """Test empty combinational block.""" + body = CombinationalBody() + result = str(body) + assert "always_comb" in result + assert "begin" in result + assert "end" in result + + def test_combinational_with_if_statement(self): + """Test combinational block with if statement.""" + cb = CombinationalBody() + ifb = IfBody() + with ifb.cm("condition") as b: + b += "assignment = value;" + cb += ifb + + result = str(cb) + assert "always_comb" in result + assert "if (condition)" in result + assert "assignment = value;" in result + + +class TestStructBody: + """Test the StructBody class.""" + + def test_simple_struct(self): + """Test simple struct definition.""" + body = StructBody("my_struct_t", packed=True, typedef=True) + body += "logic [7:0] field1;" + body += "logic field2;" + + result = str(body) + assert "typedef struct packed" in result + assert "my_struct_t" in result + assert "logic [7:0] field1;" in result + assert "logic field2;" in result + + def test_unpacked_struct(self): + """Test unpacked struct definition.""" + body = StructBody("unpacked_t", packed=False, typedef=True) + body += "int field1;" + + result = str(body) + assert "typedef struct" in result + assert "packed" not in result or "typedef struct {" in result + assert "unpacked_t" in result + + def test_struct_without_typedef(self): + """Test struct without typedef.""" + body = StructBody("my_struct", packed=True, typedef=False) + body += "logic field;" + + result = str(body) + # When typedef=False, packed is not used + assert "struct {" in result + assert "typedef" not in result + assert "my_struct" in result + + def test_empty_struct(self): + """Test empty struct.""" + body = StructBody("empty_t", packed=True, typedef=True) + result = str(body) + assert "typedef struct packed" in result + assert "empty_t" in result + + def test_nested_struct(self): + """Test struct with nested struct.""" + outer = StructBody("outer_t", packed=True, typedef=True) + inner = StructBody("inner_t", packed=True, typedef=True) + inner += "logic field1;" + outer += "logic field2;" + outer += str(inner) # Include inner struct as a string + + result = str(outer) + assert "outer_t" in result + assert "field2;" in result + # Inner struct should appear in the string + assert "inner_t" in result diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py new file mode 100644 index 0000000..0da16e4 --- /dev/null +++ b/tests/unit/test_exporter.py @@ -0,0 +1,258 @@ +"""Integration tests for the BusDecoderExporter.""" + +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif +from peakrdl_busdecoder.exporter import BusDecoderExporter + + +class TestBusDecoderExporter: + """Test the top-level BusDecoderExporter.""" + + def test_simple_register_export(self, compile_rdl, tmp_path): + """Test exporting a simple register.""" + rdl_source = """ + addrmap simple_reg { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="simple_reg") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + # Check that output files are created + module_file = tmp_path / "simple_reg.sv" + package_file = tmp_path / "simple_reg_pkg.sv" + + assert module_file.exists() + assert package_file.exists() + + # Check basic content + module_content = module_file.read_text() + assert "module simple_reg" in module_content + assert "my_reg" in module_content + + package_content = package_file.read_text() + assert "package simple_reg_pkg" in package_content + + def test_register_array_export(self, compile_rdl, tmp_path): + """Test exporting a register array.""" + rdl_source = """ + addrmap reg_array { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_regs[4] @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="reg_array") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + # Check that output files are created + module_file = tmp_path / "reg_array.sv" + assert module_file.exists() + + module_content = module_file.read_text() + assert "module reg_array" in module_content + assert "my_regs" in module_content + + def test_nested_addrmap_export(self, compile_rdl, tmp_path): + """Test exporting nested addrmaps.""" + rdl_source = """ + addrmap inner_block { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } inner_reg @ 0x0; + }; + + addrmap outer_block { + inner_block inner @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="outer_block") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + # Check that output files are created + module_file = tmp_path / "outer_block.sv" + assert module_file.exists() + + module_content = module_file.read_text() + assert "module outer_block" in module_content + assert "inner" in module_content + assert "inner_reg" in module_content + + def test_custom_module_name(self, compile_rdl, tmp_path): + """Test exporting with custom module name.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, module_name="custom_module", cpuif_cls=APB4Cpuif) + + # Check that output files use custom name + module_file = tmp_path / "custom_module.sv" + package_file = tmp_path / "custom_module_pkg.sv" + + assert module_file.exists() + assert package_file.exists() + + module_content = module_file.read_text() + assert "module custom_module" in module_content + + def test_custom_package_name(self, compile_rdl, tmp_path): + """Test exporting with custom package name.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, package_name="custom_pkg", cpuif_cls=APB4Cpuif) + + # Check that output files use custom package name + package_file = tmp_path / "custom_pkg.sv" + assert package_file.exists() + + package_content = package_file.read_text() + assert "package custom_pkg" in package_content + + def test_multiple_registers(self, compile_rdl, tmp_path): + """Test exporting multiple registers.""" + rdl_source = """ + addrmap multi_reg { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } reg1 @ 0x0; + + reg { + field { + sw=r; + hw=w; + } status[15:0]; + } reg2 @ 0x4; + + reg { + field { + sw=rw; + hw=r; + } control[7:0]; + } reg3 @ 0x8; + }; + """ + top = compile_rdl(rdl_source, top="multi_reg") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + module_file = tmp_path / "multi_reg.sv" + assert module_file.exists() + + module_content = module_file.read_text() + assert "module multi_reg" in module_content + assert "reg1" in module_content + assert "reg2" in module_content + assert "reg3" in module_content + + +class TestAPB4Interface: + """Test APB4 CPU interface generation.""" + + def test_apb4_port_declaration(self, compile_rdl, tmp_path): + """Test that APB4 interface ports are generated.""" + rdl_source = """ + addrmap apb_test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="apb_test") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + module_file = tmp_path / "apb_test.sv" + module_content = module_file.read_text() + + # Check for APB4 signals + assert "PSEL" in module_content or "psel" in module_content + assert "PENABLE" in module_content or "penable" in module_content + assert "PWRITE" in module_content or "pwrite" in module_content + assert "PADDR" in module_content or "paddr" in module_content + assert "PWDATA" in module_content or "pwdata" in module_content + assert "PRDATA" in module_content or "prdata" in module_content + assert "PREADY" in module_content or "pready" in module_content + + def test_apb4_read_write_logic(self, compile_rdl, tmp_path): + """Test that APB4 read/write logic is generated.""" + rdl_source = """ + addrmap apb_rw { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="apb_rw") + + exporter = BusDecoderExporter() + output_dir = str(tmp_path) + exporter.export(top, output_dir, cpuif_cls=APB4Cpuif) + + module_file = tmp_path / "apb_rw.sv" + module_content = module_file.read_text() + + # Basic sanity checks for logic generation + assert "always" in module_content or "assign" in module_content + assert "my_reg" in module_content diff --git a/tests/unit/test_generators.py b/tests/unit/test_generators.py new file mode 100644 index 0000000..5f678ff --- /dev/null +++ b/tests/unit/test_generators.py @@ -0,0 +1,312 @@ +"""Tests for code generation classes.""" + +from __future__ import annotations + +import pytest + +from peakrdl_busdecoder.cpuif.apb4 import APB4Cpuif +from peakrdl_busdecoder.decode_logic_gen import DecodeLogicFlavor, DecodeLogicGenerator +from peakrdl_busdecoder.design_state import DesignState +from peakrdl_busdecoder.exporter import BusDecoderExporter +from peakrdl_busdecoder.struct_gen import StructGenerator + + +class TestDecodeLogicGenerator: + """Test the DecodeLogicGenerator.""" + + def test_decode_logic_read(self, compile_rdl): + """Test decode logic generation for read operations.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) + + # Basic sanity check - it should initialize + assert gen is not None + assert gen._flavor == DecodeLogicFlavor.READ + + def test_decode_logic_write(self, compile_rdl): + """Test decode logic generation for write operations.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.WRITE) + + assert gen is not None + assert gen._flavor == DecodeLogicFlavor.WRITE + + def test_cpuif_addr_predicate(self, compile_rdl): + """Test address predicate generation.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x100; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = DecodeLogicGenerator(ds, DecodeLogicFlavor.READ) + + # Get the register node + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + predicates = gen.cpuif_addr_predicate(reg_node) + + # Should return a list of conditions + assert isinstance(predicates, list) + assert len(predicates) > 0 + # Should check address bounds + for pred in predicates: + assert "cpuif_rd_addr" in pred or ">=" in pred or "<" in pred + + def test_decode_logic_flavor_enum(self): + """Test DecodeLogicFlavor enum values.""" + assert DecodeLogicFlavor.READ.value == "rd" + assert DecodeLogicFlavor.WRITE.value == "wr" + + assert DecodeLogicFlavor.READ.cpuif_address == "cpuif_rd_addr" + assert DecodeLogicFlavor.WRITE.cpuif_address == "cpuif_wr_addr" + + assert DecodeLogicFlavor.READ.cpuif_select == "cpuif_rd_sel" + assert DecodeLogicFlavor.WRITE.cpuif_select == "cpuif_wr_sel" + + +class TestStructGenerator: + """Test the StructGenerator.""" + + def test_simple_struct_generation(self, compile_rdl): + """Test struct generation for simple register.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Should generate struct definition + assert gen is not None + result = str(gen) + + # Should contain struct declaration + assert "struct" in result or "typedef" in result + + def test_nested_struct_generation(self, compile_rdl): + """Test struct generation for nested addrmaps.""" + rdl_source = """ + addrmap inner { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } inner_reg @ 0x0; + }; + + addrmap outer { + inner my_inner @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="outer") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Walk the tree to generate structs + from systemrdl.walker import RDLWalker + + walker = RDLWalker() + walker.walk(top, gen, skip_top=True) + + result = str(gen) + + # Should contain struct declaration + assert "struct" in result or "typedef" in result + # The struct should reference the inner component + assert "my_inner" in result + + def test_array_struct_generation(self, compile_rdl): + """Test struct generation for register arrays.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_regs[4] @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + gen = StructGenerator(ds) + + # Walk the tree to generate structs + from systemrdl.walker import RDLWalker + + walker = RDLWalker() + walker.walk(top, gen, skip_top=True) + + result = str(gen) + + # Should contain array notation + assert "[" in result and "]" in result + # Should reference the register + assert "my_regs" in result + + +class TestDesignState: + """Test the DesignState class.""" + + def test_design_state_basic(self, compile_rdl): + """Test basic DesignState initialization.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + + assert ds.top_node == top + assert ds.module_name == "test" + assert ds.package_name == "test_pkg" + assert ds.cpuif_data_width == 32 # Should infer from 32-bit field + assert ds.addr_width > 0 + + def test_design_state_custom_module_name(self, compile_rdl): + """Test DesignState with custom module name.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"module_name": "custom_module"}) + + assert ds.module_name == "custom_module" + assert ds.package_name == "custom_module_pkg" + + def test_design_state_custom_package_name(self, compile_rdl): + """Test DesignState with custom package name.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"package_name": "custom_pkg"}) + + assert ds.package_name == "custom_pkg" + + def test_design_state_custom_address_width(self, compile_rdl): + """Test DesignState with custom address width.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"address_width": 16}) + + assert ds.addr_width == 16 + + def test_design_state_unroll_arrays(self, compile_rdl): + """Test DesignState with cpuif_unroll option.""" + rdl_source = """ + addrmap test { + reg { + field { + sw=rw; + hw=r; + } data[31:0]; + } my_regs[4] @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {"cpuif_unroll": True}) + + assert ds.cpuif_unroll is True + + def test_design_state_64bit_registers(self, compile_rdl): + """Test DesignState with wider data width.""" + rdl_source = """ + addrmap test { + reg { + regwidth = 32; + field { + sw=rw; + hw=r; + } data[31:0]; + } my_reg @ 0x0; + }; + """ + top = compile_rdl(rdl_source, top="test") + + ds = DesignState(top, {}) + + # Should infer 32-bit data width from field + assert ds.cpuif_data_width == 32 diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..74ace17 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,262 @@ +"""Tests for utility functions.""" + +from __future__ import annotations + +import pytest +from systemrdl import RDLCompiler +from systemrdl.node import AddrmapNode + +from peakrdl_busdecoder.utils import clog2, get_indexed_path, is_pow2, roundup_pow2 + + +class TestMathUtils: + """Test mathematical utility functions.""" + + def test_clog2_basic(self): + """Test clog2 function with basic values.""" + assert clog2(1) == 0 + assert clog2(2) == 1 + assert clog2(3) == 2 + assert clog2(4) == 2 + assert clog2(5) == 3 + assert clog2(8) == 3 + assert clog2(9) == 4 + assert clog2(16) == 4 + assert clog2(17) == 5 + assert clog2(32) == 5 + assert clog2(33) == 6 + assert clog2(64) == 6 + assert clog2(128) == 7 + assert clog2(256) == 8 + assert clog2(1024) == 10 + + def test_is_pow2_true_cases(self): + """Test is_pow2 returns True for powers of 2.""" + assert is_pow2(1) is True + assert is_pow2(2) is True + assert is_pow2(4) is True + assert is_pow2(8) is True + assert is_pow2(16) is True + assert is_pow2(32) is True + assert is_pow2(64) is True + assert is_pow2(128) is True + assert is_pow2(256) is True + assert is_pow2(512) is True + assert is_pow2(1024) is True + + def test_is_pow2_false_cases(self): + """Test is_pow2 returns False for non-powers of 2.""" + assert is_pow2(0) is False + assert is_pow2(3) is False + assert is_pow2(5) is False + assert is_pow2(6) is False + assert is_pow2(7) is False + assert is_pow2(9) is False + assert is_pow2(10) is False + assert is_pow2(15) is False + assert is_pow2(17) is False + assert is_pow2(100) is False + assert is_pow2(255) is False + assert is_pow2(1000) is False + + def test_roundup_pow2_already_power_of_2(self): + """Test roundup_pow2 with values that are already powers of 2.""" + assert roundup_pow2(1) == 1 + assert roundup_pow2(2) == 2 + assert roundup_pow2(4) == 4 + assert roundup_pow2(8) == 8 + assert roundup_pow2(16) == 16 + assert roundup_pow2(32) == 32 + assert roundup_pow2(64) == 64 + assert roundup_pow2(128) == 128 + assert roundup_pow2(256) == 256 + + def test_roundup_pow2_non_power_of_2(self): + """Test roundup_pow2 with values that are not powers of 2.""" + assert roundup_pow2(3) == 4 + assert roundup_pow2(5) == 8 + assert roundup_pow2(6) == 8 + assert roundup_pow2(7) == 8 + assert roundup_pow2(9) == 16 + assert roundup_pow2(15) == 16 + assert roundup_pow2(17) == 32 + assert roundup_pow2(31) == 32 + assert roundup_pow2(33) == 64 + assert roundup_pow2(100) == 128 + assert roundup_pow2(255) == 256 + assert roundup_pow2(257) == 512 + + +class TestGetIndexedPath: + """Test get_indexed_path function.""" + + def test_simple_path(self, compile_rdl): + """Test simple path without arrays.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field {} data; + } my_reg; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + # Get the register node by iterating through children + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + + assert reg_node is not None + path = get_indexed_path(top, reg_node) + assert path == "my_reg" + + def test_nested_path(self, compile_rdl): + """Test nested path without arrays.""" + rdl_source = """ + addrmap inner_map { + reg { + field {} data; + } my_reg; + }; + + addrmap my_addrmap { + inner_map inner; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + # Navigate to the nested register + inner_node = None + for child in top.children(): + if child.inst_name == "inner": + inner_node = child + break + assert inner_node is not None + + reg_node = None + for child in inner_node.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + path = get_indexed_path(top, reg_node) + assert path == "inner.my_reg" + + def test_array_path(self, compile_rdl): + """Test path with array indices.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field {} data; + } my_reg[4]; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + path = get_indexed_path(top, reg_node) + assert path == "my_reg[i0]" + + def test_multidimensional_array_path(self, compile_rdl): + """Test path with multidimensional arrays.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field {} data; + } my_reg[2][3]; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + path = get_indexed_path(top, reg_node) + assert path == "my_reg[i0][i1]" + + def test_nested_array_path(self, compile_rdl): + """Test path with nested arrays.""" + rdl_source = """ + addrmap inner_map { + reg { + field {} data; + } my_reg[2]; + }; + + addrmap my_addrmap { + inner_map inner[3]; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + # Navigate to the nested register + inner_node = None + for child in top.children(): + if child.inst_name == "inner": + inner_node = child + break + assert inner_node is not None + + reg_node = None + for child in inner_node.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + path = get_indexed_path(top, reg_node) + assert path == "inner[i0].my_reg[i1]" + + def test_custom_indexer(self, compile_rdl): + """Test path with custom indexer name.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field {} data; + } my_reg[4]; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + reg_node = None + for child in top.children(): + if child.inst_name == "my_reg": + reg_node = child + break + assert reg_node is not None + + path = get_indexed_path(top, reg_node, indexer="idx") + assert path == "my_reg[idx0]" + + def test_skip_kw_filter(self, compile_rdl): + """Test path with keyword filtering skipped.""" + rdl_source = """ + addrmap my_addrmap { + reg { + field {} data; + } always_reg; + }; + """ + top = compile_rdl(rdl_source, top="my_addrmap") + reg_node = None + for child in top.children(): + if child.inst_name == "always_reg": + reg_node = child + break + assert reg_node is not None + + # With keyword filter (default) - SystemRDL identifiers can use keywords but SV can't + path = get_indexed_path(top, reg_node) + # The path should contain always_reg + assert "always_reg" in path + + # Without keyword filter + path = get_indexed_path(top, reg_node, skip_kw_filter=True) + assert path == "always_reg"