%%% -*- 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 with the " ++ atom_to_list(Backend) ++ " backend", fun() -> case compile(Backend, ContractName) of #{byte_code := ByteCode, contract_source := _, type_info := _} when Backend == aevm -> ?assertMatch(Code when is_binary(Code), ByteCode); #{fate_code := Code} when Backend == fate -> 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(), Backend <- [aevm, fate], not lists:member(ContractName, not_compilable_on(Backend))] ++ [ {"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(aevm, ContractName), check_errors(ExpectedErrors, Errors) end} || {ContractName, ExpectedErrors} <- failing_contracts() ] ++ [ {"Testing " ++ atom_to_list(Backend) ++ " code generation error messages of " ++ ContractName, fun() -> Errors = compile(Backend, ContractName), Expect = case is_binary(ExpectedError) of true -> [ExpectedError]; false -> case proplists:get_value(Backend, ExpectedError, no_error) of no_error -> no_error; Err -> [Err] end end, check_errors(Expect, Errors) end} || {ContractName, ExpectedError} <- failing_code_gen_contracts(), Backend <- [aevm, fate] ] ++ [ {"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(aevm, "include", [{include, {explicit_files, FileSystem}}]), #{byte_code := Code2} = compile(aevm, "include"), ?assertMatch(true, Code1 == Code2) end} ] ++ [ {"Testing deadcode elimination for " ++ atom_to_list(Backend), fun() -> #{ byte_code := NoDeadCode } = compile(Backend, "nodeadcode"), #{ byte_code := DeadCode } = compile(Backend, "deadcode"), SizeNoDeadCode = byte_size(NoDeadCode), SizeDeadCode = byte_size(DeadCode), Delta = if Backend == aevm -> 40; Backend == fate -> 20 end, ?assertMatch({_, _, true}, {SizeDeadCode, SizeNoDeadCode, SizeDeadCode + Delta < SizeNoDeadCode}), ok end} || Backend <- [aevm, fate] ] ++ []. %% 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}, {backend, fate}], 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. compile(Backend, Name) -> compile(Backend, Name, [{include, {file_system, [aeso_test_utils:contract_path()]}}]). compile(Backend, 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"}, {backend, Backend} , {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", "simple", "simple_storage", "spend_test", "stack", "test", "builtin_bug", "builtin_map_get_bug", "lc_record_bug", "nodeadcode", "deadcode", "variant_types", "state_handling", "events", "include", "basic_auth", "basic_auth_tx", "bitcoin_auth", "address_literals", "bytes_equality", "address_chain", "namespace_bug", "bytes_to_x", "bytes_concat", "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", "test" % Custom general-purpose test file. Keep it last on the list. ]. not_compilable_on(fate) -> []; not_compilable_on(aevm) -> compilable_contracts(). 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)). 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, [<>]) %% 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, [<>]) , ?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_concat, [<>, <>, <>, <>, <>]) , ?TYPE_ERROR(bad_bytes_split, [<>, <>, <>]) , ?TYPE_ERROR(wrong_compiler_version, [<>, <>]) , ?TYPE_ERROR(interface_with_defs, [<>]) , ?TYPE_ERROR(contract_as_namespace, [<>]) , ?TYPE_ERROR(toplevel_let, [<>]) , ?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\n" " and (int) => 'a\n", "when checking the application at line 3, column 39 of\n" " f : () => unit\n" "to arguments\n" " 1 : int">>, < 'e\n" " and (int) => 'd\n" "when checking the application at line 4, column 20 of\n" " g : (int, string) => 'e\n" "to arguments\n" " 1 : int">>, < 'c\n" " and (string) => 'b\n" "when checking the application at line 5, column 20 of\n" " g : (int, string) => 'c\nto arguments\n" " \"Litwo, ojczyzno moja\" : string">> ]) , ?TYPE_ERROR(bad_state, [<>]) , ?TYPE_ERROR(factories_type_errors, [<>, < if(protected, option(void), void)\n and (gas : int, value : int, protected : bool, int, bool) => 'b\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)\nagainst the expected type\n (gas : int, value : int, protected : bool, int, bool) => 'b">>, <>, <>, < if(protected, option(void), void)\n 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, [<> ]) ]. -define(Path(File), "code_errors/" ??File). -define(Msg(File, Line, Col, Err), <>). -define(SAME(File, Line, Col, Err), {?Path(File), ?Msg(File, Line, Col, Err)}). -define(AEVM(File, Line, Col, Err), {?Path(File), [{aevm, ?Msg(File, Line, Col, Err)}]}). -define(FATE(File, Line, Col, Err), {?Path(File), [{fate, ?Msg(File, Line, Col, Err)}]}). -define(BOTH(File, Line, Col, ErrAEVM, ErrFATE), {?Path(File), [{aevm, ?Msg(File, Line, Col, ErrAEVM)}, {fate, ?Msg(File, Line, Col, ErrFATE)}]}). failing_code_gen_contracts() -> [ ?SAME(missing_definition, 2, 14, "Missing definition of function 'foo'.") , ?AEVM(polymorphic_entrypoint, 2, 17, "The argument\n" " x : 'a\n" "of entrypoint 'id' has a polymorphic (contains type variables) type.\n" "Use the FATE backend if you want polymorphic entrypoints.") , ?AEVM(polymorphic_entrypoint_return, 2, 3, "The return type\n" " 'a\n" "of entrypoint 'fail' is polymorphic (contains type variables).\n" "Use the FATE backend if you want polymorphic entrypoints.") , ?SAME(higher_order_entrypoint, 2, 20, "The argument\n" " f : (int) => int\n" "of entrypoint 'apply' has a higher-order (contains function types) type.") , ?SAME(higher_order_entrypoint_return, 2, 3, "The return type\n" " (int) => int\n" "of entrypoint 'add' is higher-order (contains function types).") , ?SAME(missing_init_function, 1, 10, "Missing init function for the contract 'MissingInitFunction'.\n" "The 'init' function can only be omitted if the state type is 'unit'.") , ?SAME(parameterised_state, 3, 8, "The state type cannot be parameterized.") , ?SAME(parameterised_event, 3, 12, "The event type cannot be parameterized.") , ?SAME(polymorphic_aens_resolve, 4, 5, "Invalid return type of AENS.resolve:\n" " 'a\n" "It must be a string or a pubkey type (address, oracle, etc).") , ?SAME(bad_aens_resolve, 6, 5, "Invalid return type of AENS.resolve:\n" " list(int)\n" "It must be a string or a pubkey type (address, oracle, etc).") , ?AEVM(polymorphic_compare, 4, 5, "Cannot compare values of type\n" " 'a\n" "The AEVM only supports '==' on values of\n" "- word type (int, bool, bits, address, oracle(_, _), etc)\n" "- type string\n" "- tuple or record of word type\n" "Use FATE if you need to compare arbitrary types.") , ?AEVM(complex_compare, 4, 5, "Cannot compare values of type\n" " (string * int)\n" "The AEVM only supports '!=' on values of\n" "- word type (int, bool, bits, address, oracle(_, _), etc)\n" "- type string\n" "- tuple or record of word type\n" "Use FATE if you need to compare arbitrary types.") , ?AEVM(complex_compare_leq, 4, 5, "Cannot compare values of type\n" " (int * int)\n" "The AEVM only supports '=<' on values of\n" "- word type (int, bool, bits, address, oracle(_, _), etc)\n" "Use FATE if you need to compare arbitrary types.") , ?AEVM(higher_order_compare, 4, 5, "Cannot compare values of type\n" " (int) => int\n" "The AEVM only supports '<' on values of\n" "- word type (int, bool, bits, address, oracle(_, _), etc)\n" "Use FATE if you need to compare arbitrary types.") , ?AEVM(unapplied_contract_call, 6, 19, "The AEVM does not support unapplied contract call to\n" " r : Remote\n" "Use FATE if you need this.") , ?AEVM(unapplied_named_arg_builtin, 4, 15, "The AEVM does not support unapplied use of Oracle.register.\n" "Use FATE if you need this.") , ?AEVM(polymorphic_map_keys, 4, 34, "Invalid map key type\n" " 'a\n" "Map keys cannot be polymorphic in the AEVM. Use FATE if you need this.") , ?AEVM(higher_order_map_keys, 4, 42, "Invalid map key type\n" " (int) => int\n" "Map keys cannot be higher-order.") , ?SAME(polymorphic_query_type, 3, 5, "Invalid oracle type\n" " oracle('a, 'b)\n" "The query type must not be polymorphic (contain type variables).") , ?SAME(polymorphic_response_type, 3, 5, "Invalid oracle type\n" " oracle(string, 'r)\n" "The response type must not be polymorphic (contain type variables).") , ?SAME(higher_order_query_type, 3, 5, "Invalid oracle type\n" " oracle((int) => int, string)\n" "The query type must not be higher-order (contain function types).") , ?SAME(higher_order_response_type, 3, 5, "Invalid oracle type\n" " oracle(string, (int) => int)\n" "The response type must not be higher-order (contain function types).") , ?AEVM(higher_order_state, 3, 3, "Invalid state type\n" " {f : (int) => int}\n" "The state cannot contain functions in the AEVM. Use FATE if you need this.") , ?FATE(child_with_decls, 2, 14, "Missing definition of function 'f'.") ]. 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(fate, 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 ++ [{backend, fate}, {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.