%%% -*- erlang-indent-level:4; indent-tabs-mode: nil -*- %%%------------------------------------------------------------------- %%% @copyright (C) 2018, Aeternity Anstalt %%% @doc Test Sophia language compiler. %%% %%% @end %%%------------------------------------------------------------------- -module(aeso_compiler_tests). -compile([export_all, nowarn_export_all]). -include_lib("eunit/include/eunit.hrl"). run_test(Test) -> TestFun = list_to_atom(lists:concat([Test, "_test_"])), [ begin io:format("~s\n", [Label]), Fun() end || {Label, Fun} <- ?MODULE:TestFun() ], ok. %% Very simply test compile the given contracts. Only basic checks %% are made on the output, just that it is a binary which indicates %% that the compilation worked. simple_compile_test_() -> [ {"Testing the " ++ ContractName ++ " contract", fun() -> case compile(ContractName) of #{fate_code := Code} -> Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), ?assertMatch({X, X}, {Code1, Code}); Error -> io:format("\n\n~p\n\n", [Error]), print_and_throw(Error) end end} || ContractName <- compilable_contracts()] ++ [ {"Test file not found error", fun() -> {error, Errors} = aeso_compiler:file("does_not_exist.aes"), ExpErr = <<"File error:\ndoes_not_exist.aes: no such file or directory">>, check_errors([ExpErr], Errors) end} ] ++ [ {"Testing error messages of " ++ ContractName, fun() -> Errors = compile(ContractName, [warn_all, warn_error]), check_errors(ExpectedErrors, Errors) end} || {ContractName, ExpectedErrors} <- failing_contracts() ] ++ [ {"Testing include with explicit files", fun() -> FileSystem = maps:from_list( [ begin {ok, Bin} = file:read_file(filename:join([aeso_test_utils:contract_path(), File])), {File, Bin} end || File <- ["included.aes", "../contracts/included2.aes"] ]), #{byte_code := Code1} = compile("include", [{include, {explicit_files, FileSystem}}]), #{byte_code := Code2} = compile("include"), ?assertMatch(true, Code1 == Code2) end} ] ++ [ {"Testing deadcode elimination", fun() -> #{ byte_code := NoDeadCode } = compile("nodeadcode"), #{ byte_code := DeadCode } = compile("deadcode"), SizeNoDeadCode = byte_size(NoDeadCode), SizeDeadCode = byte_size(DeadCode), Delta = 20, ?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}), ok end} ] ++ [ {"Testing warning messages", fun() -> #{ warnings := Warnings } = compile("warnings", [warn_all]), #{ warnings := [] } = compile("warning_unused_include_no_include", [warn_all]), #{ warnings := [] } = compile("warning_used_record_typedef", [warn_all]), check_warnings(warnings(), Warnings) end} ] ++ []. %% Check if all modules in the standard library compile stdlib_test_() -> {ok, Files} = file:list_dir(aeso_stdlib:stdlib_include_path()), [ { "Testing " ++ File ++ " from the stdlib", fun() -> String = "include \"" ++ File ++ "\"\nmain contract Test =\n entrypoint f(x) = x", Options = [{src_file, File}], case aeso_compiler:from_string(String, Options) of {ok, #{fate_code := Code}} -> Code1 = aeb_fate_code:deserialize(aeb_fate_code:serialize(Code)), ?assertMatch({X, X}, {Code1, Code}); {error, Error} -> io:format("\n\n~p\n\n", [Error]), print_and_throw(Error) end end} || File <- Files, lists:suffix(".aes", File) ]. check_errors(no_error, Actual) -> ?assertMatch(#{}, Actual); check_errors(Expect, #{}) -> ?assertEqual({error, Expect}, ok); check_errors(Expect0, Actual0) -> Expect = lists:sort(Expect0), Actual = [ list_to_binary(string:trim(aeso_errors:pp(Err))) || Err <- Actual0 ], case {Expect -- Actual, Actual -- Expect} of {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); {Missing, Extra} -> ?assertEqual(Missing, Extra) end. check_warnings(Expect0, Actual0) -> Expect = lists:sort(Expect0), Actual = [ list_to_binary(string:trim(aeso_warnings:pp(Warn))) || Warn <- Actual0 ], case {Expect -- Actual, Actual -- Expect} of {[], Extra} -> ?assertMatch({unexpected, []}, {unexpected, Extra}); {Missing, []} -> ?assertMatch({missing, []}, {missing, Missing}); {Missing, Extra} -> ?assertEqual(Missing, Extra) end. compile(Name) -> compile( Name, [{include, {file_system, [aeso_test_utils:contract_path()]}}]). compile(Name, Options) -> String = aeso_test_utils:read_contract(Name), Options1 = case lists:member(Name, debug_mode_contracts()) of true -> [debug_mode]; false -> [] end ++ [ {src_file, Name ++ ".aes"} , {include, {file_system, [aeso_test_utils:contract_path()]}} ] ++ Options, case aeso_compiler:from_string(String, Options1) of {ok, Map} -> Map; {error, ErrorString} when is_binary(ErrorString) -> ErrorString; {error, Errors} -> Errors end. %% compilable_contracts() -> [ContractName]. %% The currently compilable contracts. compilable_contracts() -> ["complex_types", "counter", "dutch_auction", "environment", "factorial", "functions", "fundme", "identity", "maps", "oracles", "remote_call", "remote_call_ambiguous_record", "simple", "simple_storage", "spend_test", "stack", "test", "builtin_bug", "builtin_map_get_bug", "lc_record_bug", "nodeadcode", "deadcode", "variant_types", "state_handling", "events", "include", "relative_include", "basic_auth", "basic_auth_tx", "bitcoin_auth", "address_literals", "bytes_equality", "address_chain", "namespace_bug", "bytes_to_x", "bytes_concat", "bytes_misc", "aens", "aens_update", "tuple_match", "cyclic_include", "stdlib_include", "double_include", "manual_stdlib_include", "list_comp", "payable", "unapplied_builtins", "underscore_number_literals", "pairing_crypto", "qualified_constructor", "let_patterns", "lhs_matching", "more_strings", "protected_call", "hermetization_turnoff", "multiple_contracts", "clone", "clone_simple", "create", "child_contract_init_bug", "using_namespace", "assign_patterns", "patterns_guards", "pipe_operator", "polymorphism_contract_implements_interface", "polymorphism_contract_multi_interface", "polymorphism_contract_interface_extends_interface", "polymorphism_contract_interface_extensions", "polymorphism_contract_interface_same_decl_multi_interface", "polymorphism_contract_interface_same_name_same_type", "polymorphism_variance_switching_chain_create", "polymorphism_variance_switching_void_supertype", "polymorphism_variance_switching_unify_with_interface_decls", "polymorphism_preserve_or_add_payable_contract", "polymorphism_preserve_or_add_payable_entrypoint", "polymorphism_preserve_or_remove_stateful_entrypoint", "missing_init_fun_state_unit", "complex_compare_leq", "complex_compare", "higher_order_compare", "higher_order_map_keys", "higher_order_state", "polymorphic_compare", "polymorphic_entrypoint", "polymorphic_entrypoint_return", "polymorphic_map_keys", "unapplied_contract_call", "unapplied_named_arg_builtin", "resolve_field_constraint_by_arity", "toplevel_constants", "ceres", "test" % Custom general-purpose test file. Keep it last on the list. ]. debug_mode_contracts() -> ["hermetization_turnoff"]. %% Contracts that should produce type errors -define(Pos(Kind, File, Line, Col), (list_to_binary(Kind))/binary, " error in '", (list_to_binary(File))/binary, ".aes' at line " ??Line ", col " ??Col ":\n"). -define(Pos(Line, Col), ?Pos(__Kind, __File, Line, Col)). -define(ERROR(Kind, Name, Errs), (fun() -> __Kind = Kind, __File = ??Name, {__File, Errs} end)()). -define(TYPE_ERROR(Name, Errs), ?ERROR("Type", Name, Errs)). -define(PARSE_ERROR(Name, Errs), ?ERROR("Parse", Name, Errs)). -define(PosW(Kind, File, Line, Col), (list_to_binary(Kind))/binary, " in '", (list_to_binary(File))/binary, ".aes' at line " ??Line ", col " ??Col ":\n"). -define(PosW(Line, Col), ?PosW(__Kind, __File, Line, Col)). -define(WARNING(Name, Warns), (fun() -> __Kind = "Warning", __File = ??Name, Warns end)()). warnings() -> ?WARNING(warnings, [<>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> ]). failing_contracts() -> {ok, V} = aeso_compiler:numeric_version(), Version = list_to_binary(string:join([integer_to_list(N) || N <- V], ".")), %% Parse errors [ ?PARSE_ERROR(field_parse_error, [<>]) , ?PARSE_ERROR(vsemi, [<>]) , ?PARSE_ERROR(vclose, [<>]) , ?PARSE_ERROR(indent_fail, [<>]) , ?PARSE_ERROR(assign_pattern_to_pattern, [<>]) %% Type errors , ?TYPE_ERROR(name_clash, [<>, <>, <>, <>, <>]) , ?TYPE_ERROR(type_errors, [<>, < list(int)`\n" "to arguments\n" " `x : int`\n" " `x : int`">>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>]) , ?TYPE_ERROR(init_type_error, [<>]) , ?TYPE_ERROR(missing_state_type, [<>]) , ?TYPE_ERROR(missing_fields_in_record_expression, [<>, <>, <>]) , ?TYPE_ERROR(namespace_clash_builtin, [<>]) , ?TYPE_ERROR(namespace_clash_included, [<>]) , ?TYPE_ERROR(namespace_clash_same_file, [<>]) , ?TYPE_ERROR(bad_events, [<>, <>]) , ?TYPE_ERROR(bad_events2, [<>, <>]) , ?TYPE_ERROR(type_clash, [<>, <>]) , ?TYPE_ERROR(not_toplevel_include, [<>]) , ?TYPE_ERROR(not_toplevel_namespace, [<>]) , ?TYPE_ERROR(not_toplevel_contract, [<>]) , ?TYPE_ERROR(bad_address_literals, [<>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>]) , ?TYPE_ERROR(stateful, [<>, <>, <>, <>, <>, <>, <>, <>]) , ?TYPE_ERROR(bad_init_state_access, [<>, <>, <>]) , ?TYPE_ERROR(modifier_checks, [<>, <>, <>, <>, < unit`">>, <>, < unit`">>]) , ?TYPE_ERROR(list_comp_not_a_list, [<> ]) , ?TYPE_ERROR(list_comp_if_not_bool, [<> ]) , ?TYPE_ERROR(list_comp_bad_shadow, [<> ]) , ?TYPE_ERROR(map_as_map_key, [<>, <>, <>]) , ?TYPE_ERROR(calling_init_function, [<>]) , ?TYPE_ERROR(bad_top_level_decl, [<>]) , ?TYPE_ERROR(missing_event_type, [<>]) , ?TYPE_ERROR(bad_bytes_to_x, [<>, < option('a)`\n" "to arguments\n" " `b : bytes(4)`">>, <>, <>]) , ?TYPE_ERROR(bad_bytes_concat, [<>, <>, <>, <>, <>]) , ?TYPE_ERROR(bad_bytes_split, [<>, <>, <>]) , ?TYPE_ERROR(wrong_compiler_version, [<>, <>]) , ?TYPE_ERROR(interface_with_defs, [<>]) , ?TYPE_ERROR(contract_as_namespace, [<>]) , ?TYPE_ERROR(empty_typedecl, [<>]) , ?TYPE_ERROR(higher_kinded_type, [<>]) , ?TYPE_ERROR(bad_arity, [<>, <>, <>, <>]) , ?TYPE_ERROR(bad_unnamed_map_update_default, [<>]) , ?TYPE_ERROR(non_functional_entrypoint, [<>]) , ?TYPE_ERROR(bad_records, [<>, <>, <> ]) , ?TYPE_ERROR(bad_protected_call, [<> ]) , ?TYPE_ERROR(bad_function_block, [<>, <> ]) , ?TYPE_ERROR(just_an_empty_file, [<> ]) , ?TYPE_ERROR(bad_number_of_args, [< unit` and `(int) => 'a`\n", "when checking the application of\n" " `f : () => unit`\n" "to arguments\n" " `1 : int`">>, < 'e` and `(int) => 'd`\n" "when checking the application of\n" " `g : (int, string) => 'e`\n" "to arguments\n" " `1 : int`">>, < 'c` and `(string) => 'b`\n" "when checking the application of\n" " `g : (int, string) => 'c`\n" "to arguments\n" " `\"Litwo, ojczyzno moja\" : string`">> ]) , ?TYPE_ERROR(bad_state, [<>]) , ?TYPE_ERROR(factories_type_errors, [<>, < if(protected, option(void), void)` and `(gas : int, value : int, protected : bool, int, bool) => if(protected, option(void), void)`\n" "when checking contract construction of type\n" " (gas : int, value : int, protected : bool) =>\n" " if(protected, option(void), void) (at line 11, column 18)\n" "against the expected type\n" " (gas : int, value : int, protected : bool, int, bool) =>\n" " if(protected, option(void), void)">>, <>, <>, <>, < if(protected, option(void), void)` and `(gas : int, value : int, protected : bool) => 'a`\n" "when checking contract construction of type\n (gas : int, value : int, protected : bool, int, bool) =>\n if(protected, option(void), void) (at line 18, column 18)\nagainst the expected type\n (gas : int, value : int, protected : bool) => 'a">>, <>, <> ]) , ?TYPE_ERROR(ambiguous_main, [<> ]) , ?TYPE_ERROR(no_main_contract, [<> ]) , ?TYPE_ERROR(multiple_main_contracts, [<> ]) , ?TYPE_ERROR(using_namespace_ambiguous_name, [ <> , <> ]) , ?TYPE_ERROR(using_namespace_wrong_scope, [ <> , <> ]) , ?TYPE_ERROR(using_namespace_undefined, [<> ]) , ?TYPE_ERROR(using_namespace_undefined_parts, [<> ]) , ?TYPE_ERROR(using_namespace_hidden_parts, [<> ]) , ?TYPE_ERROR(stateful_pattern_guard, [<> ]) , ?TYPE_ERROR(non_boolean_pattern_guard, [<> ]) , ?TYPE_ERROR(empty_record_definition, [<> ]) , ?TYPE_ERROR(operator_lambdas, [< int` and `(int) => 'a`\n" "when checking the application of\n" " `(l : _, r : _) => l + r : (int, int) => int`\n" "to arguments\n" " `1 : int`">> ]) , ?TYPE_ERROR(warnings, [<>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> ]) , ?TYPE_ERROR(polymorphism_contract_interface_recursive, [<> ]) , ?TYPE_ERROR(polymorphism_contract_interface_same_name_different_type, [<> ]) , ?TYPE_ERROR(polymorphism_contract_missing_implementation, [<> ]) , ?TYPE_ERROR(polymorphism_contract_same_decl_multi_interface, [<> ]) , ?TYPE_ERROR(polymorphism_contract_undefined_interface, [<> ]) , ?TYPE_ERROR(polymorphism_contract_same_name_different_type_multi_interface, [<> ]) , ?TYPE_ERROR(polymorphism_contract_interface_undefined_interface, [<> ]) , ?TYPE_ERROR(polymorphism_variance_switching, [< Cat`\n" "to arguments\n" " `x : Animal`">>, <>, < Animal) => Cat`\n" "to arguments\n" " `x : (Cat) => Cat`">>, <>, <> ]) , ?TYPE_ERROR(polymorphism_variance_switching_custom_types, [<>, <>, < Cat) => dt_inv(Cat)`\nto arguments\n `f_c_to_a : (Cat) => Animal`">>, <>, <>, <>, <>, < Cat) => dt_inv(Cat)`\nto arguments\n `f_c_to_a : (Cat) => Animal`">>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> ]) , ?TYPE_ERROR(polymorphism_variance_switching_records, [<>, <>, <>, <> ]) , ?TYPE_ERROR(polymorphism_variance_switching_oracles, [<>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <>, <> ]) , ?TYPE_ERROR(polymorphism_variance_switching_chain_create_fail, [<>, <>, <>, <> ]) , ?TYPE_ERROR(missing_definition, [<> ]) , ?TYPE_ERROR(child_with_decls, [<> ]) , ?TYPE_ERROR(parameterised_state, [<> ]) , ?TYPE_ERROR(parameterised_event, [<> ]) , ?TYPE_ERROR(missing_init_fun_alias_to_type, [<> ]) , ?TYPE_ERROR(missing_init_fun_alias_to_alias_to_type, [<> ]) , ?TYPE_ERROR(higher_order_entrypoint, [< int`\n" "of entrypoint `apply` has a higher-order (contains function types) type">> ]) , ?TYPE_ERROR(higher_order_entrypoint_return, [< int`\n" "of entrypoint `add` is higher-order (contains function types)">> ]) , ?TYPE_ERROR(polymorphic_aens_resolve, [<> ]) , ?TYPE_ERROR(bad_aens_resolve, [<> ]) , ?TYPE_ERROR(bad_aens_resolve_using, [<> ]) , ?TYPE_ERROR(polymorphic_query_type, [<>, <> ]) , ?TYPE_ERROR(polymorphic_response_type, [<> ]) , ?TYPE_ERROR(higher_order_query_type, [< int, string)`\n" "The query type must not be higher-order (contain function types)">> ]) , ?TYPE_ERROR(higher_order_response_type, [< int)`\n" "The response type must not be higher-order (contain function types)">> ]) , ?TYPE_ERROR(var_args_unify_let, [< 'b`">> ]) , ?TYPE_ERROR(var_args_unify_fun_call, [< 'b) => 'b`\n" "to arguments\n" " `Chain.create : (value : int, var_args) => 'c`">> ]) , ?TYPE_ERROR(polymorphism_add_stateful_entrypoint, [<> ]) , ?TYPE_ERROR(polymorphism_change_entrypoint_to_function, [<> ]) , ?TYPE_ERROR(polymorphism_non_payable_contract_implement_payable, [<> ]) , ?TYPE_ERROR(polymorphism_non_payable_interface_implement_payable, [<> ]) , ?TYPE_ERROR(polymorphism_remove_payable_entrypoint, [<> ]) , ?TYPE_ERROR(calling_child_contract_entrypoint, [<>]) , ?TYPE_ERROR(using_contract_as_namespace, [<>]) , ?TYPE_ERROR(hole_expression, [<>, <>, < int`">>, <> ]) , ?TYPE_ERROR(toplevel_constants_contract_as_namespace, [<>, <>, <>, <>, <> ]) , ?TYPE_ERROR(toplevel_constants_cycles, [<>, <> ]) , ?TYPE_ERROR(toplevel_constants_in_interface, [<>, <>, <> ]) , ?TYPE_ERROR(toplevel_constants_invalid_expr, [<>, <>, <>, <>, <>, <>, <>, <>, <>, <> ]) , ?TYPE_ERROR(toplevel_constants_invalid_id, [<>, <> ]) ]. validation_test_() -> [{"Validation fail: " ++ C1 ++ " /= " ++ C2, fun() -> Actual = case validate(C1, C2) of {error, Errs} -> Errs; ok -> #{} end, check_errors(Expect, Actual) end} || {C1, C2, Expect} <- validation_fails()] ++ [{"Validation of " ++ C, fun() -> ?assertEqual(ok, validate(C, C)) end} || C <- compilable_contracts()]. validation_fails() -> [{"deadcode", "nodeadcode", [<<"Data error:\n" "Byte code does not match source code.\n" "- Functions in the source code but not in the byte code:\n" " .MyList.map2">>]}, {"validation_test1", "validation_test2", [<<"Data error:\n" "Byte code does not match source code.\n" "- The implementation of the function code_fail is different.\n" "- The attributes of the function attr_fail differ:\n" " Byte code: payable\n" " Source code: \n" "- The type of the function type_fail differs:\n" " Byte code: integer => integer\n" " Source code: {tvar,0} => {tvar,0}">>]}, {"validation_test1", "validation_test3", [<<"Data error:\n" "Byte code contract is not payable, but source code contract is.">>]}]. validate(Contract1, Contract2) -> case compile(Contract1) of ByteCode = #{ fate_code := FCode } -> FCode1 = aeb_fate_code:serialize(aeb_fate_code:strip_init_function(FCode)), Source = aeso_test_utils:read_contract(Contract2), aeso_compiler:validate_byte_code( ByteCode#{ byte_code := FCode1 }, Source, case lists:member(Contract2, debug_mode_contracts()) of true -> [debug_mode]; false -> [] end ++ [ {src_file, lists:concat([Contract2, ".aes"])} , {include, {file_system, [aeso_test_utils:contract_path()]}} ]); Error -> print_and_throw(Error) end. print_and_throw(Err) -> case Err of ErrBin when is_binary(ErrBin) -> io:format("\n~s", [ErrBin]), error(ErrBin); Errors -> io:format("Compilation error:\n~s", [string:join([aeso_errors:pp(E) || E <- Errors], "\n\n")]), error(compilation_error) end.