"""
Unit Tests for the {{top_node.inst_name}} register model Python Wrapper

This code was generated from the PeakRDL-python package
"""
from array import array as Array
import unittest
from unittest.mock import patch, call
import random
from itertools import combinations
import math

from ..lib import CallbackSet

from ..reg_model.{{top_node.type_name}} import {{top_node.type_name}}_cls

# dummy functions to support the test cases, note that these are not used as
# they get patched
def read_addr_space(addr: int, width: int, accesswidth: int):
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    return 0

def write_addr_space(addr: int, width: int, accesswidth: int,  data: int):
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    assert isinstance(data, int)

def read_callback(addr: int, width: int, accesswidth: int):
    return read_addr_space(addr=addr, width=width, accesswidth=accesswidth)

def read_block_addr_space(addr: int, width: int, accesswidth: int, length:int) -> Array:
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    assert isinstance(length, int)

    if width == 32:
        typecode = 'L'
    elif width == 64:
        typecode = 'Q'
    elif width == 16:
        typecode = 'I'
    elif width == 8:
        typecode = 'B'
    else:
        raise ValueError('unhandled memory width')

    return Array(typecode, [0 for x in range(length)])

def read_block_callback(addr: int, width: int, accesswidth: int, length: int):
    return read_block_addr_space(addr=addr, width=width, accesswidth=accesswidth, length=length)

def write_callback(addr: int, width: int, accesswidth: int,  data: int):
    write_addr_space(addr=addr, width=width, accesswidth=accesswidth, data=data)

def write_block_addr_space(addr: int, width: int, accesswidth: int,  data: Array):
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    assert isinstance(data, Array)

def write_block_callback(addr: int, width: int, accesswidth: int,  data: Array):
    write_block_addr_space(addr=addr, width=width, accesswidth=accesswidth, data=data)

class {{top_node.type_name}}_TestCase(unittest.TestCase):

    def setUp(self):
        self.dut = {{top_node.type_name}}_cls(CallbackSet(read_callback=read_callback,
                                                          write_callback=write_callback))

    @staticmethod
    def _reverse_bits(value: int, number_bits: int) -> int:
        """

        Args:
            value: value to reverse
            number_bits: number of bits used in the value

        Returns:
            reversed valued
        """
        result = 0
        for i in range(number_bits):
            if (value >> i) & 1:
                result |= 1 << (number_bits - 1 - i)
        return result

    def test_inst_name(self):
        """
        Walk the address map and check the inst name has been correctly populated
        """
        {% for node in top_node.descendants(unroll=True) -%}
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.inst_name, '{{node.get_path_segments()[-1]}}')
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.full_inst_name, '{{'.'.join(node.get_path_segments())}}')
        {% endfor %}

    def test_register_properties(self):
        """
        Walk the address map and check the address, size and accesswidth of every register is
        correct
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlRegNode) %}
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.address, {{node.absolute_address}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.width, {{node.size * 8}})
        {% if 'accesswidth' in node.list_properties() -%}self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth, {{node.get_property('accesswidth')}}){%- else -%} self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth, self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth){%- endif %}
            {%- endif %}
        {%- endfor %}

    def test_memory_properties(self):
        """
        Walk the address map and check the address, size and accesswidth of every memory is
        correct
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlMemNode) %}
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.address, {{node.absolute_address}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.width, {{node.get_property('memwidth')}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.entries, {{node.get_property('mementries')}})
        {% if 'accesswidth' in node.list_properties() -%}self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth, {{node.get_property('accesswidth')}}){%- else -%} self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth, self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth){%- endif %}
            {%- endif %}
        {%- endfor %}

    def test_field_properties(self):
        """
        walk the address map and check:
        - that the lsb and msb of every field is correct
        - that where default values are provided they are applied correctly
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlFieldNode) %}
        # test field properties: {{'.'.join(node.get_path_segments())}}
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.lsb,
                        {{node.lsb}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.msb,
                         {{node.msb}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.low,
                        {{node.low}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.high,
                         {{node.high}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.bitmask,
                         {{get_field_bitmask_hex_string(node)}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.inverse_bitmask,
                         {{get_field_inv_bitmask_hex_string(node)}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.max_value,
                         {{get_field_max_value_hex_string(node)}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.default,
                         {{get_field_default_value(node)}})
        self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.is_volatile,
                         {{node.is_hw_writable}})
            {%- endif -%}
        {%- endfor %}

    def test_register_read_and_write(self):
        """
        Walk the register map and check every register can be read and written to correctly
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlRegNode) %}

        # test access operations (read and/or write) to register:
        # {{'.'.join(node.get_path_segments())}}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock, \
            patch(__name__ + '.' + 'read_addr_space', return_value=1) as read_callback_mock:

            {% if node.has_sw_readable -%}
            # test reading back 1 (the unpatched version returns 0 so this confirms the patch works)
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(), 1)
            read_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth)

            # test the read check with high value
            read_callback_mock.reset_mock()
            read_callback_mock.return_value = {{get_reg_max_value_hex_string(node)}}
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(), {{get_reg_max_value_hex_string(node)}})
            read_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth)

            # test the read of the low value
            read_callback_mock.reset_mock()
            read_callback_mock.return_value = 0
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(), 0x0)
            read_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth)

            # test the read of a random value
            random_value = random.randrange(0, {{get_reg_max_value_hex_string(node)}}+1)
            read_callback_mock.reset_mock()
            read_callback_mock.return_value = random_value
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(), random_value)
            read_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth)

            # at the end of the read tests the write should not have been called
            read_callback_mock.reset_mock()
            write_callback_mock.assert_not_called()
            {% endif %}

            {% if node.has_sw_writable -%}
            # test the write with high value
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write({{get_reg_max_value_hex_string(node)}})
            write_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth,
                                data={{get_reg_max_value_hex_string(node)}})
            write_callback_mock.reset_mock()

            # test the write of a low value
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(0)
            write_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth,
                                data=0)
            write_callback_mock.reset_mock()

            # test the write of a random
            random_value = random.randrange(0, {{get_reg_max_value_hex_string(node)}}+1)
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(random_value)
            write_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth,
                                data=random_value)
            write_callback_mock.reset_mock()

            # test writting a value beyond the register range is blocked with an exception being raised
            with self.assertRaises(ValueError):
                self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(-1)

            with self.assertRaises(ValueError):
                self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write({{get_reg_max_value_hex_string(node)}}+1)

            {%- else %}
            # test that a non-writable register has no write method and attempting one generates and error
            with self.assertRaises(AttributeError):
                self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(0)
            {%- endif %}

            # check the read has not been called in the write test
            read_callback_mock.assert_not_called()
            {%- endif %}
        {%- endfor %}

    def test_int_field_read_and_write(self):
        """
        Check the ability to read and write to integer (non-eumn) fields
        """
    {%- for node in top_node.descendants(unroll=True) -%}
        {%- if isinstance(node, systemrdlFieldNode) %}
        {%- if 'encode' not in node.list_properties() %}

        # test access operations (read and/or write) to field:
        # {{'.'.join(node.get_path_segments())}}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock,\
            patch(__name__ + '.' + 'read_addr_space', return_value=0) as read_callback_mock:

            {% if node.is_sw_readable %}

            # read back - zero, this is achieved by setting the register to inverse bitmask
            read_callback_mock.return_value = {{get_field_inv_bitmask_hex_string(node)}}
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(),
                             0)
            read_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth)

            # read back - max_value, this is achieved by setting the register to bitmask
            read_callback_mock.reset_mock()
            read_callback_mock.return_value = {{get_field_bitmask_hex_string(node)}}
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(),
                             {{ get_field_max_value_hex_string(node) }})
            read_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth)

            # read back - random value
            read_callback_mock.reset_mock()
            random_value = random.randrange(0, {{get_reg_max_value_hex_string(node.parent)}}+1)
            read_callback_mock.return_value = random_value
            random_field_value = (random_value & {{get_field_bitmask_hex_string(node)}}) >> {{node.low}}
            {% if node.msb == node.high -%}
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(),
                             random_field_value)
            {% else -%}
            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(),
                             self._reverse_bits(value=random_field_value, number_bits={{node.width}}))
            {% endif -%}
            read_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth)

            # at the end of the read tests the write should not have been called
            read_callback_mock.reset_mock()
            write_callback_mock.assert_not_called()
            {%- endif %}

            {%- if node.is_sw_writable %}
            # check the write
            random_reg_value = random.randrange(0, {{get_reg_max_value_hex_string(node.parent)}} + 1)
            random_field_value = random.randrange(0, {{ get_field_max_value_hex_string(node) }} + 1)
            for reg_base_value in [0, {{get_reg_max_value_hex_string(node.parent)}}, random_reg_value]:
                for field_value in [0, {{ get_field_max_value_hex_string(node) }}, random_field_value]:
                    read_callback_mock.reset_mock()
                    write_callback_mock.reset_mock()
                    read_callback_mock.return_value = reg_base_value

                    self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(field_value)

                    {% if (((node.high+1) - node.low) < (node.parent.size*8)) and (node.parent.has_sw_readable) %}
                    read_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth)
                    {% else %}
                    read_callback_mock.assert_not_called()
                    {%- endif %}
                    {% if node.parent.has_sw_readable -%}
                    {% if node.msb == node.high -%}
                    write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=(reg_base_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & (field_value << {{node.low}})))
                    {% else -%}
                    write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=(reg_base_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & (self._reverse_bits(value=field_value, number_bits={{node.width}}) << {{node.low}})))
                    {% endif -%}
                    {% else -%}
                    # if the register is not readable, the value is simply written
                    {% if node.msb == node.high -%}
                    write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=field_value << {{node.low}})
                    {% else -%}
                    write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=self._reverse_bits(value=field_value, number_bits={{node.width}}) << {{node.low}})
                    {% endif -%}
                    {% endif %}

            # check invalid write values bounce
            with self.assertRaises(ValueError):
                self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write({{ get_field_max_value_hex_string(node) }} + 1)

            with self.assertRaises(ValueError):
                self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(-1)
            {%- endif %}

        {%- endif %}
        {%- endif %}
    {%- endfor %}

    {% if uses_enum %}
    def test_enum_field_read_and_write(self):
        """
        Check the ability to read and write to enum fields
        """
    {%- for node in top_node.descendants(unroll=True) -%}
        {%- if isinstance(node, systemrdlFieldNode) %}
        {%- if 'encode' in node.list_properties() %}

        # test access operations (read and/or write) to field:
        # {{'.'.join(node.get_path_segments())}}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock,\
            patch(__name__ + '.' + 'read_addr_space', return_value=0) as read_callback_mock:

            {% if node.is_sw_readable %}
            # read back test
            {%- for value_of_enum_needed in node.get_property('encode') -%}
            # set the simulated read_back value to {{value_of_enum_needed.name}}
            random_reg_value = random.randrange(0, {{get_reg_max_value_hex_string(node.parent)}} + 1)
            read_callback_mock.reset_mock()
            {% if node.msb == node.high -%}
            read_callback_mock.return_value = (random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                              ({{get_field_bitmask_hex_string(node)}} & ({{value_of_enum_needed.value}} << {{node.low}} ))
            {% else -%}
            read_callback_mock.return_value = (random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                              ({{get_field_bitmask_hex_string(node)}} & (self._reverse_bits(value={{value_of_enum_needed.value}}, number_bits={{node.width}}) << {{node.low}} ))
            {% endif -%}

            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(),
                             self.dut.{{'.'.join(node.get_path_segments()[1:])}}.enum_cls.{{value_of_enum_needed.name}})
            read_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth)
            {% endfor %}

            {% if node.get_property('encode').__len__() < (2**((node.high+1) - node.low)) %}
            # check that other values of the field int
            # that don't appear in the enum generate an
            # error
            for field_value in range(0, {{get_field_max_value_hex_string(node)}}+1):
                if field_value in [{%- for value_of_enum_needed in node.get_property('encode') -%}{{value_of_enum_needed.value}}{% if not loop.last %}, {% endif %}{%- endfor %}]:
                    # legal int value of the eunm so no test is needed
                    continue
                with self.assertRaises(ValueError):
                    read_callback_mock.reset_mock()
                    random_reg_value = random.randrange(0, {{get_reg_max_value_hex_string(node.parent)}} + 1)
                    {% if node.msb == node.high -%}
                    read_callback_mock.return_value = (random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & (field_value << {{node.low}}))
                    {% else -%}
                    read_callback_mock.return_value = (random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & (self._reverse_bits(value=field_value, number_bits={{node.width}}) << {{node.low}}))
                    {% endif -%}
                    decode_field_value = self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read()
            {% endif %}
            write_callback_mock.assert_not_called()
            {% endif %}

            {% if node.is_sw_writable %}
            enum_cls = self.dut.{{'.'.join(node.get_path_segments()[1:])}}.enum_cls
            {% for value_of_enum_needed in node.get_property('encode') %}
            random_reg_value = random.randrange(0, {{get_reg_max_value_hex_string(node.parent)}} + 1)
            read_callback_mock.reset_mock()
            write_callback_mock.reset_mock()
            read_callback_mock.return_value = random_reg_value

            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(enum_cls.{{value_of_enum_needed.name}})

            {% if ( node.width < (node.parent.size*8)) and (node.parent.has_sw_readable) %}
            read_callback_mock.assert_called_once()
            {% else %}
            read_callback_mock.assert_not_called()
            {%- endif %}

            {% if node.parent.has_sw_readable -%}
            {% if node.msb == node.high -%}
            write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=(random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & ({{value_of_enum_needed.value}} << {{node.low}})))
            {% else -%}
            write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=(random_reg_value & {{get_field_inv_bitmask_hex_string(node)}}) | \
                                     ({{get_field_bitmask_hex_string(node)}} & (self._reverse_bits(value={{value_of_enum_needed.value}}, number_bits={{node.width}}) << {{node.low}})))
            {% endif -%}
            {% else -%}
            # if the register is not readable, the value is simply written
            {% if node.msb == node.high -%}
            write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data={{value_of_enum_needed.value}} << {{node.low}})
            {% else -%}
            write_callback_mock.assert_called_once_with(
                                addr={{node.parent.absolute_address}},
                                width={{node.parent.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.parent_register.accesswidth,
                                data=self._reverse_bits(value={{value_of_enum_needed.value}}, number_bits={{node.width}}) << {{node.low}})
            {% endif -%}
            {% endif %}

            {% endfor %}
            {% endif %}

        {%- endif %}
        {%- endif %}
    {%- endfor %}
    {%- endif %}

    def test_register_read_fields(self):
        """
        Walk the register map and check every register read_fields method
        """
        {%- for node in top_node.descendants(unroll=True) -%}
        {%- if isinstance(node, systemrdlRegNode) %}
        {% if node.has_sw_readable %}
        # test read_fields to register:
        # {{'.'.join(node.get_path_segments())}}
        # build up the register value with a random base value, overlaid with
        # a random value for each field
        rand_reg_value = random.randrange(0, {{get_reg_max_value_hex_string(node)}} + 1)
        {%- for field in node.children() %}
        {%- if field.is_sw_readable -%}
        {%- if 'encode' in field.list_properties() %}
        rand_field_value = random.choice(list(self.dut.{{'.'.join(field.get_path_segments()[1:])}}.enum_cls)).value
        {%- else %}
        rand_field_value = random.randrange(0, {{ get_field_max_value_hex_string(field) }} + 1)
        {%- endif %}
        {% if field.msb == field.high -%}
        rand_reg_value = (rand_reg_value & {{get_field_inv_bitmask_hex_string(field)}}) | (rand_field_value << {{ field.low }})
        {% else -%}
        rand_reg_value = (rand_reg_value & {{get_field_inv_bitmask_hex_string(field)}}) | (self._reverse_bits(value=rand_field_value, number_bits={{field.width}}) << {{ field.low }})
        {% endif -%}
        {%- endif %}
        {% endfor %}
        with patch(__name__ + '.' + 'read_addr_space', return_value=rand_reg_value):
            # the read_fields method gets a dictionary back
            # from the object with all the read back field
            # values
            reference_read_fields = { {% for field in node.children() -%}
                                      {%- if field.is_sw_readable %}
                                      '{{ field.inst_name }}' : self.dut.{{'.'.join(field.get_path_segments()[1:])}}.read(){%- if not loop.last -%}, {%- endif %}
                                      {%- endif -%}
                                      {%- endfor %}
                                    }

            self.assertDictEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read_fields(),
                                 reference_read_fields)

        {%- endif %}
        {%- endif %}
        {%- endfor %}

    def test_register_write_fields(self):
        """
        Walk the register map and check every register write_fields method
        """
        def write_field_cominbinations(reg, writable_fields):
            with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock, \
                patch(__name__ + '.' + 'read_addr_space', return_value=0) as read_callback_mock:
                for num_parm in range(1, len(writable_fields) + 1):
                    for fields_to_write in combinations(writable_fields, num_parm):
                        kwargs = {}
                        expected_value = 0
                        for field_str in fields_to_write:
                            field = getattr(reg, field_str)
                            if hasattr(field, 'enum_cls'):
                                rand_enum_value = random.choice(list(field.enum_cls))
                                rand_field_value = rand_enum_value.value
                                kwargs[field_str] = rand_enum_value
                            else:
                                rand_field_value = random.randrange(0, field.max_value + 1)
                                kwargs[field_str] = rand_field_value

                            if field.msb == field.high:
                                expected_value = ( expected_value & field.inverse_bitmask ) | (rand_field_value << field.low)
                            elif field.msb == field.low:
                                expected_value = ( expected_value & field.inverse_bitmask ) | (self._reverse_bits(value=rand_field_value, number_bits=field.width) << field.low)
                            else:
                                raise RuntimeError('invalid msb/lsb high/low combination')

                        reg.write_fields(**kwargs)
                        write_callback_mock.assert_called_once_with(
                                addr=reg.address,
                                width=reg.width,
                                accesswidth=reg.accesswidth,
                                data=expected_value)
                        read_callback_mock.assert_called_once()
                        write_callback_mock.reset_mock()
                        read_callback_mock.reset_mock()


        {%- for node in top_node.descendants(unroll=True) -%}
        {%- if isinstance(node, systemrdlRegNode) %}
        {% if node.has_sw_writable %}
        # test read_fields to register:
        # {{'.'.join(node.get_path_segments())}}


        {% if node.has_sw_readable %}
        write_field_cominbinations(reg=self.dut.{{'.'.join(node.get_path_segments()[1:])}},
                                   writable_fields = [ {% for field in get_reg_writable_fields(node) -%}
                                                       '{{ field.inst_name }}' {%- if not loop.last -%},{%- endif %}
                                                       {% endfor -%} ])
        {% else %}
        kwargs = {}
        expected_value = 0
        {% for field in get_reg_writable_fields(node) %}
        {%- if 'encode' in field.list_properties() %}
        rand_enum_value = random.choice(list(self.dut.{{'.'.join(field.get_path_segments()[1:])}}.enum_cls))
        rand_field_value = rand_enum_value.value
        kwargs['{{field.inst_name}}'] = rand_enum_value
        {%- else %}
        rand_field_value = random.randrange(0, {{ get_field_max_value_hex_string(field) }} + 1)
        kwargs['{{field.inst_name}}'] = rand_field_value
        {%- endif  %}
        expected_value = ( expected_value & {{get_field_inv_bitmask_hex_string(field)}} ) | (rand_field_value << {{ field.low }})
        {% endfor %}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock, \
            patch(__name__ + '.' + 'read_addr_space', return_value=0) as read_callback_mock:
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write_fields(**kwargs)
            write_callback_mock.assert_called_once_with(
                                addr={{node.absolute_address}},
                                width={{node.size * 8}},
                                accesswidth=self.dut.{{'.'.join(node.get_path_segments()[1:])}}.accesswidth,
                                data=expected_value)
            read_callback_mock.assert_not_called()
            write_callback_mock.reset_mock()
            read_callback_mock.reset_mock()

        {% endif %}


        {%- endif %}
        {%- endif %}
        {%- endfor %}

    {% if uses_memory %}
    def test_memory_read_and_write(self):
        """
        Walk the register map and check every register can be read and written to correctly
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlMemNode) %}

        # test access operations (read and/or write) to register:
        # {{'.'.join(node.get_path_segments())}}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock, \
            patch(__name__ + '.' + 'read_addr_space', return_value=1) as read_callback_mock:

            {% if node.is_sw_readable -%}
            # checks single unit accesses at the first entry, the last entry and a random entry in
            # in each case check a 0, max value and random value being read
            for entry in [0, random.randint(0,{{node.get_property('mementries')-1}}), {{node.get_property('mementries')-1}}]:
                for value in [0, random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}), {{get_memory_max_entry_value_hex_string(node)}}]:
                    read_callback_mock.reset_mock()
                    read_callback_mock.return_value = value
                    self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(start_entry=entry, number_entries=1),
                                                             Array('{{get_array_typecode(node.get_property('memwidth'))}}', [value]))
                    read_callback_mock.assert_called_once_with(
                                        addr={{node.absolute_address}}+(entry * {{get_memory_width_bytes(node)}}),
                                        width={{node.get_property('memwidth')}},
                                        accesswidth={{node.get_property('memwidth')}})

            # check a multi-entry read, if the memory is small do the entire memory, however, if
            # it is large limit the number of entries to 10
            entries_to_test = {% if node.get_property('mementries') > 10 %}10{% else %}{{node.get_property('mementries')}}{% endif %}
            random_data = Array('{{get_array_typecode(node.get_property('memwidth'))}}',
                                [random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}) for x in range(entries_to_test)])

            def read_data_mock(addr:int, width:int, accesswidth:int) -> int:
                # the memory is entry width defined where as the address is byte address, so need
                # to convert between them, the sift is determined by
                shift = int(math.log2({% if 'accesswidth' in node.list_properties() -%}{{node.get_property('accesswidth')}}{%- else -%}{{node.get_property('memwidth')}}{%- endif -%}) - 3)
                mem_entry = (addr - {{node.absolute_address}}) >> shift
                return random_data[mem_entry]

            read_callback_mock.reset_mock()
            read_callback_mock.side_effect=read_data_mock

            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(start_entry=0,
                                          number_entries=entries_to_test),
                                          random_data)

            write_callback_mock.assert_not_called()
            read_callback_mock.reset_mock()
            {% endif %}

            {% if node.is_sw_writable -%}
            # checks single unit accesses at the first entry, the last entry and a random entry in
            # in each case check a 0, max value and random value being read
            for entry in [0, random.randint(0,{{node.get_property('mementries')-1}}), {{node.get_property('mementries')-1}}]:
                for value in [0, random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}), {{get_memory_max_entry_value_hex_string(node)}}]:
                    write_callback_mock.reset_mock()
                    self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(start_entry=entry, data=Array('{{get_array_typecode(node.get_property('memwidth'))}}', [value]))
                    write_callback_mock.assert_called_once_with(
                                        addr={{node.absolute_address}}+(entry * {{get_memory_width_bytes(node)}}),
                                        width={{node.get_property('memwidth')}},
                                        accesswidth={{node.get_property('memwidth')}},
                                        data=value)

            read_callback_mock.assert_not_called()
            write_callback_mock.reset_mock()

            # check a multi-entry read, if the memory is small do the entire memory, however, if
            # it is large limit the number of entries to 10
            entries_to_test = {% if node.get_property('mementries') > 10 %}10{% else %}{{node.get_property('mementries')}}{% endif %}
            random_data = Array('{{get_array_typecode(node.get_property('memwidth'))}}',
                                [random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}) for x in range(entries_to_test)])
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(start_entry=0, data=random_data)

            for x in range(entries_to_test):
                self.assertEqual(write_callback_mock.call_args_list[x],
                                 call(addr={{node.absolute_address}} + (x * {{get_memory_width_bytes(node)}}),
                                      width={{node.get_property('memwidth')}},
                                      accesswidth={% if 'accesswidth' in node.list_properties() -%}{{node.get_property('accesswidth')}}{%- else -%}{{node.get_property('memwidth')}}{%- endif -%},
                                      data=random_data[x]))

            read_callback_mock.assert_not_called()
            write_callback_mock.reset_mock()

            {%- endif %}

        {%- endif %}
        {%- endfor %}
    {%- endif %}

    def test_adding_attributes(self):
        """
        Walk the address map and attempt to set a new value on each node

        The attribute name: cppkbrgmgeloagvfgjjeiiushygirh was randomly generated to be unlikely to
        every be a attribute name

        """
        {%- for node in top_node.descendants(unroll=True) %}
        with self.assertRaises(AttributeError):
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.cppkbrgmgeloagvfgjjeiiushygirh = 1
        {%- endfor %}

    {% macro check_readable_register_iterators(node) %}
        {# check the unrolled case first #}
        expected_readable_regs = [ {%- for child_node in node.children(unroll=True) -%}{%- if isinstance(child_node, systemrdlRegNode) %}{% if child_node.has_sw_readable %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{% endif %}{%- endfor %} ]
        readable_regs = []
        for readable_reg in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_readable_registers(unroll=True):
            readable_regs.append(readable_reg)
        self.assertCountEqual(expected_readable_regs, readable_regs)
        {# check the rolled case second #}
        expected_readable_regs = [ {%- for child_node in node.children(unroll=False) -%}{%- if isinstance(child_node, systemrdlRegNode) %}{% if child_node.has_sw_readable %}self.dut.{{'.'.join(child_node.get_path_segments(empty_array_suffix='')[1:])}}, {% endif %}{% endif %}{%- endfor %} ]
        readable_regs = []
        for readable_reg in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_readable_registers(unroll=False):
            readable_regs.append(readable_reg)
        self.assertCountEqual(expected_readable_regs, readable_regs)
    {% endmacro %}

    {% macro check_writable_register_iterators(node) %}
        {# check the unrolled case first #}
        expected_writable_regs = [ {%- for child_node in node.children(unroll=True) -%}{%- if isinstance(child_node, systemrdlRegNode) %}{% if child_node.has_sw_writable %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{% endif %}{%- endfor %} ]
        writable_regs = []
        for writable_reg in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_writable_registers(unroll=True):
            writable_regs.append(writable_reg)
        self.assertCountEqual(expected_writable_regs, writable_regs)
        {# check the rolled case second #}
        expected_writable_regs = [ {%- for child_node in node.children(unroll=False) -%}{%- if isinstance(child_node, systemrdlRegNode) %}{% if child_node.has_sw_writable %}self.dut.{{'.'.join(child_node.get_path_segments(empty_array_suffix='')[1:])}}, {% endif %}{% endif %}{%- endfor %} ]
        writable_regs = []
        for writable_reg in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_writable_registers(unroll=False):
            writable_regs.append(writable_reg)
        self.assertCountEqual(expected_writable_regs, writable_regs)
    {% endmacro %}

    {% macro check_section_iterators(node) %}
        {# check the unrolled case first #}
        expected_sections = [ {%- for child_node in node.children(unroll=True) -%}{%- if isinstance(child_node, (systemrdlAddrmapNode, systemrdlRegfileNode)) %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{%- endfor %} ]
        sections = []
        for section in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_sections(unroll=True):
            sections.append(section)
        self.assertCountEqual(expected_sections, sections)
        {# check the rolled case second #}
        expected_sections = [ {%- for child_node in node.children(unroll=False) -%}{%- if isinstance(child_node, (systemrdlAddrmapNode, systemrdlRegfileNode)) %}self.dut.{{'.'.join(child_node.get_path_segments(empty_array_suffix='')[1:])}}, {% endif %}{%- endfor %} ]
        sections = []
        for section in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_sections(unroll=False):
            sections.append(section)
        self.assertCountEqual(expected_sections, sections)
    {% endmacro %}

    {% macro check_memory_iterators(node) %}
        {# check the unrolled case first #}
        expected_memories = [ {%- for child_node in node.children(unroll=True) -%}{%- if isinstance(child_node, systemrdlMemNode) %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{%- endfor %} ]
        memories = []
        for memory in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_memories(unroll=True):
            memories.append(memory)
        self.assertCountEqual(expected_memories, memories)
        {# check the rolled case second #}
        expected_memories = [ {%- for child_node in node.children(unroll=False) -%}{%- if isinstance(child_node, systemrdlMemNode) %}self.dut.{{'.'.join(child_node.get_path_segments(empty_array_suffix='')[1:])}}, {% endif %}{%- endfor %} ]
        memories = []
        for memory in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.get_memories(unroll=False):
            memories.append(memory)
        self.assertCountEqual(expected_memories, memories)
    {% endmacro %}

    def test_traversal_iterators(self):
        """
        Walk the address map and check that the iterators for each node as as expected
        """
        {%- for node in top_node.descendants(unroll=True) %}
            {%- if isinstance(node, systemrdlRegNode) %}
                {# a register can only have fields beneath it #}
                {% if node.has_sw_writable %}
        expected_writable_fields = [ {%- for child_node in node.children(unroll=True) -%}{% if child_node.is_sw_writable %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{%- endfor %} ]
        writable_fields = []
        for writable_field in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.writable_fields:
            writable_fields.append(writable_field)
        self.assertCountEqual(expected_writable_fields, writable_fields)
                {% else %}
        # register should not have writable_fields attribute
        self.assertFalse(hasattr(self.dut.{{'.'.join(node.get_path_segments()[1:])}}, 'writable_fields'))
                {% endif %}
                {% if node.has_sw_readable %}
        expected_readable_fields = [ {%- for child_node in node.children(unroll=True) -%}{% if child_node.is_sw_readable %}self.dut.{{'.'.join(child_node.get_path_segments()[1:])}}, {% endif %}{%- endfor %} ]
        readable_fields = []
        for readable_field in self.dut.{{'.'.join(node.get_path_segments()[1:])}}.readable_fields:
            readable_fields.append(readable_field)
        self.assertCountEqual(expected_readable_fields, readable_fields)
                {% else %}
        # register should not have readable_fields attribute
        self.assertFalse(hasattr(self.dut.{{'.'.join(node.get_path_segments()[1:])}}, 'readable_fields'))
                {% endif %}
            {%- elif isinstance(node, systemrdlMemNode) %}
                {% if node.is_sw_writable %}
                    {{ check_writable_register_iterators(node) }}
                {% else %}
        self.assertFalse(hasattr(self.dut.{{'.'.join(node.get_path_segments()[1:])}}, 'get_writeable_registers'))
                {% endif %}
                {% if node.is_sw_readable %}
                    {{ check_readable_register_iterators(node) }}
                {% else %}
        self.assertFalse(hasattr(self.dut.{{'.'.join(node.get_path_segments()[1:])}}, 'get_readable_registers'))
                {% endif %}
            {%- elif isinstance(node, systemrdlAddrmapNode) %}
            {{ check_readable_register_iterators(node) }}
            {{ check_writable_register_iterators(node) }}
            {{ check_section_iterators(node) }}
            {{ check_memory_iterators(node) }}
            {%- elif isinstance(node, systemrdlRegfileNode) %}
            {{ check_readable_register_iterators(node) }}
            {{ check_writable_register_iterators(node) }}
            {{ check_section_iterators(node) }}
        self.assertFalse(hasattr(self.dut.{{'.'.join(node.get_path_segments()[1:])}}, 'get_memories'))
            {% endif %}
        {%- endfor %}

class {{top_node.type_name}}_TestCase_BlockAccess(unittest.TestCase):

    def setUp(self):
        self.dut = {{top_node.type_name}}_cls(CallbackSet(read_callback=read_callback,
                                                          write_callback=write_callback,
                                                          read_block_callback=read_block_callback,
                                                          write_block_callback=write_block_callback))

    {% if uses_memory %}
    def test_memory_read_and_write(self):
        """
        Walk the register map and check every register can be read and written to correctly
        """
        {%- for node in top_node.descendants(unroll=True) -%}
            {%- if isinstance(node, systemrdlMemNode) %}

        # test access operations (read and/or write) to register:
        # {{'.'.join(node.get_path_segments())}}
        with patch(__name__ + '.' + 'write_addr_space') as write_callback_mock,\
                            patch(__name__ + '.' + 'read_addr_space', return_value=1) as read_callback_mock, \
                            patch(__name__ + '.' + 'read_block_addr_space',
                                  return_value=Array('{{get_array_typecode(node.get_property('memwidth'))}}', [0])) as read_block_callback_mock , \
                            patch(__name__ + '.' + 'write_block_addr_space') as write_block_callback_mock:

            {% if node.is_sw_readable -%}
            # checks single unit accesses at the first entry, the last entry and a random entry in
            # in each case check a 0, max value and random value being read
            for entry in [0, random.randint(0,{{node.get_property('mementries')-1}}), {{node.get_property('mementries')-1}}]:
                for value in [0, random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}), {{get_memory_max_entry_value_hex_string(node)}}]:
                    read_block_callback_mock.reset_mock()
                    read_block_callback_mock.return_value = Array('{{get_array_typecode(node.get_property('memwidth'))}}',
                                                                  [value])
                    self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(start_entry=entry, number_entries=1),
                                                             Array('{{get_array_typecode(node.get_property('memwidth'))}}', [value]))
                    read_block_callback_mock.assert_called_once_with(
                                        addr={{node.absolute_address}}+(entry * {{get_memory_width_bytes(node)}}),
                                        width={{node.get_property('memwidth')}},
                                        accesswidth={{node.get_property('memwidth')}},
                                        length=1)

            # check a multi-entry read, if the memory is small do the entire memory, however, if
            # it is large limit the number of entries to 10
            entries_to_test = {% if node.get_property('mementries') > 10 %}10{% else %}{{node.get_property('mementries')}}{% endif %}
            random_data = Array('{{get_array_typecode(node.get_property('memwidth'))}}',
                                [random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}) for x in range(entries_to_test)])

            read_block_callback_mock.reset_mock()
            read_block_callback_mock.return_value = random_data

            self.assertEqual(self.dut.{{'.'.join(node.get_path_segments()[1:])}}.read(start_entry=0,
                                          number_entries=entries_to_test),
                                          random_data)

            write_callback_mock.assert_not_called()
            read_callback_mock.assert_not_called()
            write_block_callback_mock.assert_not_called()
            read_block_callback_mock.reset_mock()
            {% endif %}

            {% if node.is_sw_writable -%}
            # checks single unit accesses at the first entry, the last entry and a random entry in
            # in each case check a 0, max value and random value being read
            for entry in [0, random.randint(0,{{node.get_property('mementries')-1}}), {{node.get_property('mementries')-1}}]:
                for value in [0, random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}), {{get_memory_max_entry_value_hex_string(node)}}]:
                    write_block_callback_mock.reset_mock()
                    self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(start_entry=entry, data=Array('{{get_array_typecode(node.get_property('memwidth'))}}', [value]))
                    write_block_callback_mock.assert_called_once_with(
                                        addr={{node.absolute_address}}+(entry * {{get_memory_width_bytes(node)}}),
                                        width={{node.get_property('memwidth')}},
                                        accesswidth={{node.get_property('memwidth')}},
                                        data=Array('{{get_array_typecode(node.get_property('memwidth'))}}', [value]))

            read_callback_mock.assert_not_called()
            write_callback_mock.assert_not_called()
            read_block_callback_mock.assert_not_called()
            write_block_callback_mock.reset_mock()

            # check a multi-entry read, if the memory is small do the entire memory, however, if
            # it is large limit the number of entries to 10
            entries_to_test = {% if node.get_property('mementries') > 10 %}10{% else %}{{node.get_property('mementries')}}{% endif %}
            random_data = Array('{{get_array_typecode(node.get_property('memwidth'))}}',
                                [random.randint(0,{{get_memory_max_entry_value_hex_string(node)}}) for x in range(entries_to_test)])
            self.dut.{{'.'.join(node.get_path_segments()[1:])}}.write(start_entry=0, data=random_data)

            write_block_callback_mock.assert_called_once_with(
                                        addr={{node.absolute_address}},
                                        width={{node.get_property('memwidth')}},
                                        accesswidth={{node.get_property('memwidth')}},
                                        data=random_data)

            read_callback_mock.assert_not_called()
            write_callback_mock.assert_not_called()
            read_block_callback_mock.assert_not_called()
            write_block_callback_mock.reset_mock()

            {%- endif %}

        {%- endif %}
        {%- endfor %}
    {%- endif %}


if __name__ == '__main__':
    unittest.main()




