blob: 19ae5790c7becb5d915516ac1c44c093583b1ca0 [file] [log] [blame]
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for util.ir_data_utils."""
import dataclasses
import enum
import sys
from typing import Optional
import unittest
from compiler.util import expression_parser
from compiler.util import ir_data
from compiler.util import ir_data_fields
from compiler.util import ir_data_utils
class TestEnum(enum.Enum):
"""Used to test python Enum handling."""
UNKNOWN = 0
VALUE_1 = 1
VALUE_2 = 2
@dataclasses.dataclass
class Opaque(ir_data.Message):
"""Used for testing data field helpers."""
@dataclasses.dataclass
class ClassWithUnion(ir_data.Message):
"""Used for testing data field helpers."""
opaque: Optional[Opaque] = ir_data_fields.oneof_field("type")
integer: Optional[int] = ir_data_fields.oneof_field("type")
boolean: Optional[bool] = ir_data_fields.oneof_field("type")
enumeration: Optional[TestEnum] = ir_data_fields.oneof_field("type")
non_union_field: int = 0
@dataclasses.dataclass
class ClassWithTwoUnions(ir_data.Message):
"""Used for testing data field helpers."""
opaque: Optional[Opaque] = ir_data_fields.oneof_field("type_1")
integer: Optional[int] = ir_data_fields.oneof_field("type_1")
boolean: Optional[bool] = ir_data_fields.oneof_field("type_2")
enumeration: Optional[TestEnum] = ir_data_fields.oneof_field("type_2")
non_union_field: int = 0
seq_field: list[int] = ir_data_fields.list_field(int)
class IrDataUtilsTest(unittest.TestCase):
"""Tests for the miscellaneous utility functions in ir_data_utils.py."""
def test_field_specs(self):
"""Tests the `field_specs` method."""
fields = ir_data_utils.field_specs(ir_data.TypeDefinition)
self.assertIsNotNone(fields)
expected_fields = (
"external",
"enumeration",
"structure",
"name",
"attribute",
"documentation",
"subtype",
"addressable_unit",
"runtime_parameter",
"source_location",
)
self.assertEqual(len(fields), len(expected_fields))
field_names = fields.keys()
for k in expected_fields:
self.assertIn(k, field_names)
# Try a sequence
expected_field = ir_data_fields.make_field_spec(
"attribute", ir_data.Attribute, ir_data_fields.FieldContainer.LIST, None
)
self.assertEqual(fields["attribute"], expected_field)
# Try a scalar
expected_field = ir_data_fields.make_field_spec(
"addressable_unit",
ir_data.AddressableUnit,
ir_data_fields.FieldContainer.OPTIONAL,
None,
)
self.assertEqual(fields["addressable_unit"], expected_field)
# Try a IR data class
expected_field = ir_data_fields.make_field_spec(
"source_location",
ir_data.Location,
ir_data_fields.FieldContainer.OPTIONAL,
None,
)
self.assertEqual(fields["source_location"], expected_field)
# Try an oneof field
expected_field = ir_data_fields.make_field_spec(
"external",
ir_data.External,
ir_data_fields.FieldContainer.OPTIONAL,
oneof="type",
)
self.assertEqual(fields["external"], expected_field)
# Try non-optional scalar
fields = ir_data_utils.field_specs(ir_data.Position)
expected_field = ir_data_fields.make_field_spec(
"line", int, ir_data_fields.FieldContainer.NONE, None
)
self.assertEqual(fields["line"], expected_field)
fields = ir_data_utils.field_specs(ir_data.ArrayType)
expected_field = ir_data_fields.make_field_spec(
"base_type", ir_data.Type, ir_data_fields.FieldContainer.OPTIONAL, None
)
self.assertEqual(fields["base_type"], expected_field)
def test_is_sequence(self):
"""Tests for the `FieldSpec.is_sequence` helper."""
type_def = ir_data.TypeDefinition(
attribute=[
ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
),
]
)
fields = ir_data_utils.field_specs(ir_data.TypeDefinition)
# Test against a repeated field
self.assertTrue(fields["attribute"].is_sequence)
# Test against a nested IR data type
self.assertFalse(fields["name"].is_sequence)
# Test against a plain scalar type
fields = ir_data_utils.field_specs(type_def.attribute[0])
self.assertFalse(fields["is_default"].is_sequence)
def test_is_dataclass(self):
"""Tests FieldSpec.is_dataclass against ir_data."""
type_def = ir_data.TypeDefinition(
attribute=[
ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
),
]
)
fields = ir_data_utils.field_specs(ir_data.TypeDefinition)
# Test against a repeated field that holds IR data structs
self.assertTrue(fields["attribute"].is_dataclass)
# Test against a nested IR data type
self.assertTrue(fields["name"].is_dataclass)
# Test against a plain scalar type
fields = ir_data_utils.field_specs(type_def.attribute[0])
self.assertFalse(fields["is_default"].is_dataclass)
# Test against a repeated field that holds scalars
fields = ir_data_utils.field_specs(ir_data.Structure)
self.assertFalse(fields["fields_in_dependency_order"].is_dataclass)
def test_get_set_fields(self):
"""Tests that get set fields works."""
type_def = ir_data.TypeDefinition(
attribute=[
ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
),
]
)
set_fields = ir_data_utils.get_set_fields(type_def)
expected_fields = set(
["attribute", "documentation", "subtype", "runtime_parameter"]
)
self.assertEqual(len(set_fields), len(expected_fields))
found_fields = set()
for k, v in set_fields:
self.assertIn(k.name, expected_fields)
found_fields.add(k.name)
self.assertEqual(v, getattr(type_def, k.name))
self.assertSetEqual(found_fields, expected_fields)
def test_copy(self):
"""Tests the `copy` helper."""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
attribute_copy = ir_data_utils.copy(attribute)
# Should be equivalent
self.assertEqual(attribute, attribute_copy)
# But not the same instance
self.assertIsNot(attribute, attribute_copy)
# Let's do a sequence
type_def = ir_data.TypeDefinition(attribute=[attribute])
type_def_copy = ir_data_utils.copy(type_def)
# Should be equivalent
self.assertEqual(type_def, type_def_copy)
# But not the same instance
self.assertIsNot(type_def, type_def_copy)
self.assertIsNot(type_def.attribute, type_def_copy.attribute)
def test_update(self):
"""Tests the `update` helper."""
attribute_template = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
attribute = ir_data.Attribute(is_default=True)
ir_data_utils.update(attribute, attribute_template)
self.assertIsNotNone(attribute.value)
self.assertIsNot(attribute.value, attribute_template.value)
self.assertIsNotNone(attribute.name)
self.assertIsNot(attribute.name, attribute_template.name)
# Value not present in template should be untouched
self.assertTrue(attribute.is_default)
class IrDataBuilderTest(unittest.TestCase):
"""Tests for IrDataBuilder."""
def assertEmpty(self, obj):
self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
def assertLen(self, obj, length):
self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
def assertEmpty(self, obj):
self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
def assertLen(self, obj, length):
self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
def test_ir_data_builder(self):
"""Tests that basic builder chains work."""
# We start with an empty type
type_def = ir_data.TypeDefinition()
self.assertFalse(type_def.HasField("name"))
self.assertIsNone(type_def.name)
# Now setup a builder
builder = ir_data_utils.builder(type_def)
# Assign to a sub-child
builder.name.name = ir_data.Word(text="phil")
# Verify the wrapped struct is updated
self.assertIsNotNone(type_def.name)
self.assertIsNotNone(type_def.name.name)
self.assertIsNotNone(type_def.name.name.text)
self.assertEqual(type_def.name.name.text, "phil")
def test_ir_data_builder_bad_field(self):
"""Tests accessing an undefined field name fails."""
type_def = ir_data.TypeDefinition()
builder = ir_data_utils.builder(type_def)
self.assertRaises(AttributeError, lambda: builder.foo)
# Make sure it's not set on our IR data class either
self.assertRaises(AttributeError, getattr, type_def, "foo")
def test_ir_data_builder_sequence(self):
"""Tests that sequences are properly wrapped."""
# We start with an empty type
type_def = ir_data.TypeDefinition()
self.assertTrue(type_def.HasField("attribute"))
self.assertEmpty(type_def.attribute)
# Now setup a builder
builder = ir_data_utils.builder(type_def)
# Assign to a sequence
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
builder.attribute.append(attribute)
self.assertEqual(builder.attribute, [attribute])
self.assertTrue(type_def.HasField("attribute"))
self.assertLen(type_def.attribute, 1)
self.assertEqual(type_def.attribute[0], attribute)
# Lets make it longer and then try iterating
builder.attribute.append(attribute)
self.assertLen(type_def.attribute, 2)
for attr in builder.attribute:
# Modify the attributes
attr.name.text = "bob"
# Make sure we can build up auto-default entries from a sequence item
builder.attribute.append(ir_data.Attribute())
builder.attribute[-1].value.expression = ir_data.Expression()
builder.attribute[-1].name.text = "bob"
# Create an attribute to compare against
new_attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="bob"),
)
self.assertLen(type_def.attribute, 3)
for attr in type_def.attribute:
self.assertEqual(attr, new_attribute)
# Make sure the list type is a CopyValuesList
self.assertIsInstance(
type_def.attribute,
ir_data_fields.CopyValuesList,
f"Instance is: {type(type_def.attribute)}",
)
def test_copy_from(self) -> None:
"""Tests that `CopyFrom` works."""
location = ir_data.Location(
start=ir_data.Position(line=1, column=1),
end=ir_data.Position(line=1, column=2),
)
expression_ir = ir_data.Expression(source_location=location)
template: ir_data.Expression = expression_parser.parse("x + y")
expression = ir_data_utils.builder(expression_ir)
expression.CopyFrom(template)
self.assertIsNotNone(expression_ir.function)
self.assertIsInstance(expression.function, ir_data_utils._IrDataBuilder)
self.assertIsInstance(
expression.function.args, ir_data_utils._IrDataSequenceBuilder
)
self.assertTrue(expression_ir.function.args)
def test_copy_from_list(self):
specs = ir_data_utils.field_specs(ir_data.Function)
args_spec = specs["args"]
self.assertTrue(args_spec.is_dataclass)
template: ir_data.Expression = expression_parser.parse("x + y")
self.assertIsNotNone(template)
self.assertIsInstance(template, ir_data.Expression)
self.assertIsInstance(template.function, ir_data.Function)
self.assertIsInstance(template.function.args, ir_data_fields.CopyValuesList)
location = ir_data.Location(
start=ir_data.Position(line=1, column=1),
end=ir_data.Position(line=1, column=2),
)
expression_ir = ir_data.Expression(source_location=location)
self.assertIsInstance(expression_ir, ir_data.Expression)
self.assertIsNone(expression_ir.function)
expression_builder = ir_data_utils.builder(expression_ir)
self.assertIsInstance(expression_builder, ir_data_utils._IrDataBuilder)
expression_builder.CopyFrom(template)
self.assertIsNotNone(expression_ir.function)
self.assertIsInstance(expression_ir.function, ir_data.Function)
self.assertIsNotNone(expression_ir.function.args)
self.assertIsInstance(
expression_ir.function.args, ir_data_fields.CopyValuesList
)
self.assertIsInstance(expression_builder, ir_data_utils._IrDataBuilder)
self.assertIsInstance(expression_builder.function, ir_data_utils._IrDataBuilder)
self.assertIsInstance(
expression_builder.function.args, ir_data_utils._IrDataSequenceBuilder
)
def test_ir_data_builder_sequence_scalar(self):
"""Tests that sequences of scalars function properly."""
# We start with an empty type
structure = ir_data.Structure()
# Now setup a builder
builder = ir_data_utils.builder(structure)
# Assign to a scalar sequence
builder.fields_in_dependency_order.append(12)
builder.fields_in_dependency_order.append(11)
self.assertTrue(structure.HasField("fields_in_dependency_order"))
self.assertLen(structure.fields_in_dependency_order, 2)
self.assertEqual(structure.fields_in_dependency_order[0], 12)
self.assertEqual(structure.fields_in_dependency_order[1], 11)
self.assertEqual(builder.fields_in_dependency_order, [12, 11])
new_structure = ir_data.Structure(fields_in_dependency_order=[12, 11])
self.assertEqual(structure, new_structure)
def test_ir_data_builder_oneof(self):
value = ir_data.AttributeValue(
expression=ir_data.Expression(boolean_constant=ir_data.BooleanConstant())
)
builder = ir_data_utils.builder(value)
self.assertTrue(builder.HasField("expression"))
self.assertFalse(builder.expression.boolean_constant.value)
builder.expression.boolean_constant.value = True
self.assertTrue(builder.expression.boolean_constant.value)
self.assertTrue(value.expression.boolean_constant.value)
bool_constant = value.expression.boolean_constant
self.assertIsInstance(bool_constant, ir_data.BooleanConstant)
class IrDataSerializerTest(unittest.TestCase):
"""Tests for IrDataSerializer."""
def test_ir_data_serializer_to_dict(self):
"""Tests serialization with `IrDataSerializer.to_dict` with default settings."""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
serializer = ir_data_utils.IrDataSerializer(attribute)
raw_dict = serializer.to_dict()
expected = {
"name": {"text": "phil", "source_location": None},
"value": {
"expression": {
"constant": None,
"constant_reference": None,
"function": None,
"field_reference": None,
"boolean_constant": None,
"builtin_reference": None,
"type": None,
"source_location": None,
},
"string_constant": None,
"source_location": None,
},
"back_end": None,
"is_default": None,
"source_location": None,
}
self.assertDictEqual(raw_dict, expected)
def test_ir_data_serializer_to_dict_exclude_none(self):
""".Tests serialization with `IrDataSerializer.to_dict` when excluding None values"""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
serializer = ir_data_utils.IrDataSerializer(attribute)
raw_dict = serializer.to_dict(exclude_none=True)
expected = {"name": {"text": "phil"}, "value": {"expression": {}}}
self.assertDictEqual(raw_dict, expected)
def test_ir_data_serializer_to_dict_enum(self):
"""Tests that serialization of `enum.Enum` values works properly."""
type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
serializer = ir_data_utils.IrDataSerializer(type_def)
raw_dict = serializer.to_dict(exclude_none=True)
expected = {"addressable_unit": ir_data.AddressableUnit.BYTE}
self.assertDictEqual(raw_dict, expected)
def test_ir_data_serializer_from_dict(self):
"""Tests deserializing IR data from a serialized dict."""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
serializer = ir_data_utils.IrDataSerializer(attribute)
raw_dict = serializer.to_dict(exclude_none=False)
new_attribute = serializer.from_dict(ir_data.Attribute, raw_dict)
self.assertEqual(attribute, new_attribute)
def test_ir_data_serializer_from_dict_enum(self):
"""Tests that deserializing `enum.Enum` values works properly."""
type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
serializer = ir_data_utils.IrDataSerializer(type_def)
raw_dict = serializer.to_dict(exclude_none=False)
new_type_def = serializer.from_dict(ir_data.TypeDefinition, raw_dict)
self.assertEqual(type_def, new_type_def)
def test_ir_data_serializer_from_dict_enum_is_str(self):
"""Tests that deserializing `enum.Enum` values works properly when string constant is used."""
type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
raw_dict = {"addressable_unit": "BYTE"}
serializer = ir_data_utils.IrDataSerializer(type_def)
new_type_def = serializer.from_dict(ir_data.TypeDefinition, raw_dict)
self.assertEqual(type_def, new_type_def)
def test_ir_data_serializer_from_dict_exclude_none(self):
"""Tests that deserializing from a dict that excluded None values works properly."""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
serializer = ir_data_utils.IrDataSerializer(attribute)
raw_dict = serializer.to_dict(exclude_none=True)
new_attribute = ir_data_utils.IrDataSerializer.from_dict(
ir_data.Attribute, raw_dict
)
self.assertEqual(attribute, new_attribute)
def test_from_dict_list(self):
function_args = [
{
"constant": {
"value": "0",
"source_location": {
"start": {"line": 421, "column": 3},
"end": {"line": 421, "column": 4},
"is_synthetic": False,
},
},
"type": {
"integer": {
"modulus": "infinity",
"modular_value": "0",
"minimum_value": "0",
"maximum_value": "0",
}
},
"source_location": {
"start": {"line": 421, "column": 3},
"end": {"line": 421, "column": 4},
"is_synthetic": False,
},
},
{
"constant": {
"value": "1",
"source_location": {
"start": {"line": 421, "column": 11},
"end": {"line": 421, "column": 12},
"is_synthetic": False,
},
},
"type": {
"integer": {
"modulus": "infinity",
"modular_value": "1",
"minimum_value": "1",
"maximum_value": "1",
}
},
"source_location": {
"start": {"line": 421, "column": 11},
"end": {"line": 421, "column": 12},
"is_synthetic": False,
},
},
]
function_data = {"args": function_args}
func = ir_data_utils.IrDataSerializer.from_dict(ir_data.Function, function_data)
self.assertIsNotNone(func)
def test_ir_data_serializer_copy_from_dict(self):
"""Tests that updating an IR data struct from a dict works properly."""
attribute = ir_data.Attribute(
value=ir_data.AttributeValue(expression=ir_data.Expression()),
name=ir_data.Word(text="phil"),
)
serializer = ir_data_utils.IrDataSerializer(attribute)
raw_dict = serializer.to_dict(exclude_none=False)
new_attribute = ir_data.Attribute()
new_serializer = ir_data_utils.IrDataSerializer(new_attribute)
new_serializer.copy_from_dict(raw_dict)
self.assertEqual(attribute, new_attribute)
class ReadOnlyFieldCheckerTest(unittest.TestCase):
"""Tests the ReadOnlyFieldChecker."""
def test_basic_wrapper(self):
"""Tests basic field checker actions."""
union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
field_checker = ir_data_utils.reader(union)
# All accesses should return a wrapper object
self.assertIsNotNone(field_checker.opaque)
self.assertIsNotNone(field_checker.integer)
self.assertIsNotNone(field_checker.boolean)
self.assertIsNotNone(field_checker.enumeration)
self.assertIsNotNone(field_checker.non_union_field)
# Scalar field should pass through
self.assertEqual(field_checker.non_union_field, 10)
# Make sure HasField works
self.assertTrue(field_checker.HasField("opaque"))
self.assertFalse(field_checker.HasField("integer"))
self.assertTrue(field_checker.HasField("boolean"))
self.assertFalse(field_checker.HasField("enumeration"))
self.assertTrue(field_checker.HasField("non_union_field"))
def test_construct_from_field_checker(self):
"""Tests that constructing from another field checker works."""
union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
field_checker_orig = ir_data_utils.reader(union)
field_checker = ir_data_utils.reader(field_checker_orig)
self.assertIsNotNone(field_checker)
self.assertEqual(field_checker.ir_or_spec, union)
# All accesses should return a wrapper object
self.assertIsNotNone(field_checker.opaque)
self.assertIsNotNone(field_checker.integer)
self.assertIsNotNone(field_checker.boolean)
self.assertIsNotNone(field_checker.enumeration)
self.assertIsNotNone(field_checker.non_union_field)
# Scalar field should pass through
self.assertEqual(field_checker.non_union_field, 10)
# Make sure HasField works
self.assertTrue(field_checker.HasField("opaque"))
self.assertFalse(field_checker.HasField("integer"))
self.assertTrue(field_checker.HasField("boolean"))
self.assertFalse(field_checker.HasField("enumeration"))
self.assertTrue(field_checker.HasField("non_union_field"))
def test_read_only(self) -> None:
"""Tests that the read only wrapper really is read only."""
union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
field_checker = ir_data_utils.reader(union)
def set_field():
field_checker.opaque = None
self.assertRaises(AttributeError, set_field)
ir_data_fields.cache_message_specs(
sys.modules[ReadOnlyFieldCheckerTest.__module__], ir_data.Message
)
if __name__ == "__main__":
unittest.main()