diff --git a/Makefile b/Makefile index 5efdc36..fdfb17d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PROJECT = katana -DEPS = eper aleppo xref_runner +DEPS = eper aleppo xref_runner elvis SHELL_DEPS := sync TEST_DEPS = mixer LOCAL_DEPS := xmerl tools compiler syntax_tools common_test inets ssl test_server hipe public_key dialyzer wx @@ -10,6 +10,7 @@ dep_sync = git https://github.com/inaka/sync.git 0.1.3 dep_aleppo = git https://github.com/inaka/aleppo.git 0.9.2 dep_xref_runner = git https://github.com/inaka/xref_runner.git 0.2.2 dep_mixer = git https://github.com/inaka/mixer.git 0.1.4 +dep_elvis = git https://github.com/inaka/elvis.git b69eea4 include erlang.mk diff --git a/README.md b/README.md index e0cd44e..7ccdeef 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,10 @@ And you can check all of our open-source projects at * `ktn_date`: functions useful for handling dates and time values. * `ktn_debug`: functions useful for debugging. +* `ktn_fsm`: a _nice_ wrapper on top of `gen_fsm` * `ktn_json`: functions useful for processing & creating JSON. * `ktn_maps`: functions useful for handling maps. +* `ktn_meta_SUITE`: a helper common_test suite to add meta-testing to your projects * `ktn_numbers`: functions useful for processing numeric values. * `ktn_recipe`: A tool to structure code that consists of sequential steps in which decisions are made. * `ktn_rpc`: functions useful for RPC mechanisms. @@ -113,6 +115,45 @@ Possible error values for the body assert are: - {error, {nomatch, Pattern, Body}} if the body does not match the regex. - {nomatch, ResBody, Body}} if the body does not match the text. +### `ktn_meta_SUITE` + +#### Goals +The **meta** suite lets you check your code with `dialyzer`, `xref` and `elvis`. + +#### Usage +To include the suite in your project, you only need to invoke its functions from a common_test suite. If you use [mixer](https://github.com/inaka/mixer) you can just do… + +```erlang +-module(your_meta_SUITE). + +-include_lib("mixer/include/mixer.hrl"). +-mixin(ktn_meta_SUITE). + +-export([init_per_suite/1]). + +init_per_suite(Config) -> [{application, serpents} | Config]. +``` + +Of course, you can choose what functions to include, for example if you want `dialyzer` but not `elvis` nor `xref` you can do… + +```erlang +-mixin([{ ktn_meta_SUITE + , [ all/0 + , dialyzer/1 + ] + }]). +``` + +#### Configuration +`ktn_meta_SUITE` uses the following configuration parameters that you can add to the common_test config for your test suite: + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `application` | The name of your app | **no default** | +| `dialyzer_warnings` | The active warnings for _diaylzer_ | `[error_handling, race_conditions, unmatched_returns]` | +| `plts` | The list of plt files for _dialyzer_ | `filelib:wildcard("your_app/*.plt`)` | +| `elvis_config` | Config file for _elvis_ | `"your_app/elvis.config"` | + ### `ktn_recipe` #### What is a recipe? diff --git a/deps/katana b/deps/katana new file mode 120000 index 0000000..a96aa0e --- /dev/null +++ b/deps/katana @@ -0,0 +1 @@ +.. \ No newline at end of file diff --git a/elvis.config b/elvis.config index 6fe68f6..5936585 100644 --- a/elvis.config +++ b/elvis.config @@ -3,10 +3,12 @@ elvis, [ {config, - [#{dirs => ["src", "src/*"], + [#{dirs => ["src", "test"], filter => "*.erl", - rules => [{elvis_style, line_length, #{limit => 80}}, + rules => [{elvis_style, line_length, #{limit => 80, + skip_comments => false}}, {elvis_style, no_tabs}, + {elvis_style, no_trailing_whitespace}, {elvis_style, macro_names}, {elvis_style, macro_module_names}, {elvis_style, operator_spaces, #{rules => [{right, ","}, @@ -24,17 +26,21 @@ #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", ignore => []} }, + { + elvis_style, + function_naming_convention, + #{regex => "^([a-z$][a-z0-9]*_?)*$"} + }, {elvis_style, state_record_and_type}, - {elvis_style, no_spec_with_records} + {elvis_style, no_spec_with_records}, + {elvis_style, dont_repeat_yourself, #{min_complexity => 15}}, + {elvis_style, no_debug_call, #{debug_functions => [{ct, pal}]}} ] }, #{dirs => ["."], filter => "Makefile", - rules => [{elvis_project, no_deps_master_erlang_mk, #{ignore => [eper, sync]}}] - }, - #{dirs => ["."], - filter => "rebar.config", - rules => [{elvis_project, no_deps_master_rebar, #{ignore => []}}] + rules => [{elvis_project, no_deps_master_erlang_mk, #{ignore => []}}, + {elvis_project, protocol_for_deps_erlang_mk, #{ignore => []}}] }, #{dirs => ["."], filter => "elvis.config", diff --git a/src/ktn_fsm.erl b/src/ktn_fsm.erl index 8365dd1..f11ea62 100644 --- a/src/ktn_fsm.erl +++ b/src/ktn_fsm.erl @@ -11,6 +11,8 @@ %%% looping. This way you don't have to have catch-all clauses there. -module(ktn_fsm). +-ignore_xref([start/3]). + -export( [ state/1 , call/2 diff --git a/src/ktn_meta_SUITE.erl b/src/ktn_meta_SUITE.erl new file mode 100644 index 0000000..12e6b41 --- /dev/null +++ b/src/ktn_meta_SUITE.erl @@ -0,0 +1,111 @@ +%%% @doc Meta Testing SUITE +%%% Use with mixer or by yourself. Just include a call to each of its functions +%%% in your common test suites. +%%% Make sure to add an application property to your common test configuration. +-module(ktn_meta_SUITE). +-author('elbrujohalcon@inaka.net'). + +-export([all/0]). +-export([xref/1, dialyzer/1, elvis/1]). + +-type config() :: [{atom(), term()}]. + +-spec all() -> [dialyzer | elvis | xref]. +all() -> [dialyzer, elvis, xref]. + +%% @doc xref's your code using xref_runner +-spec xref(config()) -> {comment, []}. +xref(Config) -> + BaseDir = base_dir(Config), + Dirs = [ filename:join(BaseDir, "ebin") + , filename:join(BaseDir, "test") + ], + + ct:comment("Undefined Function Calls"), + UFCs = xref_runner:check(undefined_function_calls, #{dirs => Dirs}), + + ct:comment("Undefined Functions"), + UFs = xref_runner:check(undefined_functions, #{dirs => Dirs}), + + ct:comment("Locals not Used"), + LNUs = xref_runner:check(locals_not_used, #{dirs => Dirs}), + + ct:comment("Deprecated Function Calls"), + DFCs = xref_runner:check(deprecated_function_calls, #{dirs => Dirs}), + + ct:comment("Deprecated Functions"), + DFs = xref_runner:check(deprecated_functions, #{dirs => Dirs}), + + ct:comment("There are no Warnings"), + [] = UFCs ++ UFs ++ LNUs ++ DFCs ++ DFs, + + {comment, ""}. + +%% @doc dialyzes your code. +%% By default it uses all the plts in the project root folder. +%% You can change that by providing a 'plts' parameter in Config. +%% You can also change the warnings using the 'dialyzer_warnings' parameter +-spec dialyzer(config()) -> {comment, []}. +dialyzer(Config) -> + BaseDir = base_dir(Config), + Plts = plts(Config), + Dirs = [ filename:join(BaseDir, "ebin") + , filename:join(BaseDir, "test") + ], + Warnings = + case test_server:lookup_config(dialyzer_warnings, Config) of + undefined -> [error_handling, race_conditions, unmatched_returns]; + Ws -> Ws + end, + + ct:comment("Dialyzer must emit no warnings"), + Opts = + [ {analysis_type, succ_typings} + , {plts, Plts} + , {files_rec, Dirs} + , {check_plt, true} + , {warnings, Warnings} + , {get_warnings, true} + ], + [] = [dialyzer:format_warning(W, basename) || W <- dialyzer:run(Opts)], + {comment, ""}. + +%% @doc Checks your code with elvis +-spec elvis(config()) -> {comment, []}. +elvis(Config) -> + ElvisConfig = + case test_server:lookup_config(elvis_config, Config) of + undefined -> + ConfigFile = filename:join(base_dir(Config), "elvis.config"), + [ fix_dirs(Group, Config) + || Group <- elvis_config:load_file(ConfigFile)]; + ConfigFile -> elvis_config:load_file(ConfigFile) + end, + + ct:comment("Elvis rocks!"), + ok = elvis:rock(ElvisConfig), + + {comment, ""}. + +base_dir(Config) -> + case test_server:lookup_config(application, Config) of + undefined -> ct:fail("Missing application in Config: ~p", [Config]); + App -> code:lib_dir(App) + end. + +plts(Config) -> + case test_server:lookup_config(plts, Config) of + undefined -> + Wildcard = filename:join(base_dir(Config), "*.plt"), + case filelib:wildcard(Wildcard) of + [] -> + ct:fail("No plts at ~s - you need to at least have one", [Wildcard]); + Plts -> Plts + end; + Plts -> Plts + end. + +fix_dirs(#{dirs := Dirs} = Group, Config) -> + NewDirs = + [filename:join(base_dir(Config), Dir) || Dir <- Dirs], + Group#{dirs := NewDirs}. diff --git a/src/ktn_recipe_verify.erl b/src/ktn_recipe_verify.erl index 5825939..b818cc7 100644 --- a/src/ktn_recipe_verify.erl +++ b/src/ktn_recipe_verify.erl @@ -98,10 +98,7 @@ verify_transitions( (X, A) -> [X | A] end, - case verify_transitions(F, Transitions) of - ok -> {ok, State}; - Error -> {error, State#{error => Error}} - end; + verify_transitions(F, Transitions, State); verify_transitions( State = #{recipe_type := explicit, transitions := Transitions} ) -> @@ -121,15 +118,15 @@ verify_transitions( (X, A) -> [X | A] end, - case verify_transitions(F, Transitions) of - ok -> {ok, State}; - Error -> {error, State#{error => Error}} - end. + verify_transitions(F, Transitions, State). -verify_transitions(F, Transitions) -> +verify_transitions(F, Transitions, State) -> case lists:foldl(F, [], Transitions) of - [] -> ok; - InvalidElements -> {invalid_transition_table_elements, InvalidElements} + [] -> {ok, State}; + InvalidElements -> + { error + , State#{error => {invalid_transition_table_elements, InvalidElements}} + } end. -spec verify_transition_exports(state()) -> {ok, state()} | {error, state()}. diff --git a/test/elvis.config b/test/elvis.config new file mode 100644 index 0000000..abdd8ac --- /dev/null +++ b/test/elvis.config @@ -0,0 +1,53 @@ +[ + { + elvis, + [ + {config, + [#{dirs => ["../../src", "../../test"], + filter => "*.erl", + rules => [{elvis_style, line_length, #{limit => 80, + skip_comments => false}}, + {elvis_style, no_tabs}, + {elvis_style, no_trailing_whitespace}, + {elvis_style, macro_names}, + {elvis_style, macro_module_names}, + {elvis_style, operator_spaces, #{rules => [{right, ","}, + {right, "++"}, + {left, "++"}]}}, + {elvis_style, nesting_level, #{level => 3}}, + {elvis_style, god_modules, #{limit => 25}}, + {elvis_style, no_if_expression}, + {elvis_style, invalid_dynamic_call, #{ignore => [ktn_recipe_verify]}}, + {elvis_style, used_ignored_variable}, + {elvis_style, no_behavior_info}, + { + elvis_style, + module_naming_convention, + #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", + ignore => []} + }, + { + elvis_style, + function_naming_convention, + #{regex => "^([a-z$][a-z0-9]*_?)*$"} + }, + {elvis_style, state_record_and_type}, + {elvis_style, no_spec_with_records}, + {elvis_style, dont_repeat_yourself, #{min_complexity => 15}}, + {elvis_style, no_debug_call, #{debug_functions => [{ct, pal}]}} + ] + }, + #{dirs => ["../.."], + filter => "Makefile", + rules => [{elvis_project, no_deps_master_erlang_mk, #{ignore => []}}, + {elvis_project, protocol_for_deps_erlang_mk, #{ignore => []}}] + }, + #{dirs => ["../.."], + filter => "elvis.config", + rules => [{elvis_project, old_configuration_format}] + } + ] + } + ] + } +]. diff --git a/test/katana.coverspec b/test/katana.coverspec index e1b791e..8354f09 100644 --- a/test/katana.coverspec +++ b/test/katana.coverspec @@ -9,6 +9,7 @@ ktn_json, ktn_lists, ktn_maps, + ktn_meta_SUITE, ktn_numbers, ktn_os, ktn_random, diff --git a/test/ktn_meta_suite_SUITE.erl b/test/ktn_meta_suite_SUITE.erl new file mode 100644 index 0000000..28cad02 --- /dev/null +++ b/test/ktn_meta_suite_SUITE.erl @@ -0,0 +1,22 @@ +-module(ktn_meta_suite_SUITE). + +-include_lib("mixer/include/mixer.hrl"). +-mixin([{ ktn_meta_SUITE + , [ all/0 + , xref/1 + , dialyzer/1 + , elvis/1 + ] + }]). + +-export([init_per_suite/1]). + +-type config() :: [{atom(), term()}]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + [ {application, katana} + , {elvis_config, "../../test/elvis.config"} + , {plts, ["../../.katana.plt"]} + | Config + ]. diff --git a/test/secure_vault.erl b/test/secure_vault.erl index cad4278..bc368fa 100644 --- a/test/secure_vault.erl +++ b/test/secure_vault.erl @@ -2,6 +2,8 @@ -behaviour(gen_fsm). +-ignore_xref([{ktn_fsm, start, 3}]). + -export([ start/2 , state/1 , contents/1