1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1996-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20-module(erl_prim_loader_SUITE).
21
22-include_lib("kernel/include/file.hrl").
23-include_lib("common_test/include/ct.hrl").
24
25-export([all/0, suite/0,groups/0,init_per_suite/1, end_per_suite/1,
26	 init_per_testcase/2,end_per_testcase/2,
27	 init_per_group/2,end_per_group/2]).
28
29-export([get_path/1, set_path/1, get_file/1, normalize_and_backslash/1,
30	 inet_existing/1, inet_coming_up/1, inet_disconnects/1,
31	 multiple_slaves/1, file_requests/1,
32	 local_archive/1, remote_archive/1,
33	 primary_archive/1, virtual_dir_in_archive/1,
34	 get_modules/1]).
35
36-define(PRIM_FILE, prim_file).
37
38%%-----------------------------------------------------------------
39%% Test suite for erl_prim_loader. (Most code is run during system start/stop.)
40%%-----------------------------------------------------------------
41
42suite() ->
43    [{ct_hooks,[ts_install_cth]},
44     {timetrap,{minutes,3}}].
45
46all() ->
47    [get_path, set_path, get_file,
48     normalize_and_backslash, inet_existing,
49     inet_coming_up, inet_disconnects, multiple_slaves,
50     file_requests, local_archive, remote_archive,
51     primary_archive, virtual_dir_in_archive,
52     get_modules].
53
54groups() ->
55    [].
56
57init_per_suite(Config) ->
58    Config.
59
60end_per_suite(_Config) ->
61    ok.
62
63init_per_group(_GroupName, Config) ->
64    Config.
65
66end_per_group(_GroupName, Config) ->
67    Config.
68
69
70init_per_testcase(_Func, Config) ->
71    Config.
72
73end_per_testcase(_Func, _Config) ->
74    ok.
75
76get_path(Config) when is_list(Config) ->
77    case erl_prim_loader:get_path() of
78	{ok, Path} when is_list(Path) ->
79	    ok;
80	_ ->
81	    ct:fail(get_path)
82    end,
83    ok.
84
85set_path(Config) when is_list(Config) ->
86    {ok, Path} = erl_prim_loader:get_path(),
87    ok = erl_prim_loader:set_path(Path),
88    {ok, Path} = erl_prim_loader:get_path(),
89    NewPath = Path ++ ["dummy_dir","/dummy_dir/dummy_dir"],
90    ok = erl_prim_loader:set_path(NewPath),
91    {ok, NewPath} = erl_prim_loader:get_path(),
92
93    ok = erl_prim_loader:set_path(Path), % Reset path.
94    {ok, Path} = erl_prim_loader:get_path(),
95
96    {'EXIT',_} = (catch erl_prim_loader:set_path(not_a_list)),
97    {ok, Path} = erl_prim_loader:get_path(),
98    ok.
99
100get_file(Config) when is_list(Config) ->
101    case erl_prim_loader:get_file("lists" ++ code:objfile_extension()) of
102	{ok,Bin,File} when is_binary(Bin), is_list(File) ->
103	    ok;
104	_ ->
105	    ct:fail(get_valid_file)
106    end,
107    error = erl_prim_loader:get_file("duuuuuuummmy_file"),
108    error = erl_prim_loader:get_file(duuuuuuummmy_file),
109    error = erl_prim_loader:get_file({dummy}),
110    ok.
111
112get_modules(Config) ->
113    case test_server:is_cover() of
114	false -> do_get_modules(Config);
115	true -> {skip,"Cover"}
116    end.
117
118do_get_modules(Config) ->
119    PrivDir = proplists:get_value(priv_dir, Config),
120    NotADir = atom_to_list(?FUNCTION_NAME) ++ "_not_a_dir",
121    ok = file:write_file(filename:join(PrivDir, NotADir), <<>>),
122    ok = file:set_cwd(PrivDir),
123
124    MsGood = lists:sort([lists,gen_server,gb_trees,code_server]),
125    Ms = [certainly_not_existing|MsGood],
126    SuccExp = [begin
127		   F = code:which(M),
128		   {ok,Code} = file:read_file(F),
129		   {M,{F,erlang:md5(Code)}}
130	       end || M <- MsGood],
131    FailExp = [{certainly_not_existing,enoent}],
132
133    io:format("SuccExp = ~p\n", [SuccExp]),
134    io:format("FailExp = ~p\n", [FailExp]),
135
136    Path = code:get_path(),
137    Process = fun(_, F, Code) -> {ok,{F,erlang:md5(Code)}} end,
138    {ok,{SuccExp,FailExp}} = get_modules_sorted(Ms, Process, Path),
139
140    %% Test that an 'enotdir' error can be handled.
141    {ok,{SuccExp,FailExp}} = get_modules_sorted(Ms, Process, [NotADir|Path]),
142
143    Name = inet_get_modules,
144    {ok, Node, BootPid} = complete_start_node(Name),
145    ThisDir = filename:dirname(code:which(?MODULE)),
146    true = rpc:call(Node, code, add_patha, [ThisDir]),
147    _ = rpc:call(Node, code, ensure_loaded, [?MODULE]),
148    {ok,{InetSucc,FailExp}} = rpc:call(Node, erl_prim_loader,
149				       get_modules, [Ms,Process,Path]),
150    SuccExp = lists:sort(InetSucc),
151
152    stop_node(Node),
153    unlink(BootPid),
154    exit(BootPid, kill),
155
156    ok.
157
158get_modules_sorted(Ms, Process, Path) ->
159    case erl_prim_loader:get_modules(Ms, Process, Path) of
160	{ok,{Succ,FailExp}} ->
161	    {ok,{lists:sort(Succ),lists:sort(FailExp)}};
162	Other ->
163	    Other
164    end.
165
166normalize_and_backslash(Config) ->
167    %% Test OTP-11170
168    case os:type() of
169	{win32,_} ->
170	    {skip, "not on windows"};
171	_ ->
172	    test_normalize_and_backslash(Config)
173    end.
174test_normalize_and_backslash(Config) ->
175    PrivDir = proplists:get_value(priv_dir,Config),
176    Dir = filename:join(PrivDir,"\\"),
177    File = filename:join(Dir,"file-OTP-11170"),
178    ok = file:make_dir(Dir),
179    ok = file:write_file(File,"a file to test OTP-11170"),
180    {ok,["file-OTP-11170"]} = file:list_dir(Dir),
181    {ok,["file-OTP-11170"]} = erl_prim_loader:list_dir(Dir),
182    ok = file:delete(File),
183    ok = file:del_dir(Dir),
184    ok.
185
186%% Start a node using the 'inet' loading method,
187%% from an already started boot server.
188inet_existing(Config) when is_list(Config) ->
189    Name = erl_prim_test_inet_existing,
190    BootPid = start_boot_server(),
191    Node = start_node_using_inet(Name),
192    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
193    stop_node(Node),
194    unlink(BootPid),
195    exit(BootPid, kill),
196    ok.
197
198%% Start a node using the 'inet' loading method,
199%% but start the boot server afterwards.
200inet_coming_up(Config) when is_list(Config) ->
201    Name = erl_prim_test_inet_coming_up,
202    Node = start_node_using_inet(Name, [{wait,false}]),
203
204    %% Wait a while, then start boot server, and wait for node to start.
205    ct:sleep({seconds,6}),
206    BootPid = start_boot_server(),
207    wait_really_started(Node, 25),
208
209    %% Check loader argument, then cleanup.
210    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
211    stop_node(Node),
212    unlink(BootPid),
213    exit(BootPid, kill),
214    ok.
215
216wait_really_started(Node, 0) ->
217    ct:fail({not_booted,Node});
218wait_really_started(Node, N) ->
219    case rpc:call(Node, init, get_status, []) of
220 	{started, _} ->
221 	    ok;
222	_ ->
223	    ct:sleep(1000),
224 	    wait_really_started(Node, N - 1)
225    end.
226
227%% Start a node using the 'inet' loading method,
228%% then lose the connection.
229inet_disconnects(Config) when is_list(Config) ->
230    case test_server:is_native(erl_boot_server) of
231	true ->
232	    {skip,"erl_boot_server is native"};
233	false ->
234	    Name = erl_prim_test_inet_disconnects,
235
236	    BootPid = start_boot_server(),
237	    unlink(BootPid),
238	    Self = self(),
239	    %% This process shuts down the boot server during loading.
240	    Stopper = spawn_link(fun() -> stop_boot(BootPid, Self) end),
241	    receive
242		{Stopper,ready} -> ok
243	    end,
244
245	    %% Let the loading begin...
246	    Node = start_node_using_inet(Name, [{wait,false}]),
247
248	    %% When the stopper is ready, the slave node should be
249	    %% looking for a boot server again.
250	    receive
251		{Stopper,ok} ->
252		    ok;
253		{Stopper,{error,Reason}} ->
254		    ct:fail(Reason)
255	    after 60000 ->
256		    ct:fail(stopper_died)
257	    end,
258
259	    %% Start new boot server to see that loading is continued.
260	    BootPid2 = start_boot_server(),
261	    wait_really_started(Node, 25),
262	    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
263	    stop_node(Node),
264	    unlink(BootPid2),
265	    exit(BootPid2, kill),
266	    ok
267    end.
268
269%% Trace boot server calls and stop the server before loading is finished.
270stop_boot(BootPid, Super) ->
271    erlang:trace(all, true, [call]),
272    1 = erlang:trace_pattern({erl_boot_server,send_file_result,3}, true, [local]),
273    BootRef = erlang:monitor(process, BootPid),
274    Super ! {self(),ready},
275    Result = get_calls(100, BootPid),
276    exit(BootPid, kill),
277    erlang:trace_pattern({erl_boot_server,send_file_result,3}, false, [local]),
278    erlang:trace(all, false, [call]),
279    receive
280	{'DOWN',BootRef,_,_, killed} -> ok
281    end,
282    Super ! {self(),Result}.
283
284get_calls(0, _) ->
285    ok;
286get_calls(Count, Pid) ->
287    receive
288	{trace,_,call,_MFA} ->
289	    get_calls(Count-1, Pid)
290    after 10000 ->
291	    {error,{trace_msg_timeout,Count}}
292    end.
293
294%% Start nodes in parallel, all using the 'inet' loading method;
295%% verify that the boot server manages.
296multiple_slaves(Config) when is_list(Config) ->
297    Name = erl_prim_test_multiple_slaves,
298    Host = host(),
299    IpStr = ip_str(Host),
300    Args = " -loader inet -hosts " ++ IpStr,
301
302    NoOfNodes = 10,			% no of slave nodes to be started
303
304    NamesAndNodes =
305        lists:map(fun(N) ->
306                          NameN = atom_to_list(Name) ++
307                              integer_to_list(N),
308                          NodeN = NameN ++ "@" ++ Host,
309                          {list_to_atom(NameN),list_to_atom(NodeN)}
310                  end, lists:seq(1, NoOfNodes)),
311
312    Nodes = start_multiple_nodes(NamesAndNodes, Args, []),
313
314    %% "queue up" the nodes to wait for the boot server to respond
315    %% (note: test_server supervises each node start by accept()
316    %% on a socket, the timeout value for the accept has to be quite
317    %% long for this test to work).
318    ct:sleep({seconds,5}),
319    %% start the code loading circus!
320    BootPid = start_boot_server(),
321    %% give the nodes a chance to boot up before attempting to stop them
322    ct:sleep({seconds,10}),
323
324    wait_and_shutdown(lists:reverse(Nodes), 30),
325
326    unlink(BootPid),
327    exit(BootPid, kill),
328    ok.
329
330start_multiple_nodes([{Name,Node} | NNs], Args, Started) ->
331    {ok,Node} = start_node(Name, Args, [{wait, false}]),
332    start_multiple_nodes(NNs, Args, [Node | Started]);
333start_multiple_nodes([], _, Nodes) ->
334    Nodes.
335
336wait_and_shutdown([Node | Nodes], Tries) ->
337    wait_really_started(Node, Tries),
338    {ok,[["inet"]]} = rpc:call(Node, init, get_argument, [loader]),
339    stop_node(Node),
340    wait_and_shutdown(Nodes, Tries);
341wait_and_shutdown([], _) ->
342    ok.
343
344
345%% Start a node using the 'inet' loading method,
346%% verify that the boot server responds to file requests.
347file_requests(Config) when is_list(Config) ->
348    {ok, Node, BootPid} = complete_start_node(erl_prim_test_file_req),
349
350    %% compare with results from file server calls (the
351    %% boot server uses the same file sys and cwd)
352    {ok,Files} = file:list_dir("."),
353    io:format("Files: ~p~n",[Files]),
354    {ok,Files} = rpc:call(Node, erl_prim_loader, list_dir, ["."]),
355    {ok,Info} = file:read_file_info(code:which(test_server)),
356    {ok,Info} = rpc:call(Node, erl_prim_loader, read_file_info,
357			 [code:which(test_server)]),
358
359    PrivDir = proplists:get_value(priv_dir,Config),
360    Dir = filename:join(PrivDir,?MODULE_STRING++"_file_requests"),
361    ok = file:make_dir(Dir),
362    Alias = filename:join(Dir,"symlink"),
363    case file:make_symlink(code:which(test_server), Alias) of
364	{error, enotsup} ->
365	    %% Links not supported on this platform
366	    ok;
367	{error, eperm} ->
368	    {win32,_} = os:type(),
369	    %% Windows user not privileged to create symlinks"
370	    ok;
371	ok ->
372	    %% Reading file info for link should return file info for
373	    %% link target
374	    {ok,Info} = rpc:call(Node, erl_prim_loader, read_file_info,
375				 [Alias]),
376	    #file_info{type=regular} = Info,
377	    {ok,#file_info{type=symlink}} =
378		rpc:call(Node, erl_prim_loader, read_link_info,
379			 [Alias])
380    end,
381
382    {ok,Cwd} = file:get_cwd(),
383    {ok,Cwd} = rpc:call(Node, erl_prim_loader, get_cwd, []),
384    case file:get_cwd("C:") of
385	{error,enotsup} ->
386	    ok;
387	{ok,DCwd} ->
388	    {ok,DCwd} = rpc:call(Node, erl_prim_loader, get_cwd, ["C:"])
389    end,
390
391    stop_node(Node),
392    unlink(BootPid),
393    exit(BootPid, kill),
394    ok.
395
396%% Read files from local archive.
397local_archive(Config) when is_list(Config) ->
398    PrivDir = proplists:get_value(priv_dir, Config),
399    KernelDir = filename:basename(code:lib_dir(kernel)),
400    Archive = filename:join([PrivDir, KernelDir ++ init:archive_extension()]),
401    file:delete(Archive),
402    {ok, Archive} = create_archive(Archive, [KernelDir]),
403
404    Node = node(),
405    BeamName = "inet.beam",
406    ok = test_archive(Node, Archive, KernelDir, BeamName),
407
408    %% Cleanup
409    ok = rpc:call(Node, erl_prim_loader, purge_archive_cache, []),
410    ok = file:delete(Archive),
411    ok.
412
413%% Read files from remote archive.
414remote_archive(Config) when is_list(Config) ->
415    PrivDir = proplists:get_value(priv_dir, Config),
416    KernelDir = filename:basename(code:lib_dir(kernel)),
417    Archive = filename:join([PrivDir, KernelDir ++ init:archive_extension()]),
418    file:delete(Archive),
419    {ok, Archive} = create_archive(Archive, [KernelDir]),
420
421    {ok, Node, BootPid} = complete_start_node(remote_archive),
422
423    BeamName = "inet.beam",
424    ok = test_archive(Node, Archive, KernelDir, BeamName),
425
426    %% Cleanup
427    stop_node(Node),
428    unlink(BootPid),
429    exit(BootPid, kill),
430    ok.
431
432%% Read files from primary archive.
433primary_archive(Config) when is_list(Config) ->
434    %% Copy the orig files to priv_dir
435    PrivDir = proplists:get_value(priv_dir, Config),
436    Archive = filename:join([PrivDir, "primary_archive.zip"]),
437    file:delete(Archive),
438    DataDir = proplists:get_value(data_dir, Config),
439    {ok, _} = zip:create(Archive, ["primary_archive"],
440			 [{compress, []}, {cwd, DataDir}]),
441    {ok, _} = zip:extract(Archive, [{cwd, PrivDir}]),
442    TopDir = filename:join([PrivDir, "primary_archive"]),
443
444    %% Compile the code
445    DictDir = "primary_archive_dict-1.0",
446    DummyDir = "primary_archive_dummy",
447    ok = compile_app(TopDir, DictDir),
448    ok = compile_app(TopDir, DummyDir),
449
450    %% Create the archive
451    {ok, TopFiles} = file:list_dir(TopDir),
452    {ok, {_, ArchiveBin}} = zip:create(Archive, TopFiles,
453				       [memory, {compress, []}, {cwd, TopDir}]),
454
455    %% Use temporary node to simplify cleanup
456    Cookie = atom_to_list(erlang:get_cookie()),
457    Args = " -setcookie " ++ Cookie,
458    {ok,Node} = start_node(primary_archive, Args),
459    wait_really_started(Node, 25),
460    {_,_,_} = rpc:call(Node, erlang, date, []),
461
462    %% Set primary archive
463    ExpectedEbins = [Archive, DictDir ++ "/ebin", DummyDir ++ "/ebin"],
464    io:format("ExpectedEbins: ~p\n", [ExpectedEbins]),
465    {ok, FileInfo} = ?PRIM_FILE:read_file_info(Archive),
466    {ok, Ebins} = rpc:call(Node, erl_prim_loader, set_primary_archive,
467			   [Archive, ArchiveBin, FileInfo,
468			    fun escript:parse_file/1]),
469    ExpectedEbins = lists:sort(Ebins), % assert
470
471    {ok, TopFiles2} = rpc:call(Node, erl_prim_loader, list_dir, [Archive]),
472    [DictDir, DummyDir] = lists:sort(TopFiles2),
473    BeamName = "primary_archive_dict_app.beam",
474    ok = test_archive(Node, Archive, DictDir, BeamName),
475
476    %% Cleanup
477    {ok, []} = rpc:call(Node, erl_prim_loader, set_primary_archive,
478			[undefined, undefined, undefined,
479			 fun escript:parse_file/1]),
480    stop_node(Node),
481    ok = file:delete(Archive),
482    ok.
483
484test_archive(Node, TopDir, AppDir, BeamName) ->
485    %% List dir
486    io:format("test_archive: ~p\n", [rpc:call(Node, erl_prim_loader, list_dir, [TopDir])]),
487    {ok, TopFiles} = rpc:call(Node, erl_prim_loader, list_dir, [TopDir]),
488    true = lists:member(AppDir, TopFiles),
489    AbsAppDir = TopDir ++ "/" ++ AppDir,
490    {ok, AppFiles} = rpc:call(Node, erl_prim_loader, list_dir, [AbsAppDir]),
491    true = lists:member("ebin", AppFiles),
492    Ebin = AbsAppDir ++ "/ebin",
493    {ok, EbinFiles} = rpc:call(Node, erl_prim_loader, list_dir, [Ebin]),
494    Beam = Ebin ++ "/" ++ BeamName,
495    true = lists:member(BeamName, EbinFiles),
496    error = rpc:call(Node, erl_prim_loader, list_dir, [TopDir ++ "/no_such_file"]),
497    error = rpc:call(Node, erl_prim_loader, list_dir, [TopDir ++ "/ebin/no_such_file"]),
498
499    %% File info
500    {ok, #file_info{type = directory}} =
501	rpc:call(Node, erl_prim_loader, read_file_info, [TopDir]),
502    {ok, #file_info{type = directory}} =
503	rpc:call(Node, erl_prim_loader, read_file_info, [Ebin]),
504    {ok, #file_info{type = regular} = FI}  =
505	rpc:call(Node, erl_prim_loader, read_file_info, [Beam]),
506    error = rpc:call(Node, erl_prim_loader, read_file_info, [TopDir ++ "/no_such_file"]),
507    error = rpc:call(Node, erl_prim_loader, read_file_info, [TopDir ++ "/ebin/no_such_file"]),
508
509    %% Get file
510    {ok, Bin, Beam} = rpc:call(Node, erl_prim_loader, get_file, [Beam]),
511    if
512	FI#file_info.size =:= byte_size(Bin) -> ok;
513	true -> exit({FI#file_info.size, byte_size(Bin)})
514    end,
515    error = rpc:call(Node, erl_prim_loader, get_file, ["/no_such_file"]),
516    error = rpc:call(Node, erl_prim_loader, get_file, ["/ebin/no_such_file"]),
517    ok.
518
519create_archive(Archive, AppDirs) ->
520    LibDir = code:lib_dir(),
521    Opts = [{compress, []}, {cwd, LibDir}],
522    io:format("zip:create(~p,\n\t~p,\n\t~p).\n", [Archive, AppDirs, Opts]),
523    zip:create(Archive, AppDirs, Opts).
524
525
526%% Read virtual directories from archive.
527virtual_dir_in_archive(Config) when is_list(Config) ->
528    PrivDir = proplists:get_value(priv_dir, Config),
529    Data = <<"A little piece of data.">>,
530    ArchiveBase = "archive_with_virtual_dirs",
531    Archive = filename:join([PrivDir, ArchiveBase ++ init:archive_extension()]),
532    FileBase = "a_data_file.beam",
533    EbinBase = "ebin",
534    FileInArchive = filename:join([ArchiveBase, EbinBase, FileBase]),
535    BinFiles = [{FileInArchive, Data}],
536    Opts = [{compress, []}],
537    file:delete(Archive),
538    io:format("zip:create(~p,\n\t~p,\n\t~p).\n", [Archive, BinFiles, Opts]),
539    {ok, Archive} = zip:create(Archive, BinFiles, Opts),
540
541    %% Verify that there is no directories
542    {ok, BinFiles} = zip:unzip(Archive, [memory]),
543
544    FullPath = filename:join([Archive, FileInArchive]),
545    {ok, _} = erl_prim_loader:read_file_info(FullPath),
546
547    %% Read one virtual dir
548    EbinDir = filename:dirname(FullPath),
549    {ok, _} = erl_prim_loader:read_file_info(EbinDir),
550    {ok, [FileBase]} = erl_prim_loader:list_dir(EbinDir),
551
552    %% Read another virtual dir
553    AppDir = filename:dirname(EbinDir),
554    {ok, _} = erl_prim_loader:read_file_info(AppDir),
555    {ok, [EbinBase]} = erl_prim_loader:list_dir(AppDir),
556
557    %% Cleanup
558    ok = erl_prim_loader:purge_archive_cache(),
559    ok = file:delete(Archive),
560    ok.
561
562%%%
563%%% Helper functions.
564%%%
565
566complete_start_node(Name) ->
567    BootPid = start_boot_server(),
568    Node = start_node_using_inet(Name),
569    wait_really_started(Node, 25),
570    {ok, Node, BootPid}.
571
572start_boot_server() ->
573    %% Many linux systems define:
574    %%   127.0.0.1 localhost
575    %%   127.0.1.1 somehostname
576    %% Therefore, to allow the tests to work on those kind of systems,
577    %% also include "localhost" in the list of allowed hosts.
578
579    Hosts = [host(),ip_str("localhost")],
580    {ok,BootPid} = erl_boot_server:start_link(Hosts),
581    BootPid.
582
583start_node_using_inet(Name) ->
584    start_node_using_inet(Name, []).
585
586start_node_using_inet(Name, Opts) ->
587    Host = host(),
588    IpStr = ip_str(Host),
589    Args = " -loader inet -hosts " ++ IpStr,
590    {ok,Node} = start_node(Name, Args, Opts),
591    Node.
592
593
594ip_str({A, B, C, D}) ->
595    lists:concat([A, ".", B, ".", C, ".", D]);
596ip_str(Host) ->
597    {ok,Ip} = inet:getaddr(Host, inet),
598    ip_str(Ip).
599
600start_node(Name, Args) ->
601    start_node(Name, Args, []).
602
603start_node(Name, Args, Opts) ->
604    Opts2 = [{args, Args}|Opts],
605    io:format("test_server:start_node(~p, peer, ~p).\n",
606	      [Name, Opts2]),
607    Res = test_server:start_node(Name, peer, Opts2),
608    io:format("start_node -> ~p\n", [Res]),
609    Res.
610
611host() ->
612    {ok,Host} = inet:gethostname(),
613    Host.
614
615stop_node(Node) ->
616    test_server:stop_node(Node).
617
618compile_app(TopDir, AppName) ->
619    AppDir = filename:join([TopDir, AppName]),
620    SrcDir = filename:join([AppDir, "src"]),
621    OutDir = filename:join([AppDir, "ebin"]),
622    {ok, Files} = file:list_dir(SrcDir),
623    compile_files(Files, SrcDir, OutDir).
624
625compile_files([File | Files], SrcDir, OutDir) ->
626    case filename:extension(File) of
627	".erl" ->
628	    AbsFile = filename:join([SrcDir, File]),
629	    case compile:file(AbsFile, [{outdir, OutDir}]) of
630		{ok, _Mod} ->
631		    compile_files(Files, SrcDir, OutDir);
632		Error ->
633		    {compilation_error, AbsFile, OutDir, Error}
634	    end;
635	_ ->
636	    compile_files(Files, SrcDir, OutDir)
637    end;
638compile_files([], _, _) ->
639    ok.
640
641