Add utilities for creating test clusters
These utilities allow us to setup a test cluster for use during unit
tests. By default this will setup a cluster once per VM and also re-use
state from old test runs if it exists. Tests should ensure an empty
cluster when necessary by specifying the `empty` option when calling
`erlfdb_util:get_test_db/1`.
diff --git a/.gitignore b/.gitignore
index 2a481e7..6782584 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,5 @@
c_src/*.d
c_src/*.o
ebin/
-priv/
+
+priv/erlfdb_nif.*
diff --git a/priv/monitor.py b/priv/monitor.py
new file mode 100755
index 0000000..643ace9
--- /dev/null
+++ b/priv/monitor.py
@@ -0,0 +1,24 @@
+#!/usr/bin/python
+
+import os
+import sys
+import time
+
+
+def main():
+ parent_pid = int(sys.argv[1])
+ target_pid = int(sys.argv[2])
+ while True:
+ try:
+ os.kill(parent_pid, 0)
+ time.sleep(1.0)
+ except OSError:
+ try:
+ os.kill(target_pid, 9)
+ except:
+ pass
+ exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/erlfdb_util.erl b/src/erlfdb_util.erl
index cd9f99d..a6846f2 100644
--- a/src/erlfdb_util.erl
+++ b/src/erlfdb_util.erl
@@ -14,6 +14,11 @@
-export([
+ get_test_db/0,
+ get_test_db/1,
+
+ init_test_cluster/1,
+
get/2,
get/3,
@@ -21,6 +26,32 @@
]).
+get_test_db() ->
+ get_test_db([]).
+
+
+get_test_db(Options) ->
+ {ok, ClusterFile} = init_test_cluster(Options),
+ Db = erlfdb:open(ClusterFile),
+ case proplists:get_value(empty, Options) of
+ true ->
+ erlfdb:transactional(Db, fun(Tx) ->
+ erlfdb:clear_range(Tx, <<>>, <<16#FE, 16#FF, 16#FF, 16#FF>>)
+ end);
+ _ ->
+ ok
+ end,
+ Db.
+
+
+init_test_cluster(Options) ->
+ case application:get_env(erlfdb, test_cluster_file) of
+ {ok, ClusterFile} ->
+ {ok, ClusterFile};
+ undefined ->
+ init_test_cluster_int(Options)
+ end.
+
get(List, Key) ->
get(List, Key, undefined).
@@ -45,3 +76,153 @@
_ -> io_lib:format("\\x~2.16.0b", [C])
end
end, binary_to_list(Bin)) ++ [$'].
+
+
+init_test_cluster_int(Options) ->
+ {ok, CWD} = file:get_cwd(),
+ DefaultIpAddr = {127, 0, 0, 1},
+ DefaultPort = get_available_port(),
+ DefaultDir = filename:join(CWD, ".erlfdb"),
+
+
+ IpAddr = ?MODULE:get(Options, ip_addr, DefaultIpAddr),
+ Port = ?MODULE:get(Options, port, DefaultPort),
+ Dir = ?MODULE:get(Options, dir, DefaultDir),
+ ClusterName = ?MODULE:get(Options, cluster_name, <<"erlfdbtest">>),
+ ClusterId = ?MODULE:get(Options, cluster_id, <<"erlfdbtest">>),
+
+ DefaultClusterFile = filename:join(Dir, <<"erlfdb.cluster">>),
+ ClusterFile = ?MODULE:get(Options, cluster_file, DefaultClusterFile),
+
+ write_cluster_file(ClusterFile, ClusterName, ClusterId, IpAddr, Port),
+
+ FDBServerBin = find_fdbserver_bin(Options),
+
+ {FDBPid, _} = spawn_monitor(fun() ->
+ % Open the fdbserver port
+ FDBPortName = {spawn_executable, FDBServerBin},
+ FDBPortArgs = [
+ <<"-p">>, ip_port_to_str(IpAddr, Port),
+ <<"-C">>, ClusterFile,
+ <<"-d">>, Dir,
+ <<"-L">>, Dir
+ ],
+ FDBPortOpts = [{args, FDBPortArgs}],
+ FDBServer = erlang:open_port(FDBPortName, FDBPortOpts),
+ {os_pid, FDBPid} = erlang:port_info(FDBServer, os_pid),
+
+ % Open the monitor pid
+ MonitorPath = get_monitor_path(),
+ ErlPid = os:getpid(),
+
+ MonitorPortName = {spawn_executable, MonitorPath},
+ MonitorPortArgs = [{args, [ErlPid, integer_to_binary(FDBPid)]}],
+ Monitor = erlang:open_port(MonitorPortName, MonitorPortArgs),
+
+ init_fdb_db(ClusterFile, Options),
+
+ receive
+ {wait_for_init, ParentPid} ->
+ ParentPid ! {initialized, self()}
+ after 5000 ->
+ true = erlang:port_close(FDBServer),
+ true = erlang:port_close(Monitor),
+ erlang:error(fdb_parent_died)
+ end,
+
+ port_loop(FDBServer, Monitor),
+
+ true = erlang:port_close(FDBServer),
+ true = erlang:port_close(Monitor)
+ end),
+
+ FDBPid ! {wait_for_init, self()},
+ receive
+ {initialized, FDBPid} ->
+ ok;
+ Msg ->
+ erlang:error({fdbserver_error, Msg})
+ end,
+
+ ok = application:set_env(erlfdb, test_cluster_file, ClusterFile),
+ ok = application:set_env(erlfdb, test_cluster_pid, FDBPid),
+ {ok, ClusterFile}.
+
+
+get_available_port() ->
+ {ok, Socket} = gen_tcp:listen(0, []),
+ {ok, Port} = inet:port(Socket),
+ ok = gen_tcp:close(Socket),
+ Port.
+
+
+find_fdbserver_bin(Options) ->
+ Locations = case ?MODULE:get(Options, fdbserver_bin) of
+ undefined ->
+ [
+ <<"/usr/sbin/fdbserver">>,
+ <<"/usr/local/sbin/fdbserver">>,
+ <<"/usr/local/libexec/fdbserver">>
+ ];
+ Else ->
+ [Else]
+ end,
+ case lists:filter(fun filelib:is_file/1, Locations) of
+ [Path | _] -> Path;
+ [] -> erlang:error(fdbserver_bin_not_found)
+ end.
+
+
+write_cluster_file(FileName, ClusterName, ClusterId, IpAddr, Port) ->
+ Args = [ClusterName, ClusterId, ip_port_to_str(IpAddr, Port)],
+ Contents = io_lib:format("~s:~s@~s~n", Args),
+ ok = filelib:ensure_dir(FileName),
+ ok = file:write_file(FileName, iolist_to_binary(Contents)).
+
+
+get_monitor_path() ->
+ PrivDir = case code:priv_dir(?MODULE) of
+ {error, _} ->
+ EbinDir = filename:dirname(code:which(?MODULE)),
+ AppPath = filename:dirname(EbinDir),
+ filename:join(AppPath, "priv");
+ Path ->
+ Path
+ end,
+ filename:join(PrivDir, "monitor.py").
+
+
+init_fdb_db(ClusterFile, Options) ->
+ DefaultFDBCli = os:find_executable("fdbcli"),
+ FDBCli = case ?MODULE:get(Options, fdbcli_bin, DefaultFDBCli) of
+ false -> erlang:error(fdbcli_not_found);
+ FDBCli0 -> FDBCli0
+ end,
+ Fmt = "~s -C ~s --exec 'configure new single ssd'",
+ Cmd = lists:flatten(io_lib:format(Fmt, [FDBCli, ClusterFile])),
+ case os:cmd(Cmd) of
+ "Database created" ++ _ -> ok;
+ "ERROR: Database already exists!" ++ _ -> ok;
+ Msg -> erlang:error({fdb_init_error, Msg})
+ end.
+
+
+port_loop(FDBServer, Monitor) ->
+ receive
+ close ->
+ ok;
+ {FDBServer, {data, "FDBD joined cluster.\n"}} ->
+ % Silence start message
+ port_loop(FDBServer, Monitor);
+ {Port, {data, Msg}} when Port == FDBServer orelse Port == Monitor ->
+ io:format(standard_error, "~p", [Msg]),
+ port_loop(FDBServer, Monitor);
+ Error ->
+ erlang:exit({fdb_cluster_error, Error})
+ end.
+
+
+ip_port_to_str({I1, I2, I3, I4}, Port) ->
+ Fmt = "~b.~b.~b.~b:~b",
+ iolist_to_binary(io_lib:format(Fmt, [I1, I2, I3, I4, Port])).
+
diff --git a/test/erlfdb_02_anon_fdbserver_test.erl b/test/erlfdb_02_anon_fdbserver_test.erl
new file mode 100644
index 0000000..aa37042
--- /dev/null
+++ b/test/erlfdb_02_anon_fdbserver_test.erl
@@ -0,0 +1,74 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(erlfdb_02_anon_fdbserver_test).
+
+-include_lib("eunit/include/eunit.hrl").
+
+
+basic_init_test() ->
+ {ok, ClusterFile} = erlfdb_util:init_test_cluster([]),
+ ?assert(is_binary(ClusterFile)).
+
+
+basic_open_test() ->
+ {ok, ClusterFile} = erlfdb_util:init_test_cluster([]),
+ Db = erlfdb:open(ClusterFile),
+ erlfdb:transactional(Db, fun(Tx) ->
+ ?assert(true)
+ end).
+
+
+get_db_test() ->
+ Db = erlfdb_util:get_test_db(),
+ erlfdb:transactional(Db, fun(Tx) ->
+ ?assert(true)
+ end).
+
+
+get_set_get_test() ->
+ Db = erlfdb_util:get_test_db(),
+ Key = crypto:strong_rand_bytes(8),
+ Val = crypto:strong_rand_bytes(8),
+ erlfdb:transactional(Db, fun(Tx) ->
+ ?assertEqual(not_found, erlfdb:wait(erlfdb:get(Tx, Key)))
+ end),
+ erlfdb:transactional(Db, fun(Tx) ->
+ ?assertEqual(ok, erlfdb:set(Tx, Key, Val))
+ end),
+ erlfdb:transactional(Db, fun(Tx) ->
+ ?assertEqual(Val, erlfdb:wait(erlfdb:get(Tx, Key)))
+ end).
+
+
+get_empty_test() ->
+ Db1 = erlfdb_util:get_test_db(),
+ Key = crypto:strong_rand_bytes(8),
+ Val = crypto:strong_rand_bytes(8),
+ erlfdb:transactional(Db1, fun(Tx) ->
+ ok = erlfdb:set(Tx, Key, Val)
+ end),
+ erlfdb:transactional(Db1, fun(Tx) ->
+ ?assertEqual(Val, erlfdb:wait(erlfdb:get(Tx, Key)))
+ end),
+
+ % Check we can get an empty db
+ Db2 = erlfdb_util:get_test_db([empty]),
+ erlfdb:transactional(Db2, fun(Tx) ->
+ ?assertEqual(not_found, erlfdb:wait(erlfdb:get(Tx, Key)))
+ end),
+
+ % And check state that the old db handle is
+ % the same
+ erlfdb:transactional(Db1, fun(Tx) ->
+ ?assertEqual(not_found, erlfdb:wait(erlfdb:get(Tx, Key)))
+ end).