diff options
author | Siri Hansen <[email protected]> | 2013-05-21 17:50:09 +0200 |
---|---|---|
committer | Siri Hansen <[email protected]> | 2013-05-21 17:50:09 +0200 |
commit | 2886d9937990c3465671b59d744f6bacd43545c8 (patch) | |
tree | bf4a5b8e4cf45de31809eb63b60a7b559c1ed164 /lib/common_test | |
parent | e0e9885cd3561c46114ee22ac84c01d9347187ea (diff) | |
parent | cae4bc87c2b3009a505f91a1c2d38a484cc9a1e4 (diff) | |
download | otp-2886d9937990c3465671b59d744f6bacd43545c8.tar.gz otp-2886d9937990c3465671b59d744f6bacd43545c8.tar.bz2 otp-2886d9937990c3465671b59d744f6bacd43545c8.zip |
Merge branch 'siri/ct/no_prompt_check/OTP-11095' into maint
* siri/ct/no_prompt_check/OTP-11095:
[common_test] Refactor ct_telnet_SUITE.erl
[common_test] Add new option 'no_prompt_check' to ct_telnet:expect/3.
[common_test] Add debug printouts in ct_telnet_client
[common_test] Backed out wait_for_linebreak option from ct_telnet
[common_test] Add test for ct_telnet using own telnet server
[common_test] Add telnet server to use for testing
Diffstat (limited to 'lib/common_test')
-rw-r--r-- | lib/common_test/src/ct_telnet.erl | 80 | ||||
-rw-r--r-- | lib/common_test/src/ct_telnet_client.erl | 8 | ||||
-rw-r--r-- | lib/common_test/src/unix_telnet.erl | 36 | ||||
-rw-r--r-- | lib/common_test/test/Makefile | 1 | ||||
-rw-r--r-- | lib/common_test/test/ct_telnet_SUITE.erl | 69 | ||||
-rw-r--r-- | lib/common_test/test/ct_telnet_SUITE_data/ct_telnet_own_server_SUITE.erl | 184 | ||||
-rw-r--r-- | lib/common_test/test/telnet_server.erl | 228 |
7 files changed, 525 insertions, 81 deletions
diff --git a/lib/common_test/src/ct_telnet.erl b/lib/common_test/src/ct_telnet.erl index bd74991859..4755d939e0 100644 --- a/lib/common_test/src/ct_telnet.erl +++ b/lib/common_test/src/ct_telnet.erl @@ -29,9 +29,7 @@ %%% Command timeout = 10 sec (time to wait for a command to return) %%% Max no of reconnection attempts = 3 %%% Reconnection interval = 5 sek (time to wait in between reconnection attempts) -%%% Keep alive = true (will send NOP to the server every 10 sec if connection is idle) -%%% Wait for linebreak = true (Will expect answer from server to end with linebreak when -%%% using ct_telnet:expect)</pre> +%%% Keep alive = true (will send NOP to the server every 10 sec if connection is idle)</pre> %%% <p>These parameters can be altered by the user with the following %%% configuration term:</p> %%% <pre> @@ -39,8 +37,7 @@ %%% {command_timeout,Millisec}, %%% {reconnection_attempts,N}, %%% {reconnection_interval,Millisec}, -%%% {keep_alive,Bool}, -%% {wait_for_linebreak, Bool}]}.</pre> +%%% {keep_alive,Bool}]}.</pre> %%% <p><code>Millisec = integer(), N = integer()</code></p> %%% <p>Enter the <code>telnet_settings</code> term in a configuration %%% file included in the test and ct_telnet will retrieve the information @@ -312,7 +309,7 @@ expect(Connection,Patterns) -> %%% Tag = term() %%% Opts = [Opt] %%% Opt = {timeout,Timeout} | repeat | {repeat,N} | sequence | -%%% {halt,HaltPatterns} | ignore_prompt +%%% {halt,HaltPatterns} | ignore_prompt | no_prompt_check %%% Timeout = integer() %%% N = integer() %%% HaltPatterns = Patterns @@ -339,14 +336,28 @@ expect(Connection,Patterns) -> %%% will also include the matched <code>Tag</code>. Else, only %%% <code>RxMatch</code> is returned.</p> %%% -%%% <p>The function will always return when a prompt is found, unless -%%% the <code>ignore_prompt</code> options is used.</p> -%%% %%% <p>The <code>timeout</code> option indicates that the function %%% shall return if the telnet client is idle (i.e. if no data is %%% received) for more than <code>Timeout</code> milliseconds. Default %%% timeout is 10 seconds.</p> %%% +%%% <p>The function will always return when a prompt is found, unless +%%% any of the <code>ignore_prompt</code> or +%%% <code>no_prompt_check</code> options are used, in which case it +%%% will return when a match is found or after a timeout.</p> +%%% +%%% <p>If the <code>ignore_prompt</code> option is used, +%%% <code>ct_telnet</code> will ignore any prompt found. This option +%%% is useful if data sent by the server could include a pattern that +%%% would match the prompt regexp (as returned by +%%% <code>TargedMod:get_prompt_regexp/0</code>), but which should not +%%% cause the function to return.</p> +%%% +%%% <p>If the <code>no_prompt_check</code> option is used, +%%% <code>ct_telnet</code> will not search for a prompt at all. This +%%% is useful if, for instance, the <code>Pattern</code> itself +%%% matches the prompt.</p> +%%% %%% <p>The <code>repeat</code> option indicates that the pattern(s) %%% shall be matched multiple times. If <code>N</code> is given, the %%% pattern(s) will be matched <code>N</code> times, and the function @@ -732,7 +743,7 @@ teln_get_all_data(Pid,Prx,Data,Acc,LastLine) -> seq=false, repeat=false, found_prompt=false, - wait_for_linebreak=true}). + prompt_check=true}). %% @hidden %% @doc Externally the silent_teln_expect function shall only be used @@ -758,25 +769,27 @@ silent_teln_expect(Pid,Data,Pattern,Prx,Opts) -> %% condition is fullfilled. %% 3b) Repeat (sequence): 2) is repeated either N times or until a %% halt condition is fullfilled. -teln_expect(Pid,Data,Pattern0,Prx,Opts) -> - HaltPatterns = case get_ignore_prompt(Opts) of - true -> - get_haltpatterns(Opts); - false -> - [prompt | get_haltpatterns(Opts)] - end, - WaitForLineBreak = get_line_break_opt(Opts), +teln_expect(Pid,Data,Pattern0,Prx,Opts) -> + HaltPatterns = + case get_ignore_prompt(Opts) of + true -> + get_haltpatterns(Opts); + false -> + [prompt | get_haltpatterns(Opts)] + end, + + PromptCheck = get_prompt_check(Opts), Seq = get_seq(Opts), Pattern = convert_pattern(Pattern0,Seq), - + Timeout = get_timeout(Opts), - + EO = #eo{teln_pid=Pid, prx=Prx, timeout=Timeout, seq=Seq, haltpatterns=HaltPatterns, - wait_for_linebreak=WaitForLineBreak}, + prompt_check=PromptCheck}, case get_repeat(Opts) of false -> @@ -817,11 +830,6 @@ get_timeout(Opts) -> {value,{timeout,T}} -> T; false -> ?DEFAULT_TIMEOUT end. -get_line_break_opt(Opts) -> - case lists:keysearch(wait_for_linebreak,1,Opts) of - {value,{wait_for_linebreak,false}} -> false; - _ -> true - end. get_repeat(Opts) -> case lists:keysearch(repeat,1,Opts) of {value,{repeat,N}} when is_integer(N) -> @@ -845,7 +853,9 @@ get_haltpatterns(Opts) -> end. get_ignore_prompt(Opts) -> lists:member(ignore_prompt,Opts). - +get_prompt_check(Opts) -> + not lists:member(no_prompt_check,Opts). + %% Repeat either single or sequence. All match results are accumulated %% and returned when a halt condition is fulllfilled. repeat_expect(Rest,_Pattern,Acc,#eo{repeat=0}) -> @@ -906,6 +916,9 @@ get_data1(Pid) -> %% lines and each line is matched against each pattern. %% one_expect: split data chunk at prompts +one_expect(Data,Pattern,EO) when EO#eo.prompt_check==false -> +% io:format("Raw Data ~p Pattern ~p EO ~p ",[Data,Pattern,EO]), + one_expect1(Data,Pattern,[],EO#eo{found_prompt=false}); one_expect(Data,Pattern,EO) -> case match_prompt(Data,EO#eo.prx) of {prompt,UptoPrompt,PromptType,Rest} -> @@ -964,6 +977,8 @@ seq_expect(Data,[],Acc,_EO) -> {match,lists:reverse(Acc),Data}; seq_expect([],Patterns,Acc,_EO) -> {continue,Patterns,lists:reverse(Acc),[]}; +seq_expect(Data,Patterns,Acc,EO) when EO#eo.prompt_check==false -> + seq_expect1(Data,Patterns,Acc,[],EO#eo{found_prompt=false}); seq_expect(Data,Patterns,Acc,EO) -> case match_prompt(Data,EO#eo.prx) of {prompt,UptoPrompt,PromptType,Rest} -> @@ -1018,9 +1033,8 @@ seq_expect1(Data,[],Acc,Rest,_EO) -> %% Split prompt-chunk at lines match_lines(Data,Patterns,EO) -> FoundPrompt = EO#eo.found_prompt, - NeedLineBreak = EO#eo.wait_for_linebreak, case one_line(Data,[]) of - {noline,Rest} when FoundPrompt=/=false, NeedLineBreak =:= true -> + {noline,Rest} when FoundPrompt=/=false -> %% This is the line including the prompt case match_line(Rest,Patterns,FoundPrompt,EO) of nomatch -> @@ -1028,14 +1042,14 @@ match_lines(Data,Patterns,EO) -> {Tag,Match} -> {Tag,Match,[]} end; - {noline,Rest} when NeedLineBreak =:= false -> - case match_line(Rest,Patterns,FoundPrompt,EO) of + {noline,Rest} when EO#eo.prompt_check==false -> + case match_line(Rest,Patterns,false,EO) of nomatch -> - {nomatch,prompt}; + {nomatch,Rest}; {Tag,Match} -> {Tag,Match,[]} end; - {noline, Rest} -> + {noline,Rest} -> {nomatch,Rest}; {Line,Rest} -> case match_line(Line,Patterns,false,EO) of diff --git a/lib/common_test/src/ct_telnet_client.erl b/lib/common_test/src/ct_telnet_client.erl index 7329498ed6..2cbcba9c77 100644 --- a/lib/common_test/src/ct_telnet_client.erl +++ b/lib/common_test/src/ct_telnet_client.erl @@ -143,7 +143,9 @@ loop(State, Sock, Acc) -> State end; _ -> - Pid ! {data,lists:reverse(lists:append(Acc))}, + Data = lists:reverse(lists:append(Acc)), + dbg("get_data ~p\n",[Data]), + Pid ! {data,Data}, State end, loop(NewState, Sock, []); @@ -161,7 +163,9 @@ loop(State, Sock, Acc) -> NewAcc = case erlang:is_process_alive(Pid) of true -> - Pid ! {data,lists:reverse(lists:append(Acc))}, + Data = lists:reverse(lists:append(Acc)), + dbg("get_data_delayed ~p\n",[Data]), + Pid ! {data,Data}, []; false -> Acc diff --git a/lib/common_test/src/unix_telnet.erl b/lib/common_test/src/unix_telnet.erl index 71df2ab44e..88199b07d0 100644 --- a/lib/common_test/src/unix_telnet.erl +++ b/lib/common_test/src/unix_telnet.erl @@ -94,16 +94,11 @@ connect(Ip,Port,Timeout,KeepAlive,Extra) -> {Username,Password} -> connect1(Ip,Port,Timeout,KeepAlive,Username,Password); Name -> - case not_require_user_and_pass(Name) of - true -> - connect_without_username_and_pass(Ip,Port,Timeout,KeepAlive); - _ -> - case get_username_and_password(Name) of - {ok,{Username,Password}} -> - connect1(Ip,Port,Timeout,KeepAlive,Username,Password); - Error -> - Error - end + case get_username_and_password(Name) of + {ok,{Username,Password}} -> + connect1(Ip,Port,Timeout,KeepAlive,Username,Password); + Error -> + Error end end. @@ -149,27 +144,6 @@ connect1(Ip,Port,Timeout,KeepAlive,Username,Password) -> end_log(), Result. -connect_without_username_and_pass(Ip,Port,Timeout,KeepAlive) -> - start_log("unix_telnet:connect"), - Result = - case ct_telnet_client:open(Ip,Port,Timeout,KeepAlive) of - {ok,Pid} -> - {ok, Pid}; - Error -> - cont_log("Could not open telnet connection\n~p\n",[Error]), - Error - end, - end_log(), - Result. - -not_require_user_and_pass(Name) -> - case ct:get_config({Name, not_require_user_and_pass}) of - undefined -> - false; - _ -> - true - end. - get_username_and_password(Name) -> case ct:get_config({Name,username}) of undefined -> diff --git a/lib/common_test/test/Makefile b/lib/common_test/test/Makefile index 94569fa87f..31ab28c41d 100644 --- a/lib/common_test/test/Makefile +++ b/lib/common_test/test/Makefile @@ -28,6 +28,7 @@ MODULES= \ ct_test_support \ ct_test_support_eh \ ct_userconfig_callback \ + telnet_server \ ct_smoke_test_SUITE \ ct_priv_dir_SUITE \ ct_event_handler_SUITE \ diff --git a/lib/common_test/test/ct_telnet_SUITE.erl b/lib/common_test/test/ct_telnet_SUITE.erl index b4f24baa0c..17617f59eb 100644 --- a/lib/common_test/test/ct_telnet_SUITE.erl +++ b/lib/common_test/test/ct_telnet_SUITE.erl @@ -33,6 +33,10 @@ -define(eh, ct_test_support_eh). +-define(erl_telnet_server_port,1234). +-define(erl_telnet_server_user,"telnuser"). +-define(erl_telnet_server_pwd,"telnpwd"). + %%-------------------------------------------------------------------- %% TEST SERVER CALLBACK FUNCTIONS %%-------------------------------------------------------------------- @@ -48,17 +52,27 @@ init_per_suite(Config) -> end_per_suite(Config) -> ct_test_support:end_per_suite(Config). +init_per_testcase(TestCase, Config) when TestCase=/=unix_telnet-> + TS = telnet_server:start([{port,?erl_telnet_server_port}, + {users,[{?erl_telnet_server_user, + ?erl_telnet_server_pwd}]}]), + ct_test_support:init_per_testcase(TestCase, [{telnet_server,TS}|Config]); init_per_testcase(TestCase, Config) -> ct_test_support:init_per_testcase(TestCase, Config). end_per_testcase(TestCase, Config) -> + case ?config(telnet_server,Config) of + undefined -> ok; + TS -> telnet_server:stop(TS) + end, ct_test_support:end_per_testcase(TestCase, Config). suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [ - default + unix_telnet, + own_server ]. %%-------------------------------------------------------------------- @@ -67,19 +81,29 @@ all() -> %%%----------------------------------------------------------------- %%% -default(Config) when is_list(Config) -> - DataDir = ?config(data_dir, Config), - Suite = filename:join(DataDir, "ct_telnet_basic_SUITE"), - Cfg = {unix, ct:get_config(unix)}, - ok = file:write_file(filename:join(DataDir, "telnet.cfg"), io_lib:write(Cfg) ++ "."), - CfgFile = filename:join(DataDir, "telnet.cfg"), - {Opts,ERPid} = setup([{suite,Suite},{label,default}, {config, CfgFile}], Config), - ok = execute(default, Opts, ERPid, Config). +unix_telnet(Config) when is_list(Config) -> + all_tests_in_suite(unix_telnet,"ct_telnet_basic_SUITE","telnet.cfg",Config). + +own_server(Config) -> + all_tests_in_suite(own_server,"ct_telnet_own_server_SUITE", + "telnet2.cfg",Config). %%%----------------------------------------------------------------- %%% HELP FUNCTIONS %%%----------------------------------------------------------------- +all_tests_in_suite(TestCase, SuiteName, CfgFileName, Config) -> + DataDir = ?config(data_dir, Config), + Suite = filename:join(DataDir, SuiteName), + CfgFile = filename:join(DataDir, CfgFileName), + Cfg = telnet_config(TestCase), + ok = file:write_file(CfgFile, io_lib:write(Cfg) ++ "."), + {Opts,ERPid} = setup([{suite,Suite}, + {label,TestCase}, + {config,CfgFile}], + Config), + ok = execute(TestCase, Opts, ERPid, Config). + setup(Test, Config) -> Opts0 = ct_test_support:get_opts(Config), Level = ?config(trace_level, Config), @@ -103,19 +127,34 @@ execute(Name, Opts, ERPid, Config) -> reformat(Events, EH) -> ct_test_support:reformat(Events, EH). + +telnet_config(unix_telnet) -> + {unix, ct:get_config(unix)}; +telnet_config(_) -> + {unix,[{telnet,"localhost"}, + {port, ?erl_telnet_server_port}, + {username,?erl_telnet_server_user}, + {password,?erl_telnet_server_pwd}, + {keep_alive,true}]}. + %%%----------------------------------------------------------------- %%% TEST EVENTS %%%----------------------------------------------------------------- -events_to_check(default,Config) -> +events_to_check(unix_telnet,Config) -> + all_cases(ct_telnet_basic_SUITE,Config); +events_to_check(own_server,Config) -> + all_cases(ct_telnet_own_server_SUITE,Config). + +all_cases(Suite,Config) -> {module,_} = code:load_abs(filename:join(?config(data_dir,Config), - ct_telnet_basic_SUITE)), - TCs = ct_telnet_basic_SUITE:all(), - code:purge(ct_telnet_basic_SUITE), - code:delete(ct_telnet_basic_SUITE), + Suite)), + TCs = Suite:all(), + code:purge(Suite), + code:delete(Suite), OneTest = [{?eh,start_logging,{'DEF','RUNDIR'}}] ++ - [{?eh,tc_done,{ct_telnet_basic_SUITE,TC,ok}} || TC <- TCs] ++ + [{?eh,tc_done,{Suite,TC,ok}} || TC <- TCs] ++ [{?eh,stop_logging,[]}], %% 2 tests (ct:run_test + script_start) is default diff --git a/lib/common_test/test/ct_telnet_SUITE_data/ct_telnet_own_server_SUITE.erl b/lib/common_test/test/ct_telnet_SUITE_data/ct_telnet_own_server_SUITE.erl new file mode 100644 index 0000000000..3f7c0d68bf --- /dev/null +++ b/lib/common_test/test/ct_telnet_SUITE_data/ct_telnet_own_server_SUITE.erl @@ -0,0 +1,184 @@ +-module(ct_telnet_own_server_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%-------------------------------------------------------------------- +%% TEST SERVER CALLBACK FUNCTIONS +%%-------------------------------------------------------------------- + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + + +suite() -> [{require,erl_telnet_server,{unix,[telnet]}}]. + +all() -> + [expect, + expect_repeat, + expect_sequence, + expect_error_prompt, + expect_error_timeout, + no_prompt_check, + no_prompt_check_repeat, + no_prompt_check_sequence, + no_prompt_check_timeout, + ignore_prompt, + ignore_prompt_repeat, + ignore_prompt_sequence, + ignore_prompt_timeout]. + +groups() -> + []. + +init_per_group(_GroupName, Config) -> + Config. + +end_per_group(_GroupName, Config) -> + Config. + +%% Simple expect +expect(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo ayt"), + {ok,["ayt"]} = ct_telnet:expect(Handle, ["ayt"]), + ok = ct_telnet:close(Handle), + ok. + +%% Expect with repeat option +expect_repeat(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml xy xy"), + {ok,[["xy"],["xy"]],done} = ct_telnet:expect(Handle, ["xy"],[{repeat,2}]), + ok = ct_telnet:close(Handle), + ok. + +%% Expect with sequence option +expect_sequence(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml ab cd ef"), + {ok,[["ab"],["cd"],["ef"]]} = ct_telnet:expect(Handle, + [["ab"],["cd"],["ef"]], + [sequence]), + ok = ct_telnet:close(Handle), + ok. + +%% Check that expect returns when a prompt is found, even if pattern +%% is not matched. +expect_error_prompt(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo xxx> yyy"), + {error,{prompt,"> "}} = ct_telnet:expect(Handle, ["yyy"]), + ok = ct_telnet:close(Handle), + ok. + +%% Check that expect returns after idle timeout, and even if the +%% expected pattern is received - as long as not newline or prompt is +%% received it will not match. +expect_error_timeout(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_no_prompt xxx"), + {error,timeout} = ct_telnet:expect(Handle, ["xxx"], [{timeout,1000}]), + ok = ct_telnet:close(Handle), + ok. + +%% expect with ignore_prompt option should not return even if a prompt +%% is found. The pattern after the prompt (here "> ") can be matched. +ignore_prompt(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo xxx> yyy"), + {ok,["yyy"]} = ct_telnet:expect(Handle, ["yyy"], [ignore_prompt]), + ok = ct_telnet:close(Handle), + ok. + +%% expect with ignore_prompt and repeat options. +ignore_prompt_repeat(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml yyy> yyy>"), + {ok,[["yyy"],["yyy"]],done} = ct_telnet:expect(Handle, ["yyy"], + [{repeat,2}, + ignore_prompt]), + ok = ct_telnet:close(Handle), + ok. + +%% expect with ignore_prompt and sequence options. +ignore_prompt_sequence(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml xxx> yyy> zzz> "), + {ok,[["xxx"],["yyy"],["zzz"]]} = ct_telnet:expect(Handle, + [["xxx"],["yyy"],["zzz"]], + [sequence, + ignore_prompt]), + ok = ct_telnet:close(Handle), + ok. + +%% Check that expect returns after idle timeout when ignore_prompt +%% option is used. +%% As for expect without the ignore_prompt option, it a newline or a +%% prompt is required in order for the pattern to match. +ignore_prompt_timeout(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo xxx"), + {error,timeout} = ct_telnet:expect(Handle, ["yyy"], [ignore_prompt, + {timeout,1000}]), + ok = ct_telnet:send(Handle, "echo xxx"), % sends prompt and newline + {ok,["xxx"]} = ct_telnet:expect(Handle, ["xxx"], [ignore_prompt, + {timeout,1000}]), + ok = ct_telnet:send(Handle, "echo_no_prompt xxx\n"), % no prompt, but newline + {ok,["xxx"]} = ct_telnet:expect(Handle, ["xxx"], [ignore_prompt, + {timeout,1000}]), + ok = ct_telnet:send(Handle, "echo_no_prompt xxx"), % no prompt, no newline + {error,timeout} = ct_telnet:expect(Handle, ["xxx"], [ignore_prompt, + {timeout,1000}]), + ok = ct_telnet:close(Handle), + ok. + +%% no_prompt_check option shall match pattern both when prompt is sent +%% and when it is not. +no_prompt_check(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo xxx"), + {ok,["xxx"]} = ct_telnet:expect(Handle, ["xxx"], [no_prompt_check]), + ok = ct_telnet:send(Handle, "echo_no_prompt yyy"), + {ok,["yyy"]} = ct_telnet:expect(Handle, ["yyy"], [no_prompt_check]), + ok = ct_telnet:close(Handle), + ok. + +%% no_prompt_check and repeat options +no_prompt_check_repeat(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml xxx xxx"), + {ok,[["xxx"],["xxx"]],done} = ct_telnet:expect(Handle,["xxx"], + [{repeat,2}, + no_prompt_check]), + ok = ct_telnet:send(Handle, "echo_ml_no_prompt yyy yyy"), + {ok,[["yyy"],["yyy"]],done} = ct_telnet:expect(Handle,["yyy"], + [{repeat,2}, + no_prompt_check]), + ok = ct_telnet:close(Handle), + ok. + +%% no_prompt_check and sequence options +no_prompt_check_sequence(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo_ml_no_prompt ab cd ef"), + {ok,[["ab"],["cd"],["ef"]]} = ct_telnet:expect(Handle, + [["ab"],["cd"],["ef"]], + [sequence, + no_prompt_check]), + ok = ct_telnet:close(Handle), + ok. + +%% Check that expect returns after idle timeout when no_prompt_check +%% option is used. +no_prompt_check_timeout(_) -> + {ok, Handle} = ct_telnet:open(erl_telnet_server), + ok = ct_telnet:send(Handle, "echo xxx"), + {error,timeout} = ct_telnet:expect(Handle, ["yyy"], [no_prompt_check, + {timeout,1000}]), + ok = ct_telnet:close(Handle), + ok. diff --git a/lib/common_test/test/telnet_server.erl b/lib/common_test/test/telnet_server.erl new file mode 100644 index 0000000000..31884aa182 --- /dev/null +++ b/lib/common_test/test/telnet_server.erl @@ -0,0 +1,228 @@ +-module(telnet_server). +-compile(export_all). + +%% telnet control characters +-define(SE, 240). +-define(NOP, 241). +-define(DM, 242). +-define(BRK, 243). +-define(IP, 244). +-define(AO, 245). +-define(AYT, 246). +-define(EC, 247). +-define(EL, 248). +-define(GA, 249). +-define(SB, 250). +-define(WILL, 251). +-define(WONT, 252). +-define(DO, 253). +-define(DONT, 254). +-define(IAC, 255). + +%% telnet options +-define(BINARY, 0). +-define(ECHO, 1). +-define(SUPPRESS_GO_AHEAD, 3). +-define(TERMINAL_TYPE, 24). + +-record(state, + {listen, + client, + users, + authorized=false, + suppress_go_ahead=false, + buffer=[]}). + +-type options() :: [{port,pos_integer()} | {users,users()}]. +-type users() :: [{user(),password()}]. +-type user() :: string(). +-type password() :: string(). + +-spec start(Opts::options()) -> Pid::pid(). +start(Opts) when is_list(Opts) -> + spawn_link(fun() -> init(Opts) end). + +-spec stop(Pid::pid()) -> ok. +stop(Pid) -> + Ref = erlang:monitor(process,Pid), + Pid ! stop, + receive {'DOWN',Ref,_,_,_} -> ok end. + +init(Opts) -> + Port = proplists:get_value(port,Opts), + Users = proplists:get_value(users,Opts,[]), + {ok, LSock} = gen_tcp:listen(Port, [list, {packet, 0}, + {active, true}]), + State = #state{listen=LSock,users=Users}, + accept(State), + ok = gen_tcp:close(LSock). + +accept(#state{listen=LSock}=State) -> + Server = self(), + Acceptor = spawn_link(fun() -> do_accept(LSock,Server) end), + receive + {Acceptor,Sock} when is_port(Sock) -> + case init_client(State#state{client=Sock}) of + stopped -> + io:format("telnet_server stopped\n"), + ok; + R -> + io:format("connection to client closed with reason ~p~n",[R]), + accept(State) + end; + {Acceptor,closed} -> + io:format("listen socket closed unexpectedly, " + "terminating telnet_server\n"), + ok; + stop -> + io:format("telnet_server stopped\n"), + ok + end. + +do_accept(LSock,Server) -> + case gen_tcp:accept(LSock) of + {ok, Sock} -> + ok = gen_tcp:controlling_process(Sock,Server), + Server ! {self(),Sock}; + {error,closed} -> + %% This will happen when stop/1 is called, since listen + %% socket is closed. Then the server is probably already + %% dead by now, but to be 100% sure not to hang, we send a + %% message to say what happened. + Server ! {self(),closed} + end. + +init_client(#state{client=Sock}=State) -> + dbg("Server sending: ~p~n",["login: "]), + R = case gen_tcp:send(Sock,"login: ") of + ok -> + loop(State); + Error -> + Error + end, + _ = gen_tcp:close(Sock), + R. + +loop(State) -> + receive + {tcp,_,Data} -> + try handle_data(Data,State) of + {ok,State1} -> + loop(State1) + catch + throw:Error -> + Error + end; + {tcp_closed, _} -> + closed; + {tcp_error,_,Error} -> + {error,tcp,Error}; + stop -> + stopped + end. + +handle_data([?IAC|Cmd],State) -> + dbg("Server got cmd: ~p~n",[Cmd]), + handle_cmd(Cmd,State); +handle_data(Data,State) -> + dbg("Server got data: ~p~n",[Data]), + case get_line(Data,[]) of + {Line,Rest} -> + WholeLine = lists:flatten(lists:reverse(State#state.buffer,Line)), + {ok,State1} = do_handle_data(WholeLine,State), + case Rest of + [] -> {ok,State1}; + _ -> handle_data(Rest,State1) + end; + false -> + {ok,State#state{buffer=[Data|State#state.buffer]}} + end. + +%% Add function clause below to handle new telnet commands (sent with +%% ?IAC from client - this is not possible to do from ct_telnet API, +%% but ct_telnet sends DONT SUPPRESS_GO_AHEAD) +handle_cmd([?DO,?SUPPRESS_GO_AHEAD|T],State) -> + send([?IAC,?WILL,?SUPPRESS_GO_AHEAD],State), + handle_cmd(T,State#state{suppress_go_ahead=true}); +handle_cmd([?DONT,?SUPPRESS_GO_AHEAD|T],State) -> + send([?IAC,?WONT,?SUPPRESS_GO_AHEAD],State), + handle_cmd(T,State#state{suppress_go_ahead=false}); +handle_cmd([?IAC|T],State) -> + %% Multiple commands in one packet + handle_cmd(T,State); +handle_cmd([_H|T],State) -> + %% Not responding to this command + handle_cmd(T,State); +handle_cmd([],State) -> + {ok,State}. + +%% Add function clause below to handle new text command (text entered +%% from the telnet prompt) +do_handle_data(Data,#state{authorized=false}=State) -> + check_user(Data,State); +do_handle_data(Data,#state{authorized={user,_}}=State) -> + check_pwd(Data,State); +do_handle_data("echo "++ Data,State) -> + send(Data++"\r\n> ",State), + {ok,State}; +do_handle_data("echo_no_prompt "++ Data,State) -> + send(Data,State), + {ok,State}; +do_handle_data("echo_ml "++ Data,State) -> + Lines = string:tokens(Data," "), + ReturnData = string:join(Lines,"\n"), + send(ReturnData++"\r\n> ",State), + {ok,State}; +do_handle_data("echo_ml_no_prompt "++ Data,State) -> + Lines = string:tokens(Data," "), + ReturnData = string:join(Lines,"\n"), + send(ReturnData,State), + {ok,State}; +do_handle_data([],State) -> + send("> ",State), + {ok,State}; +do_handle_data(_Data,State) -> + send("error: unknown command\r\n> ",State), + {ok,State}. + +check_user(User,State) -> + case lists:keyfind(User,1,State#state.users) of + {User,Pwd} -> + dbg("user ok\n"), + send("Password: ",State), + {ok,State#state{authorized={user,Pwd}}}; + false -> + throw({error,unknown_user}) + end. + +check_pwd(Pwd,#state{authorized={user,Pwd}}=State) -> + dbg("password ok\n"), + send("Welcome to the ultimate telnet server!\r\n> ",State), + {ok,State#state{authorized=true}}; +check_pwd(_,_State) -> + throw({error,authentication}). + +send(Data,State) -> + dbg("Server sending: ~p~n",[Data]), + case gen_tcp:send(State#state.client,Data) of + ok -> + ok; + {error,Error} -> + throw({error,send,Error}) + end. + +get_line([$\r,$\n|Rest],Acc) -> + {lists:reverse(Acc),Rest}; +get_line([$\r,0|Rest],Acc) -> + {lists:reverse(Acc),Rest}; +get_line([$\n|Rest],Acc) -> + {lists:reverse(Acc),Rest}; +get_line([H|T],Acc) -> + get_line(T,[H|Acc]); +get_line([],_) -> + false. + +dbg(_F) -> + io:format(_F). +dbg(_F,_A) -> + io:format(_F,_A). |