-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use Common Test for testing when Gradualizer should pass, fail, and its known problems #567
base: master
Are you sure you want to change the base?
Changes from 17 commits
c66cfb4
3993820
d97a9f4
a46d2a0
193dcb0
81e8526
4213687
57b9ee2
fcb443d
f150d53
34aa4a4
2c4df55
0983a7a
b1d8222
e345dc1
314ca8b
b7d3c32
06e5066
e6c91a8
dbd167f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
-module(gradualizer_dynamic_suite). | ||
|
||
-export([reload/1]). | ||
|
||
-include_lib("common_test/include/ct.hrl"). | ||
-include_lib("stdlib/include/assert.hrl"). | ||
|
||
reload(Config) -> | ||
Module = ?config(dynamic_suite_module, Config), | ||
Path = ?config(dynamic_suite_test_path, Config), | ||
?assert(Module /= undefined), | ||
?assert(Path /= undefined), | ||
Forms = get_forms(Module), | ||
FilesForms = map_erl_files(fun (File) -> | ||
make_test_form(Forms, File, Config) | ||
end, Path), | ||
{TestFiles, TestForms} = lists:unzip(FilesForms), | ||
TestNames = [ list_to_atom(filename:basename(File, ".erl")) || File <- TestFiles ], | ||
ct:pal("All tests found under ~s:\n~p\n", [Path, TestNames]), | ||
NewForms = Forms ++ TestForms ++ [{eof, 0}], | ||
{ok, _} = merl:compile_and_load(NewForms), | ||
{ok, TestNames}. | ||
|
||
map_erl_files(Fun, Dir) -> | ||
Files = filelib:wildcard(filename:join(Dir, "*.erl")), | ||
[{filename:basename(File), Fun(File)} || File <- Files]. | ||
|
||
make_test_form(Forms, File, Config) -> | ||
TestTemplateName = ?config(dynamic_test_template, Config), | ||
?assert(TestTemplateName /= undefined), | ||
TestTemplate = merl:quote("'@Name'(_) -> _@Body."), | ||
{function, _Anno, _Name, 1, Clauses} = lists:keyfind(TestTemplateName, 3, Forms), | ||
[{clause, _, _Args, _Guards, ClauseBodyTemplate}] = Clauses, | ||
TestName = filename:basename(File, ".erl"), | ||
ClauseBody = merl:subst(ClauseBodyTemplate, [{'File', erl_syntax:string(File)}]), | ||
TestEnv = [ | ||
{'Name', erl_syntax:atom(TestName)}, | ||
{'Body', ClauseBody} | ||
], | ||
erl_syntax:revert(merl:subst(TestTemplate, TestEnv)). | ||
|
||
get_forms(Module) -> | ||
ModPath = code:which(Module), | ||
{ok, {Module, [Abst]}} = beam_lib:chunks(ModPath, [abstract_code]), | ||
{abstract_code, {raw_abstract_v1, Forms}} = Abst, | ||
StripEnd = fun | ||
({eof, _}) -> false; | ||
(_) -> true | ||
end, | ||
lists:filter(StripEnd, Forms). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
-module(known_problems_should_fail_SUITE). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you duplicate the eunit test suites as commontest suite? I don't want duplicated logic. Delete the eunit test suites that have been ported to commontest. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did, yes. If we're happy with moving completely to CT, I'll delete the EUnit tests. |
||
|
||
-compile([export_all, nowarn_export_all]). | ||
|
||
%% EUnit has some handy macros, so let's use it, too | ||
-include_lib("eunit/include/eunit.hrl"). | ||
|
||
%% Test server callbacks | ||
-export([suite/0, | ||
all/0, | ||
groups/0, | ||
init_per_suite/1, end_per_suite/1, | ||
init_per_group/2, end_per_group/2, | ||
init_per_testcase/2, end_per_testcase/2]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
init_per_suite(Config0) -> | ||
AppBase = code:lib_dir(gradualizer), | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(AppBase, "test/known_problems/should_fail")}, | ||
{dynamic_test_template, known_problems_should_fail_template} | ||
] ++ Config0, | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(AppBase), | ||
{ok, TestNames} = gradualizer_dynamic_suite:reload(Config), | ||
case all_tests() of | ||
TestNames -> ok; | ||
_ -> ct:fail("Please update all_tests/0 to list all tests") | ||
end, | ||
Config. | ||
|
||
load_prerequisites(AppBase) -> | ||
%% exhaustive_user_type.erl is referenced by exhaustive_remote_user_type.erl | ||
gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), | ||
ok. | ||
|
||
end_per_suite(_Config) -> | ||
ok = application:stop(gradualizer), | ||
ok. | ||
|
||
init_per_group(_GroupName, Config) -> | ||
Config. | ||
|
||
end_per_group(_GroupName, _Config) -> | ||
ok. | ||
|
||
init_per_testcase(_TestCase, Config) -> | ||
Config. | ||
|
||
end_per_testcase(_TestCase, _Config) -> | ||
ok. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
[{all_tests, [parallel], all_tests()}]. | ||
|
||
all_tests() -> | ||
[arith_op,binary_comprehension,case_pattern_should_fail, | ||
exhaustive_argumentwise,exhaustive_expr,exhaustive_map_variants, | ||
exhaustive_remote_map_variants,guard_should_fail,infer_any_pattern, | ||
intersection_with_any_should_fail,intersection_with_unreachable, | ||
lambda_wrong_args,map_refinement_fancy,poly_lists_map_should_fail, | ||
poly_should_fail,recursive_types_should_fail,refine_ty_vars,sample]. | ||
|
||
known_problems_should_fail_template(_@File) -> | ||
Result = safe_type_check_file(_@File, [return_errors]), | ||
case Result of | ||
crash -> | ||
ok; | ||
Errors -> | ||
ErrorsExceptTimeouts = lists:filter( | ||
fun ({_File, {form_check_timeout, _}}) -> false; (_) -> true end, | ||
Errors), | ||
?assertEqual(0, length(ErrorsExceptTimeouts)) | ||
end. | ||
|
||
safe_type_check_file(File) -> | ||
safe_type_check_file(File, []). | ||
|
||
safe_type_check_file(File, Opts) -> | ||
try | ||
gradualizer:type_check_file(File, Opts) | ||
catch | ||
_:_ -> crash | ||
end. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
-module(known_problems_should_pass_SUITE). | ||
|
||
-compile([export_all, nowarn_export_all]). | ||
|
||
%% EUnit has some handy macros, so let's use it, too | ||
-include_lib("eunit/include/eunit.hrl"). | ||
|
||
%% Test server callbacks | ||
-export([suite/0, | ||
all/0, | ||
groups/0, | ||
init_per_suite/1, end_per_suite/1, | ||
init_per_group/2, end_per_group/2, | ||
init_per_testcase/2, end_per_testcase/2]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
init_per_suite(Config0) -> | ||
AppBase = code:lib_dir(gradualizer), | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(AppBase, "test/known_problems/should_pass")}, | ||
{dynamic_test_template, known_problems_should_pass_template} | ||
] ++ Config0, | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(AppBase), | ||
{ok, TestNames} = gradualizer_dynamic_suite:reload(Config), | ||
case all_tests() of | ||
TestNames -> ok; | ||
_ -> ct:fail("Please update all_tests/0 to list all tests") | ||
end, | ||
Config. | ||
|
||
load_prerequisites(_AppBase) -> | ||
ok. | ||
|
||
end_per_suite(_Config) -> | ||
ok = application:stop(gradualizer), | ||
ok. | ||
|
||
init_per_group(_GroupName, Config) -> | ||
Config. | ||
|
||
end_per_group(_GroupName, _Config) -> | ||
ok. | ||
|
||
init_per_testcase(_TestCase, Config) -> | ||
Config. | ||
|
||
end_per_testcase(_TestCase, _Config) -> | ||
ok. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
[{all_tests, [parallel], all_tests()}]. | ||
|
||
all_tests() -> | ||
[arith_op_arg_types,binary_exhaustiveness_checking_should_pass, | ||
call_intersection_function_with_union_arg_should_pass, | ||
different_normalization_levels,elixir_list_first,error_in_guard, | ||
fun_subtyping,generator_var_shadow,inner_union_subtype_of_root_union, | ||
intersection_should_pass,intersection_with_any,list_concat_op_should_pass, | ||
list_tail,map_pattern_duplicate_key,maybe_expr,poly_should_pass, | ||
poly_type_vars,recursive_types,refine_bound_var_on_mismatch, | ||
refine_bound_var_with_guard_should_pass,refine_comparison_should_pass, | ||
refine_list_tail,union_fun]. | ||
|
||
known_problems_should_pass_template(_@File) -> | ||
{ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), | ||
ExpectedErrors = typechecker:number_of_exported_functions(Forms), | ||
ReturnedErrors = length(safe_type_check_file(_@File, [return_errors])), | ||
?assertEqual(ExpectedErrors, ReturnedErrors). | ||
|
||
safe_type_check_file(File) -> | ||
safe_type_check_file(File, []). | ||
|
||
safe_type_check_file(File, Opts) -> | ||
try | ||
gradualizer:type_check_file(File, Opts) | ||
catch | ||
_:_ -> crash | ||
end. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
-module(should_fail_SUITE). | ||
|
||
-compile([export_all, nowarn_export_all]). | ||
|
||
%% EUnit has some handy macros, so let's use it, too | ||
-include_lib("eunit/include/eunit.hrl"). | ||
|
||
%% Test server callbacks | ||
-export([suite/0, | ||
all/0, | ||
groups/0, | ||
init_per_suite/1, end_per_suite/1, | ||
init_per_group/2, end_per_group/2, | ||
init_per_testcase/2, end_per_testcase/2]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
init_per_suite(Config0) -> | ||
AppBase = code:lib_dir(gradualizer), | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(AppBase, "test/should_fail")}, | ||
{dynamic_test_template, should_fail_template} | ||
] ++ Config0, | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(AppBase), | ||
{ok, TestNames} = gradualizer_dynamic_suite:reload(Config), | ||
case all_tests() of | ||
TestNames -> ok; | ||
_ -> ct:fail("Please update all_tests/0 to list all tests") | ||
end, | ||
Config. | ||
|
||
load_prerequisites(AppBase) -> | ||
%% user_types.erl is referenced by opaque_fail.erl. | ||
%% It is not in the sourcemap of the DB so let's import it manually | ||
gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/user_types.erl")]), | ||
%% exhaustive_user_type.erl is referenced by exhaustive_remote_user_type.erl | ||
gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_fail/exhaustive_user_type.erl")]), | ||
ok. | ||
|
||
end_per_suite(_Config) -> | ||
ok = application:stop(gradualizer), | ||
ok. | ||
|
||
init_per_group(_GroupName, Config) -> | ||
Config. | ||
|
||
end_per_group(_GroupName, _Config) -> | ||
ok. | ||
|
||
init_per_testcase(_TestCase, Config) -> | ||
Config. | ||
|
||
end_per_testcase(_TestCase, _Config) -> | ||
ok. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
[{all_tests, [parallel], all_tests()}]. | ||
|
||
all_tests() -> | ||
[annotated_types_fail,arg,arith_op_fail,arity_mismatch, | ||
erszcz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bc_fail,bin_expression,bin_type_error,branch,branch2,call, | ||
call_intersection_function_with_union_arg_fail,case_pattern, | ||
case_pattern2,catch_expr_fail,cons,covariant_map_keys_fail, | ||
cyclic_type_vars,depth,exhaustive,exhaustive_float, | ||
exhaustive_list_variants,exhaustive_refinable_map_variants, | ||
exhaustive_remote_user_type,exhaustive_string_variants, | ||
exhaustive_type,exhaustive_user_type, | ||
exhaustiveness_check_toggling,generator,guard_fail, | ||
imported_undef,infer_enabled,intersection_check, | ||
intersection_fail,intersection_infer, | ||
intersection_with_any_fail,iodata_fail,lambda_not_fun, | ||
lc_generator_not_none_fail,lc_not_list,list_infer_fail, | ||
list_op,list_op_should_fail,list_union_fail, | ||
lists_map_nonempty_fail,literal_char,literal_patterns, | ||
logic_op,map_entry,map_fail,map_failing_expr, | ||
map_failing_subtyping,map_field_invalid_update,map_literal, | ||
map_pattern_fail,map_refinement_fail,map_type_error,match, | ||
messaging_fail,module_info_fail,named_fun_fail, | ||
named_fun_infer_fail,nil,no_idempotent_xor, | ||
non_neg_plus_pos_is_pos_fail, | ||
nonempty_list_match_in_head_nonexhaustive, | ||
nonempty_string_fail,opaque_fail,operator_pattern_fail, | ||
pattern,pattern_record_fail,poly_fail,poly_lists_map_fail, | ||
poly_union_lower_bound_fail,pp_intersection,record, | ||
record_exhaustive,record_field,record_index, | ||
record_info_fail,record_refinement_fail,record_update, | ||
record_wildcard_fail,recursive_type_fail, | ||
recursive_types_failing,rel_op,return_fun_fail, | ||
rigid_type_variables_fail,send_fail,shortcut_ops_fail, | ||
spec_and_fun_clause_intersection_fail,string_literal, | ||
tuple_union_arg_fail,tuple_union_fail,tuple_union_pattern, | ||
tuple_union_refinement,type_refinement_fail,unary_op, | ||
unary_plus_fail,union_with_any,unreachable_after_refinement]. | ||
|
||
should_fail_template(_@File) -> | ||
Errors = gradualizer:type_check_file(_@File, [return_errors]), | ||
erszcz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Timeouts = [ E || {_File, {form_check_timeout, _}} = E <- Errors], | ||
?assertEqual(0, length(Timeouts)), | ||
%% Test that error formatting doesn't crash | ||
Opts = [{fmt_location, brief}, | ||
{fmt_expr_fun, fun erl_prettypr:format/1}], | ||
lists:foreach(fun({_, Error}) -> gradualizer_fmt:handle_type_error(Error, Opts) end, Errors), | ||
{ok, Forms} = gradualizer_file_utils:get_forms_from_erl(_@File, []), | ||
ExpectedErrors = typechecker:number_of_exported_functions(Forms), | ||
?assertEqual(ExpectedErrors, length(Errors)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We made this whole makefile work without rebar3 because of some annoyances we had with it and to get better control of what's happening. If we mix rebar3 with non-rebar3 we'll have files built in various place and a mess in general. I don't like that.
Isn't it fairly straitforward to run
ct
without rebar3? It's a command line tool.Add ct to the
tests
target and to.PHONY
.We should be able to modify the logic we have in
erl_cover_run
function to have coverage computed on eunit and commontest combined, or only commontest if we port all eunit tests to commontest.To run a specific suite, we can do something similar to what erlang.mk does, e.g.
make ct suite=known_problems_should_pass_SUITE
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, I think rebar3 is quite mature and I'd happily drop the Makefile, to be honest. though I admit I see some small benefits of using a tailored Makefile.
It's possible, but the nice CLI output is provided by a custom Rebar3 CT hook, so if we run CT directly, we'll get uglier printouts.
If we're happy with moving completely to CT, I can do that. For now, I considered this a nicer alternative for local development (especially comparing test results across builds), but did not include it in the CI, nor did I remove the original EUnit tests this is based on. In this light, it didn't make sense to run them twice, so the CT variants are not in
tests
.