-
Notifications
You must be signed in to change notification settings - Fork 35
Use Common Test for testing when Gradualizer should pass, fail, and its known problems #567
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
base: master
Are you sure you want to change the base?
Changes from all 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,60 @@ | ||
-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), | ||
?assert(Module /= undefined), | ||
case erlang:function_exported(Module, generated_tests, 0) of | ||
true -> | ||
{ok, Module:generated_tests()}; | ||
Comment on lines
+9
to
+13
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. What's this test suite doing? It looks like a lot of magic. I don't like magic. :) 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. There's a little bit of magic, but let's start from the standard CT conventions. CT treats files matching
The dynamic suite helper exports only one function - The tests are generated for each of the test files defined under the respective should pass/fail/known problems directories. The most enigmatic part is where to actually invoke |
||
false -> | ||
Path = ?config(dynamic_suite_test_path, Config), | ||
?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]), | ||
GeneratedTestsForm = make_generated_tests_form(TestNames), | ||
NewForms = Forms ++ TestForms ++ [GeneratedTestsForm, {eof, 0}], | ||
{ok, _} = merl:compile_and_load(NewForms), | ||
{ok, TestNames} | ||
end. | ||
|
||
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)). | ||
|
||
make_generated_tests_form(TestNames) -> | ||
Template = merl:quote("generated_tests() -> _@Body."), | ||
erl_syntax:revert(merl:subst(Template, [{'Body', merl:term(TestNames)}])). | ||
|
||
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,63 @@ | ||
-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]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(code:lib_dir(gradualizer), "test/known_problems/should_fail")}, | ||
{dynamic_test_template, known_problems_should_fail_template} | ||
], | ||
{ok, GeneratedTests} = gradualizer_dynamic_suite:reload(Config), | ||
[{all_tests, [parallel], GeneratedTests}]. | ||
|
||
init_per_suite(Config) -> | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(code:lib_dir(gradualizer)), | ||
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. | ||
|
||
known_problems_should_fail_template(_@File) -> | ||
Result = safe_type_check_file(_@File, [return_errors, {form_check_timeout_ms, 2000}]), | ||
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,55 @@ | ||
-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]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(code:lib_dir(gradualizer), "test/known_problems/should_pass")}, | ||
{dynamic_test_template, known_problems_should_pass_template} | ||
], | ||
{ok, GeneratedTests} = gradualizer_dynamic_suite:reload(Config), | ||
[{all_tests, [parallel], GeneratedTests}]. | ||
|
||
init_per_suite(Config) -> | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(code:lib_dir(gradualizer)), | ||
Config. | ||
|
||
load_prerequisites(_AppBase) -> | ||
ok. | ||
|
||
end_per_suite(_Config) -> | ||
ok = application:stop(gradualizer), | ||
ok. | ||
|
||
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, {form_check_timeout_ms, 2000}])), | ||
?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,56 @@ | ||
-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]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
Config = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(code:lib_dir(gradualizer), "test/should_fail")}, | ||
{dynamic_test_template, should_fail_template} | ||
], | ||
{ok, GeneratedTests} = gradualizer_dynamic_suite:reload(Config), | ||
[{all_tests, [parallel], GeneratedTests}]. | ||
|
||
init_per_suite(Config) -> | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(code:lib_dir(gradualizer)), | ||
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. | ||
|
||
should_fail_template(_@File) -> | ||
Errors = gradualizer:type_check_file(_@File, [return_errors, {form_check_timeout_ms, 2000}]), | ||
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)). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
-module(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]). | ||
|
||
suite() -> | ||
[{timetrap, {minutes, 10}}]. | ||
|
||
all() -> | ||
[{group, all_tests}]. | ||
|
||
groups() -> | ||
Opts = [ | ||
{dynamic_suite_module, ?MODULE}, | ||
{dynamic_suite_test_path, filename:join(code:lib_dir(gradualizer), "test/should_pass")}, | ||
{dynamic_test_template, should_pass_template} | ||
], | ||
{ok, GeneratedTests} = gradualizer_dynamic_suite:reload(Opts), | ||
[{all_tests, [parallel], GeneratedTests}]. | ||
|
||
init_per_suite(Config) -> | ||
{ok, _} = application:ensure_all_started(gradualizer), | ||
ok = load_prerequisites(code:lib_dir(gradualizer)), | ||
Config. | ||
|
||
load_prerequisites(AppBase) -> | ||
%% user_types.erl is referenced by remote_types.erl and opaque.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")]), | ||
gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/other_module.erl")]), | ||
%% imported.erl references any.erl | ||
gradualizer_db:import_erl_files([filename:join(AppBase, "test/should_pass/any.erl")]), | ||
ok. | ||
|
||
end_per_suite(_Config) -> | ||
ok = application:stop(gradualizer), | ||
ok. | ||
|
||
should_pass_template(_@File) -> | ||
?assertEqual(ok, gradualizer:type_check_file(_@File, [{form_check_timeout_ms, 2000}])). | ||
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. The EUnit tests print a Gradualizer's error message describing the type error when a test fails. I find it useful for quickly glancing over what's going on without having to inspect all the failed test files one by one. Do you think it's possible with CT? 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. It's available in the CT HTML report, but that requires a little bit of clicking in the browser. It might be possible to get in the shell, too, but I'd have to think some more about it. 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. IIRC, CT supports plain text reports too. I don't know it well, but if it's too complicated, maybe it isn's worth it. If we run it ourselves instead of relying on rebar3, we'll have better control over it. |
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
.