diff options
Diffstat (limited to 'server/_build/default/plugins/coveralls/src/coveralls.erl')
-rw-r--r-- | server/_build/default/plugins/coveralls/src/coveralls.erl | 499 |
1 files changed, 499 insertions, 0 deletions
diff --git a/server/_build/default/plugins/coveralls/src/coveralls.erl b/server/_build/default/plugins/coveralls/src/coveralls.erl new file mode 100644 index 0000000..90954c6 --- /dev/null +++ b/server/_build/default/plugins/coveralls/src/coveralls.erl @@ -0,0 +1,499 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%% Copyright (c) 2013-2016, Markus Ekholm +%%% All rights reserved. +%%% Redistribution and use in source and binary forms, with or without +%%% modification, are permitted provided that the following conditions are met: +%%% * Redistributions of source code must retain the above copyright +%%% notice, this list of conditions and the following disclaimer. +%%% * Redistributions in binary form must reproduce the above copyright +%%% notice, this list of conditions and the following disclaimer in the +%%% documentation and/or other materials provided with the distribution. +%%% * Neither the name of the <organization> nor the +%%% names of its contributors may be used to endorse or promote products +%%% derived from this software without specific prior written permission. +%%% +%%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +%%% ARE DISCLAIMED. IN NO EVENT SHALL MARKUS EKHOLM BE LIABLE FOR ANY +%%% DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +%%% THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +%%% +%%% @copyright 2013-2016 (c) Markus Ekholm <markus@botten.org> +%%% @author Markus Ekholm <markus@botten.org> +%%% @doc coveralls +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%============================================================================= +%% Module declaration + +-module(coveralls). + +%%============================================================================= +%% Exports + +-export([ convert_file/2 + , convert_and_send_file/2 + ]). + +%%============================================================================= +%% Records + +-record(s, { importer = fun cover:import/1 + , module_lister = fun cover:imported_modules/0 + , mod_info = fun module_info_compile/1 + , file_reader = fun file:read_file/1 + , wildcard_reader = fun filelib:wildcard/1 + , analyser = fun cover:analyse/3 + , poster = fun httpc:request/4 + , poster_init = start_wrapper([fun ssl:start/0, fun inets:start/0]) + }). + +%%============================================================================= +%% Defines + +-define(COVERALLS_URL, "https://coveralls.io/api/v1/jobs"). +%%-define(COVERALLS_URL, "http://127.0.0.1:8080"). + +-ifdef(random_only). +-define(random, random). +-else. +-define(random, rand). +-endif. + +%%============================================================================= +%% API functions + +%% @doc Import and convert cover file(s) `Filenames' to a json string +%% representation suitable to post to coveralls. +%% +%% Note that this function will crash if the modules mentioned in +%% any of the `Filenames' are not availabe on the node. +%% @end +-spec convert_file(string() | [string()], map()) -> + string(). +convert_file(Filenames, Report) -> + convert_file(Filenames, Report, #s{}). + +%% @doc Import and convert cover files `Filenames' to a json string and send the +%% json to coveralls. +%% @end +-spec convert_and_send_file(string() | [string()], map()) -> ok. +convert_and_send_file(Filenames, Report) -> + convert_and_send_file(Filenames, Report, #s{}). + +%%============================================================================= +%% Internal functions + +convert_file([L|_]=Filename, Report, S) when is_integer(L) -> + %% single file or wildcard was specified + WildcardReader = S#s.wildcard_reader, + Filenames = WildcardReader(Filename), + convert_file(Filenames, Report, S); +convert_file([[_|_]|_]=Filenames, Report, S) -> + ok = lists:foreach( + fun(Filename) -> ok = import(S, Filename) end, + Filenames), + ConvertedModules = convert_modules(S), + jsx:encode(Report#{source_files => ConvertedModules}, []). + +convert_and_send_file(Filenames, Report, S) -> + send(convert_file(Filenames, Report, S), S). + +send(Json, #s{poster=Poster, poster_init=Init}) -> + ok = Init(), + Boundary = ["----------", integer_to_list(?random:uniform(1000))], + Type = "multipart/form-data; boundary=" ++ Boundary, + Body = to_body(Json, Boundary), + R = Poster(post, {?COVERALLS_URL, [], Type, Body}, [], []), + {ok, {{_, ReturnCode, _}, _, Message}} = R, + case ReturnCode of + 200 -> ok; + ErrCode -> throw({error, {ErrCode, Message}}) + end. + +%%----------------------------------------------------------------------------- +%% HTTP helpers + +to_body(Json, Boundary) -> + iolist_to_binary(["--", Boundary, "\r\n", + "Content-Disposition: form-data; name=\"json_file\"; " + "filename=\"json_file.json\" \r\n" + "Content-Type: application/json\r\n\r\n", + Json, "\r\n", "--", Boundary, "--", "\r\n"]). + +%%----------------------------------------------------------------------------- +%% Callback mockery + +import(#s{importer=F}, File) -> F(File). + +imported_modules(#s{module_lister=F}) -> F(). + +analyze(#s{analyser=F}, Mod) -> F(Mod, calls, line). + +compile_info(#s{mod_info=F}, Mod) -> F(Mod). + +-ifdef(TEST). +module_info_compile(Mod) -> Mod:module_info(compile). +-else. +module_info_compile(Mod) -> + code:load_file(Mod), + case code:is_loaded(Mod) of + {file, _} -> Mod:module_info(compile); + _ -> [] + end. +-endif. + +read_file(#s{file_reader=_F}, "") -> {ok, <<"">>}; +read_file(#s{file_reader=F}, SrcFile) -> F(SrcFile). + +start_wrapper(Funs) -> + fun() -> + lists:foreach(fun(F) -> ok = wrap_start(F) end, Funs) + end. + +wrap_start(StartFun) -> + case StartFun() of + {error,{already_started,_}} -> ok; + ok -> ok + end. + +digit(I) when I < 10 -> <<($0 + I):8>>; +digit(I) -> <<($a -10 + I):8>>. + +hex(<<>>) -> + <<>>; +hex(<<I:4, R/bitstring>>) -> + <<(digit(I))/binary, (hex(R))/binary>>. + +%%----------------------------------------------------------------------------- +%% Converting modules + +convert_modules(S) -> + F = fun(Mod, L) -> convert_module(Mod, S, L) end, + lists:foldr(F, [], imported_modules(S)). + +convert_module(Mod, S, L) -> + {ok, CoveredLines0} = analyze(S, Mod), + %% Remove strange 0 indexed line + FilterF = fun({{_, X}, _}) -> X =/= 0 end, + CoveredLines = lists:filter(FilterF, CoveredLines0), + case proplists:get_value(source, compile_info(S, Mod), "") of + "" -> L; + SrcFile -> + {ok, SrcBin} = read_file(S, SrcFile), + Src0 = lists:flatten(io_lib:format("~s", [SrcBin])), + SrcDigest = erlang:md5(SrcBin), + LinesCount = count_lines(Src0), + Cov = create_cov(CoveredLines, LinesCount), + [#{name => unicode:characters_to_binary(relative_to_cwd(SrcFile), utf8, utf8), + source_digest => hex(SrcDigest), + coverage => Cov} + | L] + end. + +expand(Path) -> expand(filename:split(Path), []). + +expand([], Acc) -> filename:join(lists:reverse(Acc)); +expand(["."|Tail], Acc) -> expand(Tail, Acc); +expand([".."|Tail], []) -> expand(Tail, []); +expand([".."|Tail], [_|Acc]) -> expand(Tail, Acc); +expand([Segment|Tail], Acc) -> expand(Tail, [Segment|Acc]). + +realpath(Path) -> realpath(filename:split(Path), "./"). + +realpath([], Acc) -> filename:absname(expand(Acc)); +realpath([Head | Tail], Acc) -> + NewAcc0 = filename:join([Acc, Head]), + NewAcc = case file:read_link(NewAcc0) of + {ok, Link} -> + case filename:pathtype(Link) of + absolute -> realpath(Link); + relative -> filename:join([Acc, Link]) + end; + _ -> NewAcc0 + end, + realpath(Tail, NewAcc). + +relative_to_cwd(Path) -> + case file:get_cwd() of + {ok, Base} -> relative_to(Path, Base); + _ -> Path + end. + +relative_to(Path, From) -> + Path1 = realpath(Path), + relative_to(filename:split(Path1), filename:split(From), Path). + +relative_to([H|T1], [H|T2], Original) -> relative_to(T1, T2, Original); +relative_to([_|_] = L1, [], _Original) -> filename:join(L1); +relative_to(_, _, Original) -> Original. + +create_cov(_CoveredLines, []) -> + []; +create_cov(CoveredLines, LinesCount) when is_integer(LinesCount) -> + create_cov(CoveredLines, lists:seq(1, LinesCount)); +create_cov([{{_,LineNo},Count}|CoveredLines], [LineNo|LineNos]) -> + [Count | create_cov(CoveredLines, LineNos)]; +create_cov(CoveredLines, [_|LineNos]) -> + [null | create_cov(CoveredLines, LineNos)]. + +%%----------------------------------------------------------------------------- +%% Generic helpers + +count_lines("") -> 1; +count_lines("\n") -> 1; +count_lines([$\n|S]) -> 1 + count_lines(S); +count_lines([_|S]) -> count_lines(S). + +%%============================================================================= +%% Tests + +-ifdef(TEST). +-define(DEBUG, true). +-include_lib("eunit/include/eunit.hrl"). + +normalize_json_str(Str) when is_binary(Str) -> + jsx:encode(jsx:decode(Str, [return_maps, {labels, existing_atom}])); +normalize_json_str(Str) when is_list(Str) -> + normalize_json_str(iolist_to_binary(Str)). + +convert_file_test() -> + Expected = + jsx:decode( + <<"{\"service_job_id\": \"1234567890\"," + " \"service_name\": \"travis-ci\"," + " \"source_files\": [" + " {\"name\": \"example.rb\"," + " \"source_digest\": \"3feb892deff06e7accbe2457eec4cd8b\"," + " \"coverage\": [null,1,null]" + " }," + " {\"name\": \"two.rb\"," + " \"source_digest\": \"fce46ee19702bd262b2e4907a005aff4\"," + " \"coverage\": [null,1,0,null]" + " }" + " ]" + "}">>, [return_maps, {labels, existing_atom}]), + Report = #{service_job_id => <<"1234567890">>, + service_name => <<"travis-ci">>}, + Got = jsx:decode( + convert_file("example.rb", Report, mock_s()), + [return_maps, {labels, existing_atom}]), + ?assertEqual(Expected, Got). + +convert_and_send_file_test() -> + Expected = + normalize_json_str( + "{\"service_job_id\": \"1234567890\"," + " \"service_name\": \"travis-ci\"," + " \"source_files\": [" + " {\"name\": \"example.rb\"," + " \"source_digest\": \"3feb892deff06e7accbe2457eec4cd8b\"," + " \"coverage\": [null,1,null]" + " }," + " {\"name\": \"two.rb\"," + " \"source_digest\": \"fce46ee19702bd262b2e4907a005aff4\"," + " \"coverage\": [null,1,0,null]" + " }" + " ]" + "}"), + Report = #{service_job_id => <<"1234567890">>, + service_name => <<"travis-ci">>}, + ?assertEqual(ok, convert_and_send_file("example.rb", Report, mock_s(Expected))). + +send_test_() -> + Expected = + normalize_json_str( + "{\"service_job_id\": \"1234567890\",\n" + " \"service_name\": \"travis-ci\",\n" + " \"source_files\": [\n" + " {\"name\": \"example.rb\",\n" + " \"source_digest\": \"\tdef four\\n 4\\nend\",\n" + " \"coverage\": [null,1,null]\n" + " }" + " ]" + "}"), + [ ?_assertEqual(ok, send(Expected, mock_s(Expected))) + , ?_assertThrow({error, {_,_}}, send("foo", mock_s(<<"bar">>))) + ]. + +%%----------------------------------------------------------------------------- +%% Generic helpers tests + +count_lines_test_() -> + [ ?_assertEqual(1, count_lines("")) + , ?_assertEqual(1, count_lines("foo")) + , ?_assertEqual(1, count_lines("bar\n")) + , ?_assertEqual(2, count_lines("foo\nbar")) + , ?_assertEqual(3, count_lines("foo\n\nbar")) + , ?_assertEqual(2, count_lines("foo\nbar\n")) + ]. + +expand_test_() -> + [ ?_assertEqual("/a/b", expand(["/", "a", "b"], [])) + , ?_assertEqual("a/c" , expand(["a", "b", "..", ".", "c"], [])) + , ?_assertEqual("/" , expand(["..", ".", "/"], [])) + ]. + +realpath_and_relative_test_() -> + {setup, + fun() -> %% setup + {ok, Cwd} = file:get_cwd(), + Root = string:strip( + os:cmd("mktemp -d -t coveralls_tests.XXX"), right, $\n), + ok = file:set_cwd(Root), + {Cwd, Root} + end, + fun({Cwd, _Root}) -> %% teardown + ok = file:set_cwd(Cwd) + end, + fun({_Cwd, Root}) -> %% tests + Filename = "file", + Dir1 = filename:join([Root, "_test_src", "dir1"]), + Dir2 = filename:join([Root, "_test_src", "dir2"]), + File1 = filename:join([Dir1, Filename]), + File2 = filename:join([Dir2, Filename]), + Link1 = filename:join([ Root + , "_test_build" + , "default" + , "lib" + , "mylib" + , "src" + , "dir1" + ]), + Link2 = filename:join([ Root + , "_test_build" + , "default" + , "lib" + , "mylib" + , "src" + , "dir2" + ]), + [ ?_assertEqual(ok, + filelib:ensure_dir(filename:join([Dir1, "dummy"]))) + , ?_assertEqual(ok, + filelib:ensure_dir(filename:join([Dir2, "dummy"]))) + , ?_assertEqual(ok, + file:write_file(File1, "data")) + , ?_assertEqual(ok, + file:write_file(File2, "data")) + , ?_assertEqual(ok, + filelib:ensure_dir(Link1)) + , ?_assertEqual(ok, + filelib:ensure_dir(Link2)) + , ?_assertEqual(ok, + file:make_symlink(Dir1, Link1)) + , ?_assertEqual(ok, + file:make_symlink(filename:join([ ".." + , ".." + , ".." + , ".." + , ".." + , "_test_src" + , "dir2" + ]) + , Link2)) + , ?_assertEqual(realpath(File1), + realpath(filename:join([Link1, Filename]))) + , ?_assertEqual(realpath(File2), + realpath(filename:join([Link2, Filename]))) + , ?_assertEqual(realpath(File1), + filename:absname( + relative_to_cwd( + filename:join([Link1, Filename])))) + , ?_assertEqual(realpath(File2), + filename:absname( + relative_to_cwd( + filename:join([Link2, Filename])))) + ] + end}. + +%%----------------------------------------------------------------------------- +%% Callback mockery tests +module_info_compile_test() -> + ?assert(is_tuple(lists:keyfind(source, 1, module_info_compile(?MODULE)))). + +start_wrapper_test_() -> + F = fun() -> ok end, + StartedF = fun() -> {error,{already_started,mod}} end, + ErrorF = fun() -> {error, {error, mod}} end, + [ ?_assertEqual(ok, (start_wrapper([F, StartedF]))()) + , ?_assertError(_, (start_wrapper([F, StartedF, ErrorF]))()) + ]. + +%%----------------------------------------------------------------------------- +%% Converting modules tests + +create_cov_test() -> + ?assertEqual([null, 3, null, 4, null], + create_cov([{{foo, 2}, 3}, {{foo, 4}, 4}], 5)). + +convert_module_test() -> + Expected = + [#{name => <<"example.rb">>, + source_digest => <<"3feb892deff06e7accbe2457eec4cd8b">>, + coverage => [null,1,null]}], + ?assertEqual(Expected, convert_module('example.rb', mock_s(), [])). + +convert_modules_test() -> + Expected = + [#{name => <<"example.rb">>, + source_digest => <<"3feb892deff06e7accbe2457eec4cd8b">>, + coverage => [null,1,null] + }, + #{name => <<"two.rb">>, + source_digest => <<"fce46ee19702bd262b2e4907a005aff4">>, + coverage => [null,1,0,null] + }], + ?assertEqual(Expected, + convert_modules(mock_s())). + +%%----------------------------------------------------------------------------- +%% Setup helpers + +mock_s() -> mock_s(""). + +mock_s(Json) -> + #s{ importer = + fun(_) -> ok end + , module_lister = + fun() -> ['example.rb', 'two.rb'] end + , mod_info = + fun('example.rb') -> [{source,"example.rb"}]; + ('two.rb') -> [{source,"two.rb"}] + end + , file_reader = + fun("example.rb") -> + {ok, <<"def four\n 4\nend">>}; + ("two.rb") -> + {ok, <<"def seven\n eight\n nine\nend">>} + end + , wildcard_reader = fun(AnyFile) -> [AnyFile] end + , analyser = + fun('example.rb' , calls, line) -> {ok, [ {{'example.rb', 2}, 1} ]}; + ('two.rb' , calls, line) -> {ok, [ {{'two.rb', 2}, 1} + , {{'two.rb', 3}, 0} + ] + } + end + , poster_init = + fun() -> ok end + , poster = + fun(post, {_, _, _, Body}, _, _) -> + case binary:match(Body, Json) =/= nomatch of + true -> {ok, {{"", 200, ""}, "", ""}}; + false -> {ok, {{"", 666, ""}, "", "Not expected"}} + end + end + }. + +-endif. + +%%% Local Variables: +%%% allout-layout: t +%%% erlang-indent-level: 2 +%%% End: |