%%% -*- 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}); ErrBin -> io:format("\n~s", [ErrBin]), error(ErrBin) end end} || ContractName <- compilable_contracts(), Backend <- [aevm, fate], not lists:member(ContractName, not_yet_compilable(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_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), case aeso_compiler:from_string(String, [{src_file, Name ++ ".aes"}, {backend, Backend} | Options]) 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", "nodeadcode", "deadcode", "variant_types", "state_handling", "events", "include", "basic_auth", "bitcoin_auth", "address_literals", "bytes_equality", "address_chain", "namespace_bug", "bytes_to_x", "bytes_concat", "aens", "tuple_match", "cyclic_include", "stdlib_include", "double_include", "manual_stdlib_include", "list_comp", "payable", "unapplied_builtins", "underscore_number_literals" ]. not_yet_compilable(fate) -> []; not_yet_compilable(aevm) -> []. %% 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, [< Remote.themap\n" "against the expected type\n" " (gas : int, value : int) => map(string, int)">>]) , ?TYPE_ERROR(bad_include_and_ns, [<>, <>]) , ?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(multiple_contracts, [<>]) , ?TYPE_ERROR(contract_as_namespace, [<>]) ]. -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(last_declaration_must_be_contract, 1, 1, "Expected a contract as the last declaration instead of the namespace 'LastDeclarationIsNotAContract'") , ?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.") ]. 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) -> ByteCode = #{ fate_code := FCode } = compile(fate, Contract1), 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, [{backend, fate}, {include, {file_system, [aeso_test_utils:contract_path()]}}]).