From eaead6bf3d330fa909ca5feb33fb211d71891c20 Mon Sep 17 00:00:00 2001 From: Daniel Goertzen Date: Wed, 5 Sep 2012 08:25:18 -0500 Subject: ssh: add test suite for testing channel send/receive behavior. --- lib/ssh/test/Makefile | 3 +- lib/ssh/test/ssh_connection_SUITE.erl | 386 ++++++++++++++++++++++++++++++++++ 2 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 lib/ssh/test/ssh_connection_SUITE.erl (limited to 'lib/ssh') diff --git a/lib/ssh/test/Makefile b/lib/ssh/test/Makefile index 25072688ad..4be49da435 100644 --- a/lib/ssh/test/Makefile +++ b/lib/ssh/test/Makefile @@ -36,7 +36,8 @@ MODULES= \ ssh_to_openssh_SUITE \ ssh_sftp_SUITE \ ssh_sftpd_SUITE \ - ssh_sftpd_erlclient_SUITE + ssh_sftpd_erlclient_SUITE \ + ssh_connection_SUITE HRL_FILES_NEEDED_IN_TEST= \ $(ERL_TOP)/lib/ssh/src/ssh.hrl \ diff --git a/lib/ssh/test/ssh_connection_SUITE.erl b/lib/ssh/test/ssh_connection_SUITE.erl new file mode 100644 index 0000000000..5cd892def8 --- /dev/null +++ b/lib/ssh/test/ssh_connection_SUITE.erl @@ -0,0 +1,386 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2008-2012. All Rights Reserved. +%% +%% The contents of this file are subject to the Erlang Public License, +%% Version 1.1, (the "License"); you may not use this file except in +%% compliance with the License. You should have received a copy of the +%% Erlang Public License along with this software. If not, it can be +%% retrieved online at http://www.erlang.org/. +%% +%% Software distributed under the License is distributed on an "AS IS" +%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%% the License for the specific language governing rights and limitations +%% under the License. +%% +%% %CopyrightEnd% +%% + +%% +-module(ssh_connection_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include("test_server_line.hrl"). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +-define(SSH_DEFAULT_PORT, 22). +-define(EXEC_TIMEOUT, 10000). + +%% Test server callback functions +%%-------------------------------------------------------------------- +%% Function: init_per_suite(Config) -> Config +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Initialization before the whole suite +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + case catch crypto:start() of + ok -> + case gen_tcp:connect("localhost", 22, []) of + {error,econnrefused} -> + {skip,"No openssh deamon"}; + _ -> + Config + end; + _Else -> + {skip,"Could not start crypto!"} + end. + +%%-------------------------------------------------------------------- +%% Function: end_per_suite(Config) -> _ +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Cleanup after the whole suite +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + crypto:stop(), + ok. + +%%-------------------------------------------------------------------- +%% Function: init_per_testcase(TestCase, Config) -> Config +%% Case - atom() +%% Name of the test case that is about to be run. +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% +%% Description: Initialization before each test case +%% +%% Note: This function is free to add any key/value pairs to the Config +%% variable, but should NOT alter/remove any existing entries. +%% Description: Initialization before each test case +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + ssh:start(), + Config. + +%%-------------------------------------------------------------------- +%% Function: end_per_testcase(TestCase, Config) -> _ +%% Case - atom() +%% Name of the test case that is about to be run. +%% Config - [tuple()] +%% A list of key/value pairs, holding the test case configuration. +%% Description: Cleanup after each test case +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ssh:stop(), + ok. + +%%-------------------------------------------------------------------- +%% Function: all(Clause) -> TestCases +%% Clause - atom() - suite | doc +%% TestCases - [Case] +%% Case - atom() +%% Name of a test case. +%% Description: Returns a list of all test cases in this test suite +%%-------------------------------------------------------------------- +all() -> + case os:find_executable("ssh") of + false -> + {skip, "openSSH not installed on host"}; + _ -> + [{group, erlang_client} + ] + end. + +groups() -> + [{erlang_client, [], [ + simple_exec, + small_cat, + big_cat, + send_after_exit, + interrupted_send + ]}]. + +init_per_group(erlang_server, Config) -> + DataDir = ?config(data_dir, Config), + UserDir = ?config(priv_dir, Config), + ssh_test_lib:setup_dsa_known_host(DataDir, UserDir), + Config; +init_per_group(_, Config) -> + Config. + +end_per_group(erlang_server, Config) -> + UserDir = ?config(priv_dir, Config), + ssh_test_lib:clean_dsa(UserDir), + Config; +end_per_group(_, Config) -> + Config. + +%% TEST cases starts here. +%-------------------------------------------------------------------- +simple_exec(doc) -> + ["Simple openssh connectivity test for ssh_connection:exec"]; + +simple_exec(suite) -> + []; + +simple_exec(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "echo testing", infinity), + + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, <<"testing\n">>}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + + +%-------------------------------------------------------------------- +small_cat(doc) -> + ["Use 'cat' to echo small data block back to us."]; + +small_cat(suite) -> + []; + +small_cat(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "cat", infinity), + + Data = <<"I like spaghetti squash">>, + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% receive response to input + receive + {ssh_cm, ConnectionRef, {data, ChannelId0, 0, Data}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +%-------------------------------------------------------------------- +big_cat(doc) -> + ["Use 'cat' to echo large data block back to us."]; + +big_cat(suite) -> + []; + +big_cat(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "cat", infinity), + + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), + + test_server:format("sending ~p byte binary~n",[size(Data)]), + ok = ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000), + %timer:sleep(3000), + ok = ssh_connection:send_eof(ConnectionRef, ChannelId0), + + %% collect echoed data until eof + case big_cat_rx(ConnectionRef, ChannelId0) of + {ok, Data} -> ok; + {ok, Other} -> + case size(Data) =:= size(Other) of + true -> + test_server:format("received and sent data are same size but do not match~n",[]); + false -> + test_server:format("sent ~p but only received ~p~n",[size(Data), size(Other)]) + end, + test_server:fail(receive_data_mismatch); + Else -> + test_server:fail(Else) + end, + + %% receive close messages (eof already consumed) + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +big_cat_rx(ConnectionRef, ChannelId) -> + big_cat_rx(ConnectionRef, ChannelId, []). + +big_cat_rx(ConnectionRef, ChannelId, Acc) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here + big_cat_rx(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + {ok, iolist_to_binary(lists:reverse(Acc))} + after ?EXEC_TIMEOUT -> timeout + end. + +%-------------------------------------------------------------------- +send_after_exit(doc) -> + ["Send channel data after the channel has been closed."]; + +send_after_exit(suite) -> + []; + +send_after_exit(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + + %% Shell command "false" will exit immediately + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "false", infinity), + + timer:sleep(2000), %% Allow incoming eof/close/exit_status ssh messages to be processed + + Data = <<"I like spaghetti squash">>, + case ssh_connection:send(ConnectionRef, ChannelId0, Data, 2000) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + {error, timeout} -> test_server:fail({expected,{error,closed}}); + Else -> test_server:fail(Else) + end, + + %% receive close messages + receive + {ssh_cm, ConnectionRef, {eof, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, _}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +%-------------------------------------------------------------------- +interrupted_send(doc) -> + ["Use 'head' to cause a channel exit partway through a large send."]; + +interrupted_send(suite) -> + []; + +interrupted_send(Config) when is_list(Config) -> + ConnectionRef = ssh_test_lib:connect(?SSH_DEFAULT_PORT, [{silently_accept_hosts, true}, + {user_interaction, false}]), + {ok, ChannelId0} = ssh_connection:session_channel(ConnectionRef, infinity), + success = ssh_connection:exec(ConnectionRef, ChannelId0, + "head -c 4000000", infinity), + + %% build 10MB binary + Data = << <> || X <- lists:seq(1,2500000)>>, + + %% expect remote end to send us 4MB back + <> = Data, + + %% pre-adjust receive window so the other end doesn't block + ssh_connection:adjust_window(ConnectionRef, ChannelId0, size(Data)), + + test_server:format("sending ~p byte binary~n",[size(Data)]), + + case ssh_connection:send(ConnectionRef, ChannelId0, Data, 10000) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + {error, timeout} -> test_server:fail({expected,{error,closed}}); + SendElse -> test_server:fail(SendElse) + end, + + case ssh_connection:send_eof(ConnectionRef, ChannelId0) of + {error, closed} -> ok; + ok -> test_server:fail({expected,{error,closed}}); + EofElse -> test_server:fail(EofElse) + end, + + %% collect echoed data until eof + case interrupted_send_rx(ConnectionRef, ChannelId0) of + {ok, ExpectedData} -> ok; + {ok, Other} -> + case size(ExpectedData) =:= size(Other) of + true -> + test_server:format("received expected number of bytes, but bytes do not match~n",[]); + false -> + test_server:format("expected ~p but only received ~p~n",[size(ExpectedData), size(Other)]) + end, + test_server:fail(receive_data_mismatch); + RxElse -> + test_server:fail(RxElse) + end, + + %% receive close messages (eof already consumed) + receive + {ssh_cm, ConnectionRef, {exit_status, ChannelId0, 0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end, + receive + {ssh_cm, ConnectionRef,{closed, ChannelId0}} -> ok + after ?EXEC_TIMEOUT -> test_server:fail() + end. + +interrupted_send_rx(ConnectionRef, ChannelId) -> + interrupted_send_rx(ConnectionRef, ChannelId, []). + +interrupted_send_rx(ConnectionRef, ChannelId, Acc) -> + receive + {ssh_cm, ConnectionRef, {data, ChannelId, 0, Data}} -> + %% ssh_connection:adjust_window(ConnectionRef, ChannelId, size(Data)), % window was pre-adjusted, don't adjust again here + interrupted_send_rx(ConnectionRef, ChannelId, [Data | Acc]); + {ssh_cm, ConnectionRef, {eof, ChannelId}} -> + {ok, iolist_to_binary(lists:reverse(Acc))} + after ?EXEC_TIMEOUT -> timeout + end. -- cgit v1.2.3