class ParserTests(unittest.TestCase): def setUp(self) -> None: try: self.parser = EtchParser(CONTRACT_TEXT) self.assertIsNotNone(self.parser._parsed_tree, "Parsed tree missing when code passed") except ParseError as e: self.fail("Failed to parse contract text: \n" + str(e)) def test_grammar(self): """Check that grammer compiles""" # TODO: Grammar is loaded from a file, which may impact unit test performance try: parser = EtchParser() self.assertIsNone(parser._parsed_tree, "Parsed tree present when no code passed") except GrammarError as e: self.fail("Etch grammar failed to load with: \n" + str(e)) def test_get_functions(self): """Check that functions properly identified""" functions = self.parser.get_functions() # Check all functions found function_dict = {f.name: f for f in functions} self.assertTrue( all(n in function_dict.keys() for n in ['setup', 'transfer', 'balance', 'sub'])) # Check transfer parsed self.assertEqual(function_dict['transfer'].annotation, 'action') self.assertEqual(function_dict['transfer'].lines, (11, 24)) self.assertIsNotNone(function_dict['transfer'].code_block) # Check return value correctly parsed self.assertEqual(function_dict['balance'].return_type, 'UInt64') # Check parameter block correctly parsed self.assertEqual(len(function_dict['setup'].parameters), 1) self.assertIsNone(function_dict['setup'].parameters[0].value) self.assertEqual(function_dict['setup'].parameters[0].name, 'owner') self.assertEqual(function_dict['setup'].parameters[0].ptype, 'Address') def test_entry_points(self): entry_points = self.parser.entry_points() self.assertIn('init', entry_points) self.assertIn('action', entry_points) self.assertIn('query', entry_points) self.assertEqual(entry_points['init'], ['setup']) self.assertEqual(entry_points['action'], ['transfer']) self.assertEqual(entry_points['query'], ['balance']) def test_globals_declared(self): glob_decl = self.parser.globals_declared() self.assertEqual(set(glob_decl.keys()), {'balance_state', 'owner_name'}) self.assertEqual(glob_decl['balance_state'].name, 'balance_state') self.assertEqual(glob_decl['balance_state'].gtype, 'UInt64') self.assertEqual(glob_decl['balance_state'].is_sharded, True) self.assertEqual(glob_decl['owner_name'].name, 'owner_name') self.assertEqual(glob_decl['owner_name'].gtype, 'String') self.assertEqual(glob_decl['owner_name'].is_sharded, False) def test_globals_used(self): """Test accurate parsing of globals used in entry points""" # Test accurate parsing of declared globals glob_used = self.parser.globals_used('setup', ['abc']) self.assertEqual(len(glob_used), 1) self.assertEqual(len(glob_used[0]), 2) self.assertEqual(glob_used[0][0], 'balance_state') self.assertEqual(glob_used[0][1].value, 'abc') self.assertEqual(glob_used[0][1].name, 'owner') def test_global_addresses(self): """Test accurate parsing of globals used in entry points""" with patch('logging.warning') as mock_warn: glob_addresses = self.parser.used_globals_to_addresses( 'transfer', ['abc', 'def', 100]) self.assertEqual(mock_warn.call_count, 1) self.assertEqual(len(glob_addresses), 5) # Unsharded use statement self.assertEqual(glob_addresses[0], 'owner_name') # Sharded use statements self.assertEqual(glob_addresses[1], 'balance_state.abc') # Parameter self.assertEqual(glob_addresses[2], 'balance_state.def') # Parameter self.assertEqual(glob_addresses[3], 'balance_state.constant_string') # String constant self.assertEqual(glob_addresses[4], 'balance_state.prefix.def') # String concatenation def test_scope(self): """Tests which instructions are allowed at each scope""" # Regular instructions are not allowed at global scope with patch('logging.warning') as mock_warn: self.assertFalse(self.parser.parse("var a = 5;")) self.assertEqual(mock_warn.call_count, 2) # Allowed at global scope try: # Persistent global declaration self.assertTrue( self.parser.parse("persistent sharded balance_state : UInt64;") is not False) self.assertTrue( self.parser.parse("persistent owner : String;") is not False) # Functions self.assertTrue( self.parser.parse("""function a(owner : String) var b = owner; endfunction""") is not False) # Annotated functions self.assertTrue( self.parser.parse("""@action function a(owner : String) var b = owner; endfunction""") is not False) # Comments self.assertTrue(self.parser.parse("// A comment") is not False) except UnexpectedCharacters as e: self.fail("Etch parsing of top level statement failed: \n" + str(e)) def test_builtins(self): """Tests for correct parsing of all supported builtin types""" parser = EtchParser() int_types = ['Int' + str(x) for x in [8, 16, 32, 64, 256]] uint_types = ['UInt' + str(x) for x in [8, 16, 32, 64, 256]] float_types = ['Float' + str(x) for x in [32, 64]] fixed_types = ['Fixed' + str(x) for x in [32, 64]] # Test declaration of numerical types for t in int_types + uint_types: tree = self.parser.parse( FUNCTION_BLOCK.format("var a : {};".format(t))) tree = next(tree.find_data("instruction")) self.assertEqual(tree.children[0].data, 'declaration') self.assertEqual(tree.children[0].children[1].type, 'BASIC_TYPE') self.assertEqual(tree.children[0].children[1].value, t) for t in float_types: tree = self.parser.parse( FUNCTION_BLOCK.format("var a : {};".format(t))) tree = next(tree.find_data("instruction")) self.assertEqual(tree.children[0].data, 'declaration') self.assertEqual(tree.children[0].children[1].type, 'FLOAT_TYPE') self.assertEqual(tree.children[0].children[1].value, t) for t in fixed_types: tree = self.parser.parse( FUNCTION_BLOCK.format("var a : {};".format(t))) tree = next(tree.find_data("instruction")) self.assertEqual(tree.children[0].data, 'declaration') self.assertEqual(tree.children[0].children[1].type, 'FIXED_TYPE') self.assertEqual(tree.children[0].children[1].value, t) # Test declaration of other types other_types = ['Boolean', 'String'] for t in other_types: tree = self.parser.parse( FUNCTION_BLOCK.format("var a : {};".format(t))) tree = next(tree.find_data("instruction")) self.assertEqual(tree.children[0].data, 'declaration') self.assertEqual(tree.children[0].children[1].type, 'NAME') self.assertEqual(tree.children[0].children[1].value, t) # TODO: Test these in a meaningful way, beyond simply that they parse # Test declaration of array tree = self.parser.parse( FUNCTION_BLOCK.format("var myArray = Array<Int32>(5);")) # Test assignment to array tree = self.parser.parse(FUNCTION_BLOCK.format("myArray[0] = 5;")) # Test assignment from array tree = self.parser.parse(FUNCTION_BLOCK.format("b = myArray[0];")) tree = self.parser.parse(FUNCTION_BLOCK.format("var b = myArray[0];")) # As above, for map type tree = self.parser.parse( FUNCTION_BLOCK.format("var myArray = Map<String, Int32>(5);")) # Test assignment to array tree = self.parser.parse(FUNCTION_BLOCK.format("myArray['test'] = 5;")) # Test assignment from array tree = self.parser.parse(FUNCTION_BLOCK.format("b = myArray['test'];")) tree = self.parser.parse( FUNCTION_BLOCK.format("var b = myArray['test'];")) def test_instantiation(self): """Tests for correct parsing of valid variable instantiation""" # Check that the following parse without error tree = self.parser.parse( FUNCTION_BLOCK.format("var b = get();")) # Untyped instantiation tree = self.parser.parse( FUNCTION_BLOCK.format( "var b : UInt64 = get();")) # Typed instantiation def test_template(self): """Tests for correct parsing of template variables""" tree = self.parser.parse(FUNCTION_BLOCK.format("a = State<UInt64>();")) tree = self.parser.parse( FUNCTION_BLOCK.format("a = State<UInt64, UInt64>();")) # Test function parsing with template parameters tree = self.parser.parse( """function a(b : Array<StructuredData>) : Int32 var c : State<UInt32>; endfunction """) functions = self.parser.get_functions() # Check that argument list correctly parsed self.assertEqual(functions[0].parameters[0].name, 'b') self.assertEqual(functions[0].parameters[0].ptype, 'Array<StructuredData>') def test_functions(self): """Tests correct detection of non-entry-point functions""" self.assertEqual(self.parser.subfunctions(), ['sub']) def test_class_function(self): """Tests correct ingestion of functions""" # Test minimal function tree = self.parser.parse("""function init() endfunction""") func = Function.from_tree(next(tree.find_data('function'))) self.assertIsNone(func.annotation) self.assertIsNone(func.code_block) self.assertIsNone(func.return_type) self.assertEqual(func.name, 'init') self.assertEqual(func.parameters, []) # Test Function parsing from_tree tree = self.parser.parse(""" @action function a(b : UInt64) : String return b; endfunction """) func = Function.from_tree(next(tree.find_data('annotation'))) self.assertEqual(func.name, 'a') self.assertEqual(func.return_type, 'String') self.assertEqual(func.annotation, 'action') self.assertEqual(func.parameters[0].name, 'b') self.assertEqual(func.parameters[0].ptype, 'UInt64') # Test all_from_tree tree = self.parser.parse(""" @action function a(b : UInt64) : String return 'test'; endfunction function c(d: UInt64): String return 'test2'; endfunction """) funcs = Function.all_from_tree(tree) self.assertEqual(funcs[0].name, 'a') self.assertEqual(funcs[0].return_type, 'String') self.assertEqual(funcs[0].annotation, 'action') self.assertEqual(funcs[0].parameters[0].name, 'b') self.assertEqual(funcs[0].parameters[0].ptype, 'UInt64') self.assertEqual(funcs[1].name, 'c') self.assertEqual(funcs[1].return_type, 'String') self.assertIsNone(funcs[1].annotation) self.assertEqual(funcs[1].parameters[0].name, 'd') self.assertEqual(funcs[1].parameters[0].ptype, 'UInt64') def test_nested_function_call(self): """Check that nested function calls are supported by parser""" try: tree = self.parser.parse(NESTED_FUNCTION) self.assertTrue(tree is not False) except: self.fail("Parsing of dot nested function calls failed") def test_expressions(self): """Check that common expressions parse correctly""" # Instantiation tree = self.parser.parse(FUNCTION_BLOCK.format("var a = 1i32;")) # Binary operation tree = self.parser.parse(FUNCTION_BLOCK.format("var a = 1i32 + 2i32;")) # Pre-unary operation tree = self.parser.parse(FUNCTION_BLOCK.format("var a = - 2i32;")) # Post-unary operation tree = self.parser.parse(FUNCTION_BLOCK.format("var a = 2i32++;")) # Comparison operation tree = self.parser.parse( FUNCTION_BLOCK.format("var a = 2i32 == 3i32;")) # Type cast tree = self.parser.parse(FUNCTION_BLOCK.format("var a = Int64(3i32);")) def test_assignments(self): """Check successful parsing of assignment operators""" FB_WITH_DECLARATION = FUNCTION_BLOCK.format("var a : Int64; {}") tree = self.parser.parse(FB_WITH_DECLARATION.format("a += 5;")) tree = self.parser.parse(FB_WITH_DECLARATION.format("a -= 5;")) tree = self.parser.parse(FB_WITH_DECLARATION.format("a *= 5;")) tree = self.parser.parse(FB_WITH_DECLARATION.format("a /= 5;")) tree = self.parser.parse(FB_WITH_DECLARATION.format("a %= 5;")) def test_assert_statement(self): """Check boolean expressions valid in any context""" tree = self.parser.parse( FUNCTION_BLOCK.format("assert(a >= 0 && a <= 15);")) def test_template_global(self): """Checks correct parsing of globals with template types""" self.parser.parse(TEMPLATE_GLOBAL) # Function A contains a non-sharded global of type Array<Address> addresses = self.parser.used_globals_to_addresses('A', []) self.assertEqual(addresses, ['users']) # Function B contains a sharded global of type Array<Address> addresses = self.parser.used_globals_to_addresses('B', []) self.assertEqual(addresses, ['sharded_users.abc']) def test_if_blocks(self): """Checks correct parsing of if blocks""" # Partial contract text with function block and variable instantiation PARTIAL_BLOCK = FUNCTION_BLOCK.format(""" var a: Int64 = 5; var b: Int64 = 0; {}""") # Simple if block tree = self.parser.parse( PARTIAL_BLOCK.format(""" if (a > 5) b = 6; endif""")) self.assertTrue(tree is not False) # If-else block tree = self.parser.parse( PARTIAL_BLOCK.format(""" if (a > 5) b = 6; else b = 7; endif""")) self.assertTrue(tree is not False) # Nested if-else-if block tree = self.parser.parse( PARTIAL_BLOCK.format(""" if (a > 5) b = 6; else if (a < 5) b = 4; endif endif""")) self.assertTrue(tree is not False) # If-elseif block tree = self.parser.parse( PARTIAL_BLOCK.format(""" if (a > 5) b = 6; elseif (a < 5) b = 4; endif""")) self.assertTrue(tree is not False) # Complex example tree = self.parser.parse( PARTIAL_BLOCK.format(""" if (a > 5 && a < 100) b = 6; elseif (a < 2 || a > 100) if (a < 0) b = 4; else b = 2; endif else b = 3; endif""")) self.assertTrue(tree is not False) def test_warn_on_parse_fail(self): with patch('logging.warning') as mock_warn: tree = self.parser.parse("This code is not valid") self.assertFalse(tree) self.assertEqual(mock_warn.call_count, 2)
class ShardMaskParsingTests(unittest.TestCase): def setUp(self) -> None: try: self.parser = EtchParser() self.assertIsNone(self.parser._parsed_tree, "Unexpected initialisation of parsed tree") except ParseError as e: self.fail("Parser isntantiation failed with: \n" + str(e)) def test_outside_annotation(self): """Test handling of calls to subfunctions containing use statements""" self.parser.parse(NON_ENTRY_GLOBAL) # Detect call to non-entry function that uses globals with self.assertRaises(UnparsableAddress): glob_used = self.parser.globals_used('setup', ['abc']) self.assertEqual( len(glob_used), 0, "Unexpected used globals found when declared in non annotated function" ) # Test transfer function, which calls a non-global-using subfunction glob_used = self.parser.globals_used('transfer', ['abc', 'def', 100]) self.assertEqual( '{}.{}'.format(glob_used[0][0], glob_used[0][1].value), 'balance_state.abc') self.assertEqual( '{}.{}'.format(glob_used[1][0], glob_used[1][1].value), 'balance_state.def') def test_global_using_subfunctions(self): """Test detection of non-annotated functions containing 'use' statements""" self.parser.parse(NON_ENTRY_GLOBAL) # List of non-annotated functions that use globals global_using_subfunctions = self.parser.global_using_subfunctions() self.assertIn('set_balance', global_using_subfunctions) self.assertNotIn('sub', global_using_subfunctions) # Test that wildcard used when annotated function calls global using subfunction with self.assertRaises(UnparsableAddress): self.parser.used_globals_to_addresses('setup', ['abc']) # Parsing of function that doesn't call global-using-subfunction should succeed glob_addresses = self.parser.used_globals_to_addresses( 'transfer', ['abc', 'def', 100]) self.assertEqual(glob_addresses, ['balance_state.abc', 'balance_state.def']) def test_use_any(self): """Test correct handling of 'use any'""" self.parser.parse(USE_ANY_NON_SHARDED) # Test correct detection of all persistent globals when none are sharded used_globals = self.parser.globals_used('swap', []) self.assertEqual(set(used_globals), {'var1', 'var2'}) # Test correct raising of wildcard-needed exception if any globals are sharded self.parser.parse(USE_ANY_SHARDED) with self.assertRaises(UseWildcardShardMask): used_globals = self.parser.globals_used('swap', []) def test_unparsable(self): """Test that parser raises an exception when parsing fails""" with patch('logging.warning') as mock_warn: self.parser.parse("This is not valid etch code") self.assertEqual(mock_warn.call_count, 2) with self.assertRaises(EtchParserError): used_globals = self.parser.globals_used('entry', [])