From 1ec8bda21c2b41b41c222d5fd316434d68a5acba Mon Sep 17 00:00:00 2001 From: Andrew Thompson Date: Wed, 22 Jan 2014 12:21:22 -0500 Subject: [PATCH] Add a simple 'smoke test' tool to run eunit/dialyzer on riak source trees --- .gitignore | 1 + Makefile | 1 + rebar.config.script | 8 ++ src/smoke_test_escript.erl | 214 +++++++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 rebar.config.script create mode 100755 src/smoke_test_escript.erl diff --git a/.gitignore b/.gitignore index ce1f3b6c..79d1ae60 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ deps ebin log riak_test +smoke_test .eunit .DS_Store out diff --git a/Makefile b/Makefile index 1528b0c0..72632010 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ PLT = $(HOME)/.riak-test_dialyzer_plt all: deps compile ./rebar skip_deps=true escriptize + SMOKE_TEST=1 ./rebar skip_deps=true escriptize deps: ./rebar get-deps diff --git a/rebar.config.script b/rebar.config.script new file mode 100644 index 00000000..0fcc5e0f --- /dev/null +++ b/rebar.config.script @@ -0,0 +1,8 @@ +case os:getenv("SMOKE_TEST") of + false -> CONFIG; + [] -> CONFIG; + _ -> + C1 = lists:keystore(escript_emu_args, 1, CONFIG, + {escript_emu_args, "%%! -escript main smoke_test_escript +K true +P 10000 -env ERL_MAX_PORTS 10000\n"}), + lists:keystore(escript_name, 1, C1, {escript_name, smoke_test}) +end. diff --git a/src/smoke_test_escript.erl b/src/smoke_test_escript.erl new file mode 100755 index 00000000..d508b5ac --- /dev/null +++ b/src/smoke_test_escript.erl @@ -0,0 +1,214 @@ +-module(smoke_test_escript). +-include_lib("kernel/include/file.hrl"). + +-export([main/1, get_version/0, worker/3]). + +get_version() -> + list_to_binary(string:strip(os:cmd("git describe"), right, $\n)). + +cli_options() -> +%% Option Name, Short Code, Long Code, Argument Spec, Help Message +[ + {project, $p, "project", string, "specifices which project"}, + {debug, $v, "debug", undefined, "debug?"}, + {directory, $d, "directory", string, "source tree directory"}, + {jobs, $j, "jobs", integer, "jobs?"} +]. + + +main(Args) -> + {ok, {Parsed, _Other}} = getopt:parse(cli_options(), Args), + application:start(ibrowse), + lager:start(), + rt_config:load("default", filename:join([os:getenv("HOME"), ".smoke_test.config"])), + case lists:keyfind(project, 1, Parsed) of + false -> + lager:error("Must specify project!"), + application:stop(lager), + halt(1); + {project, Project} -> + rt_config:set(rt_project, Project) + end, + case lists:keyfind(directory, 1, Parsed) of + false -> + %% run in current working directory + ok; + {directory, Dir} -> + lager:info("Changing working dir to ~s", [Dir]), + ok = file:set_cwd(filename:absname(Dir)) + end, + case lists:member(debug, Parsed) of + true -> + lager:set_loglevel(lager_console_backend, debug); + _ -> + ok + end, + rt_config:set(rt_harness, ?MODULE), + lager:debug("ParsedArgs ~p", [Parsed]), + Suites = giddyup:get_suite(rt_config:get(platform)), + Jobs = case lists:keyfind(jobs, 1, Parsed) of + false -> + 1; + {jobs, J} -> + J + end, + + {ok, PWD} = file:get_cwd(), + Rebar = filename:join(PWD, "rebar"), + + + setup_deps(Rebar, PWD, [filename:join([PWD, "deps", F]) + || F <- element(2, file:list_dir(filename:join(PWD, "deps"))), + filelib:is_dir(filename:join([PWD, "deps", F]))]), + + case Jobs > 1 of + true -> + %% partiton the suite list by the number of jobs + SplitSuites = dict:to_list(element(2, lists:foldl(fun(S, {Counter, Dict}) -> + {Counter + 1, dict:append(Counter rem Jobs, S, Dict)} + end, {0, dict:new()}, Suites))), + lager:debug("Split into ~p lists", [length(SplitSuites)]), + Workers = [spawn_monitor(?MODULE, worker, [Rebar, PWD, SS]) || {_, SS} <- SplitSuites], + wait_for_workers(Workers); + _ -> + worker(Rebar, PWD, Suites) + end. + +worker(Rebar, PWD, Suites) -> + lists:foreach(fun({Suite, Config}) -> + lager:info("Suite ~p config ~p", [Suite, Config]), + [Dep, Task] = string:tokens(atom_to_list(Suite), ":"), + FDep = filename:join([PWD, deps, Dep]), + case filelib:is_dir(FDep) of + true -> + case Task of + "eunit" -> + %% set up a symlink so that each dep has deps + P = erlang:open_port({spawn_executable, Rebar}, + [{args, ["eunit", "skip_deps=true"]}, + {cd, FDep}, exit_status, + {line, 1024}, stderr_to_stdout, binary]), + {Res, Log} = accumulate(P, []), + CleanedLog = cleanup_logs(Log), + giddyup:post_result([{test, Suite}, {status, get_status(Res)}, + {log, CleanedLog} | Config]), + Res; + "dialyzer" -> + P = erlang:open_port({spawn_executable, "/usr/bin/make"}, + [{args, ["dialyzer"]}, + {cd, FDep}, exit_status, + {line, 1024}, stderr_to_stdout, binary]), + {Res, Log} = accumulate(P, []), + %% TODO split the logs so that the PLT stuff is elided + CleanedLog = cleanup_logs(Log), + giddyup:post_result([{test, Suite}, {status, get_status(Res)}, + {log, CleanedLog} | Config]), + Res; + _ -> + ok + + end; + false -> + lager:debug("Not a dep: ~p", [FDep]) + end + end, Suites). + +setup_deps(_, _, []) -> ok; +setup_deps(Rebar, PWD, [Dep|Deps]) -> + %% clean up an old deps dir, if present + remove_deps_dir(Dep), + %% symlink ALL the deps in + file:make_symlink(filename:join(PWD, "deps"), filename:join(Dep, "deps")), + lager:debug("ln -sf ~s ~s", [filename:join(PWD, "deps"), + filename:join(Dep, "deps")]), + %% run rebar list deps, to find out which ones to keep + P = erlang:open_port({spawn_executable, Rebar}, + [{args, ["list-deps"]}, + {cd, Dep}, exit_status, + {line, 1024}, stderr_to_stdout, binary]), + {0, Log} = accumulate(P, []), + %% find all the deps, amongst the noise + case re:run(Log, "([a-zA-Z0-9_]+) (?:BRANCH|TAG|REV)", + [global, {capture, all_but_first, list}]) of + {match, Matches} -> + lager:info("Deps for ~p are ~p", [Dep, Matches]), + ok = file:delete(filename:join(Dep, "deps")), + ok = filelib:ensure_dir(filename:join(Dep, "deps")++"/"), + [file:make_symlink(filename:join([PWD, "deps", M]), + filename:join([Dep, "deps", M])) + || M <- Matches]; + nomatch -> + %% remove the symlink + file:delete(filename:join(Dep, "deps")), + lager:info("~p has no deps", [Dep]) + end, + setup_deps(Rebar, PWD, Deps). + +remove_deps_dir(Dep) -> + case filelib:is_dir(filename:join(Dep, "deps")) of + true -> + %% there should ONLY be a deps dir leftover from a previous run, + %% so it should be a directory filled with symlinks + {ok, Files} = file:list_dir(filename:join(Dep, "deps")), + lists:foreach(fun(F) -> + File = filename:join([Dep, "deps", F]), + {ok, FI} = file:read_link_info(File), + case FI#file_info.type of + symlink -> + ok = file:delete(File); + _ -> + ok + end + end, Files), + %% this will fail if the directory is not now empty + ok = file:del_dir(filename:join(Dep, "deps")), + ok; + false -> + ok + end. + +wait_for_workers([]) -> + ok; +wait_for_workers(Workers) -> + receive + {'DOWN', _, _, Pid, normal} -> + lager:info("Worker exited normally"), + wait_for_workers(Workers -- [Pid]); + {'DOWN', _, _, Pid, Reason} -> + lager:info("Worker exited abnormally: ~p", [Reason]), + wait_for_workers(Workers -- [Pid]) + end. + +cleanup_logs(Logs) -> + case unicode:characters_to_binary(Logs, latin1, unicode) of + {error, Bin, Rest} -> + lager:error("Bad binary ~p", [Rest]), + Bin; + {incomplete, Bin, Rest} -> + lager:error("Bad binary ~p", [Rest]), + Bin; + Bin -> + Bin + end. + +maybe_eol(eol) -> + "\n"; +maybe_eol(noeol) -> + "". + +get_status(0) -> + pass; +get_status(_) -> + fail. + +accumulate(P, Acc) -> + receive + {P, {data, {EOL, Data}}} -> + accumulate(P, [[Data,maybe_eol(EOL)]|Acc]); + {P, {exit_status, Status}} -> + lager:debug("Exited with status ~b", [Status]), + {Status, list_to_binary(lists:reverse(Acc))}; + {P, Other} -> + lager:warning("Unexpected return from port: ~p", [Other]), + accumulate(P, Acc) + end.