├── .gitignore ├── .travis.yml ├── COPYING ├── README.md ├── rebar.config ├── rebar.config.script └── src ├── coveralls.app.src ├── coveralls.erl └── rebar3_coveralls.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | doc 3 | ebin 4 | erl_crash.dump 5 | *~ 6 | _build 7 | rebar.lock 8 | .rebar 9 | .rebar3 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - wget https://s3.amazonaws.com/rebar3/rebar3 3 | - chmod u+x ./rebar3 4 | install: "true" 5 | language: erlang 6 | sudo: false 7 | otp_release: 8 | - 23.0 9 | - 22.0 10 | - 21.0 11 | - 20.0 12 | - 19.0 13 | script: 14 | - ./rebar3 do eunit,edoc 15 | - ./rebar3 as test coveralls send 16 | notifications: 17 | webhooks: https://coveralls.io/webhook 18 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015, Markus Ekholm 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL MARKUS EKHOLM BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | coveralls-erl 2 | ============= 3 | [![Build Status](https://travis-ci.org/markusn/coveralls-erl.png?branch=master)](https://travis-ci.org/markusn/coveralls-erl) 4 | [![Coverage Status](https://coveralls.io/repos/markusn/coveralls-erl/badge.png?branch=master)](https://coveralls.io/r/markusn/coveralls-erl?branch=master) 5 | [![Hex.pm](https://img.shields.io/hexpm/v/coveralls.svg?style=flat)](https://hex.pm/packages/coveralls) 6 | 7 | Erlang module to convert and send cover data to coveralls. Available as a hex package on https://hex.pm/packages/coveralls. 8 | 9 | ## Example usage: rebar3 and Travis CI 10 | In order to use coveralls-erl + Travis CI in your project you will need to add the following lines to your 11 | `rebar.config.script`: 12 | 13 | ```erlang 14 | case os:getenv("TRAVIS") of 15 | "true" -> 16 | JobId = os:getenv("TRAVIS_JOB_ID"), 17 | lists:keystore(coveralls_service_job_id, 1, CONFIG, {coveralls_service_job_id, JobId}); 18 | _ -> 19 | CONFIG 20 | end. 21 | ``` 22 | 23 | This will ensure that the rebar coveralls plugin will have access to the needed JobId and that the plugin is only run from Travis CI. 24 | 25 | You will also need to add the following lines to your `rebar.config`: 26 | ```erlang 27 | {plugins , [coveralls]}. % use hex package 28 | {cover_enabled , true}. 29 | {cover_export_enabled , true}. 30 | {coveralls_coverdata , "_build/test/cover/eunit.coverdata"}. % or a string with wildcards or a list of files 31 | {coveralls_service_name , "travis-ci"}. % use "travis-pro" when using with travis-ci.com 32 | ``` 33 | When using with travis-ci.com coveralls repo token also has to be added as `{coveralls_repo_token, "token_goes_here"}` 34 | 35 | These changes will add `coveralls-erl` as a dependency, tell `rebar3` where to find the plugin, make sure that the coverage data is produced and exported and configure `coveralls-erl` to use this data and the service `travis-ci`. 36 | 37 | And you send the coverdata to coveralls by issuing: `rebar3 as test coveralls send` 38 | 39 | **Note:** 40 | If you have dependencies specific to the test profile, or if you only add the coveralls dependency or any of its' configuration variables to the test profile you need to run coveralls using: `rebar3 as test coveralls send` 41 | 42 | ## Example: rebar3 and CircleCI 43 | Example `rebar.config.script`: 44 | 45 | ```erlang 46 | case {os:getenv("CIRCLECI"), os:getenv("COVERALLS_REPO_TOKEN")} of 47 | {"true", Token} when is_list(Token) -> 48 | JobId = os:getenv("CIRCLE_BUILD_NUM"), 49 | CONFIG1 = lists:keystore(coveralls_service_job_id, 1, CONFIG, {coveralls_service_job_id, JobId}), 50 | lists:keystore(coveralls_repo_token, 1, CONFIG1, {coveralls_repo_token, Token}); 51 | _ -> 52 | CONFIG 53 | end. 54 | ``` 55 | 56 | Example `rebar.config`: 57 | 58 | ```erlang 59 | 60 | {plugins , [coveralls]}. % use hex package 61 | {cover_enabled , true}. 62 | {cover_export_enabled , true}. 63 | {coveralls_coverdata , "_build/test/cover/ct.coverdata"}. 64 | {coveralls_service_name , "circle-ci"}. 65 | ``` 66 | 67 | Note that you'll need to set `COVERALLS_REPO_TOKEN` in your CircleCI environment variables! 68 | 69 | ## Example usage: rebar3 and GitHub Actions 70 | 71 | In order to use coveralls-erl + GitHub Actions in your project, you will need to add the following lines to your 72 | `rebar.config.script`: 73 | 74 | ```erlang 75 | case {os:getenv("GITHUB_ACTIONS"), os:getenv("GITHUB_TOKEN")} of 76 | {"true", Token} when is_list(Token) -> 77 | CONFIG1 = [{coveralls_repo_token, Token}, 78 | {coveralls_service_job_id, os:getenv("GITHUB_RUN_ID")}, 79 | {coveralls_commit_sha, os:getenv("GITHUB_SHA")}, 80 | {coveralls_service_number, os:getenv("GITHUB_RUN_NUMBER")} | CONFIG], 81 | case os:getenv("GITHUB_EVENT_NAME") =:= "pull_request" 82 | andalso string:tokens(os:getenv("GITHUB_REF"), "/") of 83 | [_, "pull", PRNO, _] -> 84 | [{coveralls_service_pull_request, PRNO} | CONFIG1]; 85 | _ -> 86 | CONFIG1 87 | end; 88 | _ -> 89 | CONFIG 90 | end. 91 | ``` 92 | 93 | This will ensure that the rebar coveralls plugin will have access to the needed JobId and that the plugin is only run from GitHub Actions. 94 | 95 | You will also need to add the following lines to your `rebar.config`: 96 | ```erlang 97 | {plugins , [coveralls]}. % use hex package 98 | {cover_enabled , true}. 99 | {cover_export_enabled , true}. 100 | {coveralls_coverdata , "_build/test/cover/eunit.coverdata"}. % or a string with wildcards or a list of files 101 | {coveralls_service_name , "github"}. 102 | ``` 103 | 104 | These changes will add `coveralls-erl` as a dependency, tell `rebar3` where to find the plugin, make sure that the coverage data is produced and exported and configure `coveralls-erl` to use this data and the service `github`. 105 | 106 | And you send the coverdata to coveralls by adding a step like: 107 | 108 | ``` 109 | - name: Coveralls 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | run: rebar3 as test coveralls send 113 | ``` 114 | 115 | Other available GitHub Actions Environment Variables are available [here](https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables) 116 | 117 | ## Optional settings 118 | 119 | The plugin also support the `coveralls_service_pull_request` and `coveralls_parallel` settings. 120 | See the Coveralls documentation for the meaning of those. 121 | 122 | ## Author 123 | Markus Ekholm (markus at botten dot org). 124 | 125 | ## License 126 | 3-clause BSD. For details see `COPYING`. 127 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [{jsx, "2.10.0"}]}. 2 | {profiles, [{test, [{plugins, [{coveralls, {git, "https://github.com/markusn/coveralls-erl", {branch, "master"}}}]}]}]}. 3 | {cover_enabled , true}. 4 | {cover_export_enabled , true}. 5 | {coveralls_coverdata , "_build/test/cover/eunit.coverdata"}. % or a string with wildcards or a list of files 6 | {coveralls_service_name , "travis-ci"}. 7 | {coveralls_parallel, true}. 8 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case os:getenv("TRAVIS") of 2 | "true" -> 3 | JobId = os:getenv("TRAVIS_JOB_ID"), 4 | lists:keystore(coveralls_service_job_id, 1, CONFIG, {coveralls_service_job_id, JobId}); 5 | _ -> 6 | CONFIG 7 | end. -------------------------------------------------------------------------------- /src/coveralls.app.src: -------------------------------------------------------------------------------- 1 | {application, coveralls, 2 | [ {description, "Coveralls for Erlang"} 3 | , {vsn, git} 4 | , {licenses, ["BSD"]} 5 | , {modules, []} 6 | , {registered, []} 7 | , {applications,[kernel, stdlib]} 8 | , {env, [{providers, [rebar3_coveralls]}]} 9 | , {maintainers, ["Markus Ekholm"]} 10 | , {links, [{"Github", "https://github.com/markusn/coveralls-erl"}]} 11 | ] 12 | }. 13 | -------------------------------------------------------------------------------- /src/coveralls.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2013-2016, Markus Ekholm 3 | %%% All rights reserved. 4 | %%% Redistribution and use in source and binary forms, with or without 5 | %%% modification, are permitted provided that the following conditions are met: 6 | %%% * Redistributions of source code must retain the above copyright 7 | %%% notice, this list of conditions and the following disclaimer. 8 | %%% * Redistributions in binary form must reproduce the above copyright 9 | %%% notice, this list of conditions and the following disclaimer in the 10 | %%% documentation and/or other materials provided with the distribution. 11 | %%% * Neither the name of the nor the 12 | %%% names of its contributors may be used to endorse or promote products 13 | %%% derived from this software without specific prior written permission. 14 | %%% 15 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | %%% ARE DISCLAIMED. IN NO EVENT SHALL MARKUS EKHOLM BE LIABLE FOR ANY 19 | %%% DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | %%% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | %%% 26 | %%% @copyright 2013-2016 (c) Markus Ekholm 27 | %%% @author Markus Ekholm 28 | %%% @doc coveralls 29 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 30 | 31 | %%============================================================================= 32 | %% Module declaration 33 | 34 | -module(coveralls). 35 | 36 | %%============================================================================= 37 | %% Exports 38 | 39 | -export([ convert_file/2 40 | , convert_and_send_file/2 41 | ]). 42 | 43 | %%============================================================================= 44 | %% Records 45 | 46 | -record(s, { importer = fun cover:import/1 47 | , module_lister = fun cover:imported_modules/0 48 | , mod_info = fun module_info_compile/1 49 | , file_reader = fun file:read_file/1 50 | , wildcard_reader = fun filelib:wildcard/1 51 | , analyser = fun cover:analyse/3 52 | , poster = fun httpc:request/4 53 | , poster_init = start_wrapper([fun ssl:start/0, fun inets:start/0]) 54 | }). 55 | 56 | %%============================================================================= 57 | %% Defines 58 | 59 | -define(COVERALLS_URL, "https://coveralls.io/api/v1/jobs"). 60 | %%-define(COVERALLS_URL, "http://127.0.0.1:8080"). 61 | 62 | -ifdef(random_only). 63 | -define(random, random). 64 | -else. 65 | -define(random, rand). 66 | -endif. 67 | 68 | %%============================================================================= 69 | %% API functions 70 | 71 | %% @doc Import and convert cover file(s) `Filenames' to a json string 72 | %% representation suitable to post to coveralls. 73 | %% 74 | %% Note that this function will crash if the modules mentioned in 75 | %% any of the `Filenames' are not available on the node. 76 | %% @end 77 | -spec convert_file(string() | [string()], map()) -> 78 | string(). 79 | convert_file(Filenames, Report) -> 80 | convert_file(Filenames, Report, #s{}). 81 | 82 | %% @doc Import and convert cover files `Filenames' to a json string and send the 83 | %% json to coveralls. 84 | %% @end 85 | -spec convert_and_send_file(string() | [string()], map()) -> ok. 86 | convert_and_send_file(Filenames, Report) -> 87 | convert_and_send_file(Filenames, Report, #s{}). 88 | 89 | %%============================================================================= 90 | %% Internal functions 91 | 92 | convert_file([L|_]=Filename, Report, S) when is_integer(L) -> 93 | %% single file or wildcard was specified 94 | WildcardReader = S#s.wildcard_reader, 95 | Filenames = WildcardReader(Filename), 96 | convert_file(Filenames, Report, S); 97 | convert_file([[_|_]|_]=Filenames, Report, S) -> 98 | ok = lists:foreach( 99 | fun(Filename) -> ok = import(S, Filename) end, 100 | Filenames), 101 | ConvertedModules = convert_modules(S), 102 | jsx:encode(Report#{source_files => ConvertedModules}, []). 103 | 104 | convert_and_send_file(Filenames, Report, S) -> 105 | send(convert_file(Filenames, Report, S), S). 106 | 107 | send(Json, #s{poster=Poster, poster_init=Init}) -> 108 | ok = Init(), 109 | Boundary = ["----------", integer_to_list(?random:uniform(1000))], 110 | Type = "multipart/form-data; boundary=" ++ Boundary, 111 | Body = to_body(Json, Boundary), 112 | R = Poster(post, {?COVERALLS_URL, [], Type, Body}, [], []), 113 | {ok, {{_, ReturnCode, _}, _, Message}} = R, 114 | case ReturnCode of 115 | 200 -> ok; 116 | ErrCode -> throw({error, {ErrCode, Message}}) 117 | end. 118 | 119 | %%----------------------------------------------------------------------------- 120 | %% HTTP helpers 121 | 122 | to_body(Json, Boundary) -> 123 | iolist_to_binary(["--", Boundary, "\r\n", 124 | "Content-Disposition: form-data; name=\"json_file\"; " 125 | "filename=\"json_file.json\" \r\n" 126 | "Content-Type: application/json\r\n\r\n", 127 | Json, "\r\n", "--", Boundary, "--", "\r\n"]). 128 | 129 | %%----------------------------------------------------------------------------- 130 | %% Callback mockery 131 | 132 | import(#s{importer=F}, File) -> F(File). 133 | 134 | imported_modules(#s{module_lister=F}) -> F(). 135 | 136 | analyze(#s{analyser=F}, Mod) -> F(Mod, calls, line). 137 | 138 | compile_info(#s{mod_info=F}, Mod) -> F(Mod). 139 | 140 | -ifdef(TEST). 141 | module_info_compile(Mod) -> Mod:module_info(compile). 142 | -else. 143 | module_info_compile(Mod) -> 144 | code:load_file(Mod), 145 | case code:is_loaded(Mod) of 146 | {file, _} -> Mod:module_info(compile); 147 | _ -> [] 148 | end. 149 | -endif. 150 | 151 | read_file(#s{file_reader=_F}, "") -> {ok, <<"">>}; 152 | read_file(#s{file_reader=F}, SrcFile) -> F(SrcFile). 153 | 154 | start_wrapper(Funs) -> 155 | fun() -> 156 | lists:foreach(fun(F) -> ok = wrap_start(F) end, Funs) 157 | end. 158 | 159 | wrap_start(StartFun) -> 160 | case StartFun() of 161 | {error,{already_started,_}} -> ok; 162 | ok -> ok 163 | end. 164 | 165 | digit(I) when I < 10 -> <<($0 + I):8>>; 166 | digit(I) -> <<($a -10 + I):8>>. 167 | 168 | hex(<<>>) -> 169 | <<>>; 170 | hex(<>) -> 171 | <<(digit(I))/binary, (hex(R))/binary>>. 172 | 173 | %%----------------------------------------------------------------------------- 174 | %% Converting modules 175 | 176 | convert_modules(S) -> 177 | F = fun(Mod, L) -> convert_module(Mod, S, L) end, 178 | lists:foldr(F, [], imported_modules(S)). 179 | 180 | convert_module(Mod, S, L) -> 181 | {ok, CoveredLines0} = analyze(S, Mod), 182 | %% Remove strange 0 indexed line 183 | FilterF = fun({{_, X}, _}) -> X =/= 0 end, 184 | CoveredLines = lists:filter(FilterF, CoveredLines0), 185 | case proplists:get_value(source, compile_info(S, Mod), "") of 186 | "" -> L; 187 | SrcFile -> 188 | {ok, SrcBin} = read_file(S, SrcFile), 189 | Src0 = lists:flatten(io_lib:format("~s", [SrcBin])), 190 | SrcDigest = erlang:md5(SrcBin), 191 | LinesCount = count_lines(Src0), 192 | Cov = create_cov(CoveredLines, LinesCount), 193 | [#{name => unicode:characters_to_binary(relative_to_cwd(SrcFile), utf8, utf8), 194 | source_digest => hex(SrcDigest), 195 | coverage => Cov} 196 | | L] 197 | end. 198 | 199 | expand(Path) -> expand(filename:split(Path), []). 200 | 201 | expand([], Acc) -> filename:join(lists:reverse(Acc)); 202 | expand(["."|Tail], Acc) -> expand(Tail, Acc); 203 | expand([".."|Tail], []) -> expand(Tail, []); 204 | expand([".."|Tail], [_|Acc]) -> expand(Tail, Acc); 205 | expand([Segment|Tail], Acc) -> expand(Tail, [Segment|Acc]). 206 | 207 | realpath(Path) -> realpath(filename:split(Path), "./"). 208 | 209 | realpath([], Acc) -> filename:absname(expand(Acc)); 210 | realpath([Head | Tail], Acc) -> 211 | NewAcc0 = filename:join([Acc, Head]), 212 | NewAcc = case file:read_link(NewAcc0) of 213 | {ok, Link} -> 214 | case filename:pathtype(Link) of 215 | absolute -> realpath(Link); 216 | relative -> filename:join([Acc, Link]) 217 | end; 218 | _ -> NewAcc0 219 | end, 220 | realpath(Tail, NewAcc). 221 | 222 | relative_to_cwd(Path) -> 223 | case file:get_cwd() of 224 | {ok, Base} -> relative_to(Path, Base); 225 | _ -> Path 226 | end. 227 | 228 | relative_to(Path, From) -> 229 | Path1 = realpath(Path), 230 | relative_to(filename:split(Path1), filename:split(From), Path). 231 | 232 | relative_to([H|T1], [H|T2], Original) -> relative_to(T1, T2, Original); 233 | relative_to([_|_] = L1, [], _Original) -> filename:join(L1); 234 | relative_to(_, _, Original) -> Original. 235 | 236 | create_cov(_CoveredLines, []) -> 237 | []; 238 | create_cov(CoveredLines, LinesCount) when is_integer(LinesCount) -> 239 | create_cov(CoveredLines, lists:seq(1, LinesCount)); 240 | create_cov([{{_,LineNo},Count}|CoveredLines], [LineNo|LineNos]) -> 241 | [Count | create_cov(CoveredLines, LineNos)]; 242 | create_cov(CoveredLines, [_|LineNos]) -> 243 | [null | create_cov(CoveredLines, LineNos)]. 244 | 245 | %%----------------------------------------------------------------------------- 246 | %% Generic helpers 247 | 248 | count_lines("") -> 1; 249 | count_lines("\n") -> 1; 250 | count_lines([$\n|S]) -> 1 + count_lines(S); 251 | count_lines([_|S]) -> count_lines(S). 252 | 253 | %%============================================================================= 254 | %% Tests 255 | 256 | -ifdef(TEST). 257 | -define(DEBUG, true). 258 | -include_lib("eunit/include/eunit.hrl"). 259 | 260 | normalize_json_str(Str) when is_binary(Str) -> 261 | jsx:encode(jsx:decode(Str, [return_maps, {labels, existing_atom}])); 262 | normalize_json_str(Str) when is_list(Str) -> 263 | normalize_json_str(iolist_to_binary(Str)). 264 | 265 | convert_file_test() -> 266 | Expected = 267 | jsx:decode( 268 | <<"{\"service_job_id\": \"1234567890\"," 269 | " \"service_name\": \"travis-ci\"," 270 | " \"source_files\": [" 271 | " {\"name\": \"example.rb\"," 272 | " \"source_digest\": \"3feb892deff06e7accbe2457eec4cd8b\"," 273 | " \"coverage\": [null,1,null]" 274 | " }," 275 | " {\"name\": \"two.rb\"," 276 | " \"source_digest\": \"fce46ee19702bd262b2e4907a005aff4\"," 277 | " \"coverage\": [null,1,0,null]" 278 | " }" 279 | " ]" 280 | "}">>, [return_maps, {labels, existing_atom}]), 281 | Report = #{service_job_id => <<"1234567890">>, 282 | service_name => <<"travis-ci">>}, 283 | Got = jsx:decode( 284 | convert_file("example.rb", Report, mock_s()), 285 | [return_maps, {labels, existing_atom}]), 286 | ?assertEqual(Expected, Got). 287 | 288 | convert_and_send_file_test() -> 289 | Expected = 290 | normalize_json_str( 291 | "{\"service_job_id\": \"1234567890\"," 292 | " \"service_name\": \"travis-ci\"," 293 | " \"source_files\": [" 294 | " {\"name\": \"example.rb\"," 295 | " \"source_digest\": \"3feb892deff06e7accbe2457eec4cd8b\"," 296 | " \"coverage\": [null,1,null]" 297 | " }," 298 | " {\"name\": \"two.rb\"," 299 | " \"source_digest\": \"fce46ee19702bd262b2e4907a005aff4\"," 300 | " \"coverage\": [null,1,0,null]" 301 | " }" 302 | " ]" 303 | "}"), 304 | Report = #{service_job_id => <<"1234567890">>, 305 | service_name => <<"travis-ci">>}, 306 | ?assertEqual(ok, convert_and_send_file("example.rb", Report, mock_s(Expected))). 307 | 308 | send_test_() -> 309 | Expected = 310 | normalize_json_str( 311 | "{\"service_job_id\": \"1234567890\",\n" 312 | " \"service_name\": \"travis-ci\",\n" 313 | " \"source_files\": [\n" 314 | " {\"name\": \"example.rb\",\n" 315 | " \"source_digest\": \"\tdef four\\n 4\\nend\",\n" 316 | " \"coverage\": [null,1,null]\n" 317 | " }" 318 | " ]" 319 | "}"), 320 | [ ?_assertEqual(ok, send(Expected, mock_s(Expected))) 321 | , ?_assertThrow({error, {_,_}}, send("foo", mock_s(<<"bar">>))) 322 | ]. 323 | 324 | %%----------------------------------------------------------------------------- 325 | %% Generic helpers tests 326 | 327 | count_lines_test_() -> 328 | [ ?_assertEqual(1, count_lines("")) 329 | , ?_assertEqual(1, count_lines("foo")) 330 | , ?_assertEqual(1, count_lines("bar\n")) 331 | , ?_assertEqual(2, count_lines("foo\nbar")) 332 | , ?_assertEqual(3, count_lines("foo\n\nbar")) 333 | , ?_assertEqual(2, count_lines("foo\nbar\n")) 334 | ]. 335 | 336 | expand_test_() -> 337 | [ ?_assertEqual("/a/b", expand(["/", "a", "b"], [])) 338 | , ?_assertEqual("a/c" , expand(["a", "b", "..", ".", "c"], [])) 339 | , ?_assertEqual("/" , expand(["..", ".", "/"], [])) 340 | ]. 341 | 342 | realpath_and_relative_test_() -> 343 | {setup, 344 | fun() -> %% setup 345 | {ok, Cwd} = file:get_cwd(), 346 | Root = string:strip( 347 | os:cmd("mktemp -d -t coveralls_tests.XXX"), right, $\n), 348 | ok = file:set_cwd(Root), 349 | {Cwd, Root} 350 | end, 351 | fun({Cwd, _Root}) -> %% teardown 352 | ok = file:set_cwd(Cwd) 353 | end, 354 | fun({_Cwd, Root}) -> %% tests 355 | Filename = "file", 356 | Dir1 = filename:join([Root, "_test_src", "dir1"]), 357 | Dir2 = filename:join([Root, "_test_src", "dir2"]), 358 | File1 = filename:join([Dir1, Filename]), 359 | File2 = filename:join([Dir2, Filename]), 360 | Link1 = filename:join([ Root 361 | , "_test_build" 362 | , "default" 363 | , "lib" 364 | , "mylib" 365 | , "src" 366 | , "dir1" 367 | ]), 368 | Link2 = filename:join([ Root 369 | , "_test_build" 370 | , "default" 371 | , "lib" 372 | , "mylib" 373 | , "src" 374 | , "dir2" 375 | ]), 376 | [ ?_assertEqual(ok, 377 | filelib:ensure_dir(filename:join([Dir1, "dummy"]))) 378 | , ?_assertEqual(ok, 379 | filelib:ensure_dir(filename:join([Dir2, "dummy"]))) 380 | , ?_assertEqual(ok, 381 | file:write_file(File1, "data")) 382 | , ?_assertEqual(ok, 383 | file:write_file(File2, "data")) 384 | , ?_assertEqual(ok, 385 | filelib:ensure_dir(Link1)) 386 | , ?_assertEqual(ok, 387 | filelib:ensure_dir(Link2)) 388 | , ?_assertEqual(ok, 389 | file:make_symlink(Dir1, Link1)) 390 | , ?_assertEqual(ok, 391 | file:make_symlink(filename:join([ ".." 392 | , ".." 393 | , ".." 394 | , ".." 395 | , ".." 396 | , "_test_src" 397 | , "dir2" 398 | ]) 399 | , Link2)) 400 | , ?_assertEqual(realpath(File1), 401 | realpath(filename:join([Link1, Filename]))) 402 | , ?_assertEqual(realpath(File2), 403 | realpath(filename:join([Link2, Filename]))) 404 | , ?_assertEqual(realpath(File1), 405 | filename:absname( 406 | relative_to_cwd( 407 | filename:join([Link1, Filename])))) 408 | , ?_assertEqual(realpath(File2), 409 | filename:absname( 410 | relative_to_cwd( 411 | filename:join([Link2, Filename])))) 412 | ] 413 | end}. 414 | 415 | %%----------------------------------------------------------------------------- 416 | %% Callback mockery tests 417 | module_info_compile_test() -> 418 | ?assert(is_tuple(lists:keyfind(source, 1, module_info_compile(?MODULE)))). 419 | 420 | start_wrapper_test_() -> 421 | F = fun() -> ok end, 422 | StartedF = fun() -> {error,{already_started,mod}} end, 423 | ErrorF = fun() -> {error, {error, mod}} end, 424 | [ ?_assertEqual(ok, (start_wrapper([F, StartedF]))()) 425 | , ?_assertError(_, (start_wrapper([F, StartedF, ErrorF]))()) 426 | ]. 427 | 428 | %%----------------------------------------------------------------------------- 429 | %% Converting modules tests 430 | 431 | create_cov_test() -> 432 | ?assertEqual([null, 3, null, 4, null], 433 | create_cov([{{foo, 2}, 3}, {{foo, 4}, 4}], 5)). 434 | 435 | convert_module_test() -> 436 | Expected = 437 | [#{name => <<"example.rb">>, 438 | source_digest => <<"3feb892deff06e7accbe2457eec4cd8b">>, 439 | coverage => [null,1,null]}], 440 | ?assertEqual(Expected, convert_module('example.rb', mock_s(), [])). 441 | 442 | convert_modules_test() -> 443 | Expected = 444 | [#{name => <<"example.rb">>, 445 | source_digest => <<"3feb892deff06e7accbe2457eec4cd8b">>, 446 | coverage => [null,1,null] 447 | }, 448 | #{name => <<"two.rb">>, 449 | source_digest => <<"fce46ee19702bd262b2e4907a005aff4">>, 450 | coverage => [null,1,0,null] 451 | }], 452 | ?assertEqual(Expected, 453 | convert_modules(mock_s())). 454 | 455 | %%----------------------------------------------------------------------------- 456 | %% Setup helpers 457 | 458 | mock_s() -> mock_s(""). 459 | 460 | mock_s(Json) -> 461 | #s{ importer = 462 | fun(_) -> ok end 463 | , module_lister = 464 | fun() -> ['example.rb', 'two.rb'] end 465 | , mod_info = 466 | fun('example.rb') -> [{source,"example.rb"}]; 467 | ('two.rb') -> [{source,"two.rb"}] 468 | end 469 | , file_reader = 470 | fun("example.rb") -> 471 | {ok, <<"def four\n 4\nend">>}; 472 | ("two.rb") -> 473 | {ok, <<"def seven\n eight\n nine\nend">>} 474 | end 475 | , wildcard_reader = fun(AnyFile) -> [AnyFile] end 476 | , analyser = 477 | fun('example.rb' , calls, line) -> {ok, [ {{'example.rb', 2}, 1} ]}; 478 | ('two.rb' , calls, line) -> {ok, [ {{'two.rb', 2}, 1} 479 | , {{'two.rb', 3}, 0} 480 | ] 481 | } 482 | end 483 | , poster_init = 484 | fun() -> ok end 485 | , poster = 486 | fun(post, {_, _, _, Body}, _, _) -> 487 | case binary:match(Body, Json) =/= nomatch of 488 | true -> {ok, {{"", 200, ""}, "", ""}}; 489 | false -> {ok, {{"", 666, ""}, "", "Not expected"}} 490 | end 491 | end 492 | }. 493 | 494 | -endif. 495 | 496 | %%% Local Variables: 497 | %%% allout-layout: t 498 | %%% erlang-indent-level: 2 499 | %%% End: 500 | -------------------------------------------------------------------------------- /src/rebar3_coveralls.erl: -------------------------------------------------------------------------------- 1 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 2 | %%% Copyright (c) 2013-2016, Markus Ekholm 3 | %%% All rights reserved. 4 | %%% Redistribution and use in source and binary forms, with or without 5 | %%% modification, are permitted provided that the following conditions are met: 6 | %%% * Redistributions of source code must retain the above copyright 7 | %%% notice, this list of conditions and the following disclaimer. 8 | %%% * Redistributions in binary form must reproduce the above copyright 9 | %%% notice, this list of conditions and the following disclaimer in the 10 | %%% documentation and/or other materials provided with the distribution. 11 | %%% * Neither the name of the nor the 12 | %%% names of its contributors may be used to endorse or promote products 13 | %%% derived from this software without specific prior written permission. 14 | %%% 15 | %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | %%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | %%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 18 | %%% ARE DISCLAIMED. IN NO EVENT SHALL MARKUS EKHOLM BE LIABLE FOR ANY 19 | %%% DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | %%% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | %%% 26 | %%% @copyright 2013-2016 (c) Yury Gargay , 27 | %%% Markus Ekholm 28 | %%% @end 29 | %%% @author Yury Gargay 30 | %%% @author Markus Ekholm 31 | %%% @doc coveralls plugin for rebar3 32 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 33 | 34 | -module(rebar3_coveralls). 35 | -behaviour(provider). 36 | 37 | -export([ init/1 38 | , do/1 39 | , format_error/1 40 | ]). 41 | 42 | -define(PROVIDER, send). 43 | -define(DEPS, [{default, app_discovery}]). 44 | 45 | %% =================================================================== 46 | %% Public API 47 | %% =================================================================== 48 | -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. 49 | init(State) -> 50 | Provider = providers:create([ {name, ?PROVIDER} 51 | , {module, ?MODULE} 52 | , {namespace, coveralls} 53 | , {bare, true} 54 | , {deps, ?DEPS} 55 | , {example, "rebar3 coveralls send"} 56 | , {short_desc, "Send coverdata to coveralls."} 57 | , {desc, "Send coveralls to coveralls."} 58 | , {opts, []} 59 | ]), 60 | {ok, rebar_state:add_provider(State, Provider)}. 61 | 62 | -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. 63 | do(State) -> 64 | rebar_api:info("Running coveralls...", []), 65 | ConvertAndSend = fun coveralls:convert_and_send_file/2, 66 | Get = fun(Key, Def) -> rebar_state:get(State, Key, Def) end, 67 | GetLocal = fun(Key, Def) -> rebar_state:get(State, Key, Def) end, 68 | MaybeSkip = fun() -> ok end, 69 | ok = cover_paths(State), 70 | try 71 | do_coveralls(ConvertAndSend, 72 | Get, 73 | GetLocal, 74 | MaybeSkip, 75 | 'send-coveralls'), 76 | {ok, State} 77 | catch throw:{error, {ErrCode, Msg}} -> 78 | io:format("Failed sending coverdata to coveralls, ~p: ~p", 79 | [ErrCode, Msg]), 80 | {error, rebar_abort} 81 | end. 82 | 83 | -spec format_error(any()) -> iolist(). 84 | format_error(Reason) -> 85 | io_lib:format("~p", [Reason]). 86 | 87 | cover_paths(State) -> 88 | lists:foreach(fun(App) -> 89 | AppDir = rebar_app_info:out_dir(App), 90 | true = code:add_patha(filename:join([AppDir, "ebin"])), 91 | _ = code:add_patha(filename:join([AppDir, "test"])) 92 | end, 93 | rebar_state:project_apps(State)), 94 | _ = code:add_patha(filename:join([rebar_dir:base_dir(State), "test"])), 95 | ok. 96 | 97 | %%============================================================================= 98 | %% Internal functions 99 | 100 | to_binary(List) when is_list(List) -> 101 | unicode:characters_to_binary(List, utf8, utf8); 102 | to_binary(Atom) when is_atom(Atom) -> 103 | atom_to_binary(Atom, utf8); 104 | to_binary(Bin) when is_binary(Bin) -> 105 | Bin. 106 | to_boolean(true) -> true; 107 | to_boolean(1) -> true; 108 | to_boolean(_) -> false. 109 | 110 | do_coveralls(ConvertAndSend, Get, GetLocal, MaybeSkip, Task) -> 111 | File = GetLocal(coveralls_coverdata, undef), 112 | ServiceName = to_binary(GetLocal(coveralls_service_name, undef)), 113 | ServiceJobId = to_binary(GetLocal(coveralls_service_job_id, undef)), 114 | F = fun(X) -> X =:= undef orelse X =:= false end, 115 | CoverExport = Get(cover_export_enabled, false), 116 | case lists:any(F, [File, ServiceName, ServiceJobId, CoverExport]) of 117 | true -> 118 | throw({error, 119 | "need to specify coveralls_* and cover_export_enabled " 120 | "in rebar.config"}); 121 | false -> 122 | ok 123 | end, 124 | 125 | Report0 = 126 | #{service_job_id => ServiceJobId, 127 | service_name => ServiceName}, 128 | Opts = [{coveralls_repo_token, repo_token, string}, 129 | {coveralls_service_pull_request, service_pull_request, string}, 130 | {coveralls_commit_sha, commit_sha, string}, 131 | {coveralls_service_number, service_number, string}, 132 | {coveralls_parallel, parallel, boolean}], 133 | Report = 134 | lists:foldl(fun({Cfg, Key, Conv}, R) -> 135 | case GetLocal(Cfg, undef) of 136 | undef -> R; 137 | Value when Conv =:= string -> maps:put(Key, to_binary(Value), R); 138 | Value when Conv =:= boolean -> maps:put(Key, to_boolean(Value), R); 139 | Value -> maps:put(Key, Value, R) 140 | end 141 | end, Report0, Opts), 142 | 143 | DoCoveralls = (GetLocal(do_coveralls_after_ct, true) andalso Task == ct) 144 | orelse (GetLocal(do_coveralls_after_eunit, true) andalso Task == eunit) 145 | orelse Task == 'send-coveralls', 146 | case DoCoveralls of 147 | true -> 148 | io:format("rebar_coveralls:" 149 | "Exporting cover data " 150 | "from ~s using service ~s and jobid ~s~n", 151 | [File, ServiceName, ServiceJobId]), 152 | ok = ConvertAndSend(File, Report); 153 | _ -> MaybeSkip() 154 | end. 155 | 156 | 157 | %%============================================================================= 158 | %% Tests 159 | 160 | -ifdef(TEST). 161 | -include_lib("eunit/include/eunit.hrl"). 162 | 163 | task_test_() -> 164 | File = "foo", 165 | ServiceJobId = "123", 166 | ServiceName = "bar", 167 | ConvertAndSend = fun("foo", #{service_job_id := <<"123">>, 168 | service_name := <<"bar">>}) -> ok end, 169 | ConvertWithOpts = fun("foo", #{service_job_id := <<"123">>, 170 | service_name := <<"bar">>, 171 | service_pull_request := <<"PR#1">>, 172 | parallel := true}) -> ok 173 | end, 174 | Get = fun(cover_export_enabled, _) -> true end, 175 | GetLocal = fun(coveralls_coverdata, _) -> File; 176 | (coveralls_service_name, _) -> ServiceName; 177 | (coveralls_service_job_id, _) -> ServiceJobId; 178 | (do_coveralls_after_eunit, _) -> true; 179 | (do_coveralls_after_ct, _) -> true; 180 | (coveralls_repo_token, _) -> []; 181 | (_, Default) -> Default 182 | end, 183 | GetLocalAllOpt = fun(coveralls_coverdata, _) -> File; 184 | (coveralls_service_name, _) -> ServiceName; 185 | (coveralls_service_job_id, _) -> ServiceJobId; 186 | (coveralls_service_pull_request, _) -> "PR#1"; 187 | (coveralls_parallel, _) -> true; 188 | (do_coveralls_after_eunit, _) -> true; 189 | (do_coveralls_after_ct, _) -> true; 190 | (coveralls_repo_token, _) -> []; 191 | (_, Default) -> Default 192 | end, 193 | GetLocalWithCoverallsTask 194 | = fun(coveralls_coverdata, _) -> File; 195 | (coveralls_service_name, _) -> ServiceName; 196 | (coveralls_service_job_id, _) -> ServiceJobId; 197 | (do_coveralls_after_eunit, _) -> false; 198 | (do_coveralls_after_ct, _) -> false; 199 | (coveralls_repo_token, _) -> []; 200 | (_, Default) -> Default 201 | end, 202 | GetBroken = fun(cover_export_enabled, _) -> false end, 203 | MaybeSkip = fun() -> skip end, 204 | [ ?_assertEqual(ok, do_coveralls(ConvertAndSend, Get, GetLocal, MaybeSkip, eunit)) 205 | , ?_assertEqual(ok, do_coveralls(ConvertAndSend, Get, GetLocal, MaybeSkip, ct)) 206 | , ?_assertThrow({error, _}, do_coveralls(ConvertAndSend, GetBroken, GetLocal, MaybeSkip, eunit)) 207 | , ?_assertThrow({error, _}, do_coveralls(ConvertAndSend, GetBroken, GetLocal, MaybeSkip, ct)) 208 | , ?_assertEqual(skip, do_coveralls(ConvertAndSend, Get, GetLocalWithCoverallsTask, MaybeSkip, eunit)) 209 | , ?_assertEqual(skip, do_coveralls(ConvertAndSend, Get, GetLocalWithCoverallsTask, MaybeSkip, ct)) 210 | , ?_assertEqual(ok, do_coveralls(ConvertAndSend, Get, GetLocalWithCoverallsTask, MaybeSkip, 'send-coveralls')) 211 | , ?_assertEqual(ok, do_coveralls(ConvertWithOpts, Get, GetLocalAllOpt, MaybeSkip, eunit)) 212 | , ?_assertEqual(ok, do_coveralls(ConvertWithOpts, Get, GetLocalAllOpt, MaybeSkip, ct)) 213 | ]. 214 | 215 | -endif. 216 | 217 | %%% Local Variables: 218 | %%% allout-layout: t 219 | %%% erlang-indent-level: 2 220 | %%% End: 221 | --------------------------------------------------------------------------------