# Copyright 2023 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 attribute_checker.py.""" import unittest from compiler.back_end.cpp import header_generator from compiler.front_end import glue from compiler.util import error from compiler.util import ir_data from compiler.util import ir_data_utils from compiler.util import test_util def _make_ir_from_emb(emb_text, name="m.emb"): ir, unused_debug_info, errors = glue.parse_emboss_file( name, test_util.dict_file_reader({name: emb_text})) assert not errors return ir class NormalizeIrTest(unittest.TestCase): def test_accepts_string_attribute(self): ir = _make_ir_from_emb('[(cpp) namespace: "foo"]\n') self.assertEqual([], header_generator.generate_header(ir)[1]) def test_rejects_wrong_type_for_string_attribute(self): ir = _make_ir_from_emb("[(cpp) namespace: 9]\n") attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.value.source_location, "Attribute '(cpp) namespace' must have a string value.") ]], header_generator.generate_header(ir)[1]) def test_rejects_emboss_internal_attribute_with_back_end_specifier(self): ir = _make_ir_from_emb('[(cpp) byte_order: "LittleEndian"]\n') attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.name.source_location, "Unknown attribute '(cpp) byte_order' on module 'm.emb'.") ]], header_generator.generate_header(ir)[1]) def test_accepts_enum_case(self): mod_ir = _make_ir_from_emb('[(cpp) $default enum_case: "kCamelCase"]') self.assertEqual([], header_generator.generate_header(mod_ir)[1]) enum_ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') self.assertEqual([], header_generator.generate_header(enum_ir)[1]) enum_value_ir = _make_ir_from_emb('enum Foo:\n' ' BAR = 1 [(cpp) enum_case: "kCamelCase"]\n' ' BAZ = 2\n' ' [(cpp) enum_case: "kCamelCase"]\n') self.assertEqual([], header_generator.generate_header(enum_value_ir)[1]) enum_in_struct_ir = _make_ir_from_emb('struct Outer:\n' ' [(cpp) $default enum_case: "kCamelCase"]\n' ' enum Inner:\n' ' BAR = 1\n' ' BAZ = 2\n') self.assertEqual([], header_generator.generate_header(enum_in_struct_ir)[1]) enum_in_bits_ir = _make_ir_from_emb('bits Outer:\n' ' [(cpp) $default enum_case: "kCamelCase"]\n' ' enum Inner:\n' ' BAR = 1\n' ' BAZ = 2\n') self.assertEqual([], header_generator.generate_header(enum_in_bits_ir)[1]) enum_ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE,"]\n' ' BAR = 1\n' ' BAZ = 2\n') self.assertEqual([], header_generator.generate_header(enum_ir)[1]) enum_ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE ,kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') self.assertEqual([], header_generator.generate_header(enum_ir)[1]) def test_rejects_bad_enum_case_at_start(self): ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHORTY_CASE, kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') attr = ir.module[0].type[0].attribute[0] bad_case_source_location = ir_data.Location() bad_case_source_location = ir_data_utils.builder(bad_case_source_location) bad_case_source_location.CopyFrom(attr.value.source_location) # Location of SHORTY_CASE in the attribute line. bad_case_source_location.start.column = 30 bad_case_source_location.end.column = 41 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Unsupported enum case "SHORTY_CASE", ' 'supported cases are: SHOUTY_CASE, kCamelCase.') ]], header_generator.generate_header(ir)[1]) def test_rejects_bad_enum_case_in_middle(self): ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE, bad_CASE, kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') attr = ir.module[0].type[0].attribute[0] bad_case_source_location = ir_data.Location() bad_case_source_location = ir_data_utils.builder(bad_case_source_location) bad_case_source_location.CopyFrom(attr.value.source_location) # Location of bad_CASE in the attribute line. bad_case_source_location.start.column = 43 bad_case_source_location.end.column = 51 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Unsupported enum case "bad_CASE", ' 'supported cases are: SHOUTY_CASE, kCamelCase.') ]], header_generator.generate_header(ir)[1]) def test_rejects_bad_enum_case_at_end(self): ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase, BAD_case"]\n' ' BAR = 1\n' ' BAZ = 2\n') attr = ir.module[0].type[0].attribute[0] bad_case_source_location = ir_data.Location() bad_case_source_location = ir_data_utils.builder(bad_case_source_location) bad_case_source_location.CopyFrom(attr.value.source_location) # Location of BAD_case in the attribute line. bad_case_source_location.start.column = 55 bad_case_source_location.end.column = 63 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Unsupported enum case "BAD_case", ' 'supported cases are: SHOUTY_CASE, kCamelCase.') ]], header_generator.generate_header(ir)[1]) def test_rejects_duplicate_enum_case(self): ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE, SHOUTY_CASE"]\n' ' BAR = 1\n' ' BAZ = 2\n') attr = ir.module[0].type[0].attribute[0] bad_case_source_location = ir_data.Location() bad_case_source_location = ir_data_utils.builder(bad_case_source_location) bad_case_source_location.CopyFrom(attr.value.source_location) # Location of the second SHOUTY_CASE in the attribute line. bad_case_source_location.start.column = 43 bad_case_source_location.end.column = 54 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Duplicate enum case "SHOUTY_CASE".') ]], header_generator.generate_header(ir)[1]) def test_rejects_empty_enum_case(self): # Double comma ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE,, kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') attr = ir.module[0].type[0].attribute[0] bad_case_source_location = ir_data.Location() bad_case_source_location = ir_data_utils.builder(bad_case_source_location) bad_case_source_location.CopyFrom(attr.value.source_location) # Location of excess comma. bad_case_source_location.start.column = 42 bad_case_source_location.end.column = 42 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # Leading comma ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: ", SHOUTY_CASE, kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 30 bad_case_source_location.end.column = 30 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # Excess trailing comma ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE, kCamelCase,,"]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 54 bad_case_source_location.end.column = 54 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # Whitespace enum case ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: "SHOUTY_CASE, , kCamelCase"]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 45 bad_case_source_location.end.column = 45 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # Empty enum_case string ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: ""]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 30 bad_case_source_location.end.column = 30 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # Whitespace enum_case string ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: " "]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 35 bad_case_source_location.end.column = 35 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) # One-character whitespace enum_case string ir = _make_ir_from_emb('enum Foo:\n' ' [(cpp) $default enum_case: " "]\n' ' BAR = 1\n' ' BAZ = 2\n') bad_case_source_location.start.column = 31 bad_case_source_location.end.column = 31 self.assertEqual([[ error.error("m.emb", bad_case_source_location, 'Empty enum case (or excess comma).') ]], header_generator.generate_header(ir)[1]) def test_accepts_namespace(self): for test in [ '[(cpp) namespace: "basic"]\n', '[(cpp) namespace: "multiple::components"]\n', '[(cpp) namespace: "::absolute"]\n', '[(cpp) namespace: "::fully::qualified"]\n', '[(cpp) namespace: "CAN::Be::cAPITAL"]\n', '[(cpp) namespace: "trailingNumbers54321"]\n', '[(cpp) namespace: "containing4321numbers"]\n', '[(cpp) namespace: "can_have_underscores"]\n', '[(cpp) namespace: "_initial_underscore"]\n', '[(cpp) namespace: "_initial::_underscore"]\n', '[(cpp) namespace: "::_initial::_underscore"]\n', '[(cpp) namespace: "trailing_underscore_"]\n', '[(cpp) namespace: "trailing_::underscore_"]\n', '[(cpp) namespace: "::trailing_::underscore_"]\n', '[(cpp) namespace: " spaces "]\n', '[(cpp) namespace: "with :: spaces"]\n', '[(cpp) namespace: " ::fully:: qualified :: with::spaces"]\n', ]: ir = _make_ir_from_emb(test) self.assertEqual([], header_generator.generate_header(ir)[1]) def test_rejects_non_namespace_strings(self): for test in [ '[(cpp) namespace: "5th::avenue"]\n', '[(cpp) namespace: "can\'t::have::apostrophe"]\n', '[(cpp) namespace: "cannot-have-dash"]\n', '[(cpp) namespace: "no/slashes"]\n', '[(cpp) namespace: "no\\\\slashes"]\n', '[(cpp) namespace: "apostrophes*are*rejected"]\n', '[(cpp) namespace: "avoid.dot"]\n', '[(cpp) namespace: "does5+5"]\n', '[(cpp) namespace: "=10"]\n', '[(cpp) namespace: "?"]\n', '[(cpp) namespace: "reject::spaces in::components"]\n', '[(cpp) namespace: "totally::valid::but::extra +"]\n', '[(cpp) namespace: "totally::valid::but::extra ::?"]\n', '[(cpp) namespace: "< totally::valid::but::extra"]\n', '[(cpp) namespace: "< ::totally::valid::but::extra"]\n', '[(cpp) namespace: "::totally::valid::but::extra::"]\n', '[(cpp) namespace: ":::extra::colon"]\n', '[(cpp) namespace: "::extra:::colon"]\n', ]: ir = _make_ir_from_emb(test) attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.value.source_location, 'Invalid namespace, must be a valid C++ namespace, such ' 'as "abc", "abc::def", or "::abc::def::ghi" (ISO/IEC ' '14882:2017 enclosing-namespace-specifier).') ]], header_generator.generate_header(ir)[1]) def test_rejects_empty_namespace(self): for test in [ '[(cpp) namespace: ""]\n', '[(cpp) namespace: " "]\n', '[(cpp) namespace: " "]\n', ]: ir = _make_ir_from_emb(test) attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.value.source_location, 'Empty namespace value is not allowed.') ]], header_generator.generate_header(ir)[1]) def test_rejects_global_namespace(self): for test in [ '[(cpp) namespace: "::"]\n', '[(cpp) namespace: " ::"]\n', '[(cpp) namespace: ":: "]\n', '[(cpp) namespace: " :: "]\n', ]: ir = _make_ir_from_emb(test) attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.value.source_location, 'Global namespace is not allowed.') ]], header_generator.generate_header(ir)[1]) def test_rejects_reserved_namespace(self): for test, expected in [ # Only component ('[(cpp) namespace: "class"]\n', 'class'), # Only component, fully qualified name ('[(cpp) namespace: "::const"]\n', 'const'), # First component ('[(cpp) namespace: "if::valid"]\n', 'if'), # First component, fully qualified name ('[(cpp) namespace: "::auto::pilot"]\n', 'auto'), # Last component ('[(cpp) namespace: "make::do"]\n', 'do'), # Middle component ('[(cpp) namespace: "our::new::product"]\n', 'new'), ]: ir = _make_ir_from_emb(test) attr = ir.module[0].attribute[0] self.assertEqual([[ error.error("m.emb", attr.value.source_location, f'Reserved word "{expected}" is not allowed ' f'as a namespace component.')]], header_generator.generate_header(ir)[1]) if __name__ == "__main__": unittest.main()