unit IdTestTcpClient;

//todo add test to TIdCustomTCPServer to ensure OnDisconnect is called
//when connection is closed, also when exception happens, eg graceful close

{
observations:

  IdDefTimeout = 0;
  IdTimeoutDefault = -1;
  IdTimeoutInfinite = -2;

IdTimeoutDefault when passed as a default param means 'use Self.ReadTimeout' ?

From help file:

IdDefTimeout
Default timeout value when establishing socket-based connections.
IdDefTimeout is an Integer constant that represents the value to
use as the default timeout value for socket-based connection operations.
used in
  TIdIOHandlerStack.ReadTimeout,
  TIdFTP.TransferTimeout (but actually sets FDataChannel.IOHandler.ReadTimeout)

property TIdIOHandlerStack.ReadTimeout: Integer;
Indicates the milliseconds to wait for a readable IOHandler connection.
If ReadTimeout contains the value 0 (zero), the value in IdTimeoutInfinite
is used as the timeout interval.

TIdIOHandlerStack
  published
    property ReadTimeout default IdDefTimeout;
but actually initialised to:
  FReadTimeOut := IdTimeoutDefault;

It can seem more intuitive that timeout=0 means immediate timeout?
or assume it means 'no timeout' eg infinite
currently have 2 options for infinite, none for immediate

how to test that a client is actually using infinite timeout?
}

interface

uses
  IdTest,
  IdStack,
  IdComponent,
  IdGlobal,
  IdExceptionCore,
  IdTcpClient,
  IdObjs,
  IdSys,
  IdThreadSafe,
  IdTcpServer,
  IdContext;

type

  TIdTestTcpClient = class(TIdTest)
  private
    FList:TIdThreadSafeStringList;
    FServerShouldEcho: Boolean;
    procedure DoServerExecute(AContext: TIdContext);
    procedure CallbackConnect(Sender:TObject);
    procedure CallbackDisconnect(Sender:TObject);
    procedure CallbackStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string);
  published
    procedure TestTimeouts;
    procedure TestConnectErrors;
    procedure TestEvent;
  end;

implementation

procedure TIdTestTcpClient.CallbackConnect(Sender: TObject);
var
 astr:string;
begin
 aStr:='OnConnect: Sender='+sender.Classname;
 FList.Add(astr);
end;

procedure TIdTestTcpClient.CallbackDisconnect(Sender: TObject);
var
 astr:string;
begin
 aStr:='OnDisconnect: Sender='+Sender.ClassName;
 FList.Add(astr);
end;

procedure TIdTestTcpClient.CallbackStatus(ASender: TObject;
  const AStatus: TIdStatus; const AStatusText: string);
var
 astr:string;
begin
 aStr:='OnStatus: Sender='+ASender.ClassName+' Text='+AStatusText;
 FList.Add(aStr);
end;

procedure TIdTestTcpClient.DoServerExecute(AContext: TIdContext);
var
  aStr:string;
begin
  aStr:=AContext.Connection.IOHandler.Readln;
  if FServerShouldEcho then
  begin
    AContext.Connection.IOHandler.WriteLn(aStr);
  end;

  if aStr='normal' then
   begin
   AContext.Connection.IOHandler.WriteLn('reply');
   end
  else if aStr='dropme' then
   begin
   AContext.Connection.Disconnect;
   end;
end;

procedure TIdTestTcpClient.TestConnectErrors;
//checks that exceptions are raised as expected
var
  aClient:TIdTCPClient;
  aExpected:Boolean;
begin

  aClient:=TIdTCPClient.Create(nil);
  try

  //no host given
  try
  aExpected:=False;
  aClient.Port:=80;
  aClient.Host:='';
  aClient.Connect;
  except
  on e:Exception do
   begin
   aExpected:=e is EIdHostRequired;
   end;
  end;
  Assert(aExpected);

  //no port given
  try
  aExpected:=False;
  aClient.Port:=0;
  aClient.Host:='127.0.0.1';
  aClient.Connect;
  except
  on e:Exception do
   begin
   aExpected:=e is EIdPortRequired;
   end;
  end;
  Assert(aExpected);

  //hoping that this port is unused (its the max port number)
  try
  aExpected:=False;
  aClient.Port:=65535;
  aClient.Host:='127.0.0.1';
  //odd. specifying timeout gives EIdNotASocket
  //aClient.ConnectTimeout:=500;
  aClient.Connect;
  except
  on e:Exception do
   begin
   aExpected:=e is EIdSocketError;
   end;
  end;
  Assert(aExpected);

  finally
  Sys.FreeAndNil(aClient);
  end;
end;

function ListToCommaText(const aSafe:TIdThreadSafeStringList):string;
begin
 with aSafe.Lock do
  try
  Result:=CommaText;
  finally
  aSafe.Unlock;
  end;

end;

procedure ListToDebug(const aList:TIdStringList);
var
  i:Integer;
begin
  for i:=0 to aList.Count-1 do
    begin
    DebugOutput(aList[i]);
    end;
end;

procedure SafeListToDebug(const aSafe:TIdThreadSafeStringList);
var
 aList:TIdStringList;
begin
 aList:=aSafe.Lock;
 try
 ListToDebug(aList);
 finally
 aSafe.Unlock;
 end;
end;

procedure TIdTestTcpClient.TestEvent;
var
 aClient:TIdTCPClient;
 aServer:TIdTCPServer;
 aStr:string;
 //TIdIOHandlerStack.CheckForDisconnect( commented disconnect
begin
 FList:=TIdThreadSafeStringList.Create;
 aClient:=TIdTCPClient.Create;
 aServer:=TIdTCPServer.Create;
 try
 aServer.OnExecute:=Self.DoServerExecute;
 aServer.DefaultPort:=12121;
 aServer.Active:=True;

 aClient.OnStatus:=Self.CallbackStatus;
 aClient.OnDisconnected:=Self.CallbackDisconnect;
 aClient.OnConnected:=Self.CallbackConnect;
 aClient.Host:='127.0.0.1';
 aClient.Port:=12121;

 //scenario #1
 DebugOutput('Client Disconnects:');

 FList.Clear;

 aClient.Connect;
 aClient.IOHandler.WriteLn('normal');
 aStr:=aClient.IOHandler.Readln;
 aClient.Disconnect;

 SafeListToDebug(FList);

 //scenario #2
 DebugOutput('Server Disconnects:');

 FList.Clear;

 try
 aClient.Connect;
 aClient.IOHandler.WriteLn('dropme');
 aStr:=aClient.IOHandler.Readln;
 except
 //ignore the graceful disconnect exception
 end;

 SafeListToDebug(FList);

 //aStr:=ListToCommaText(FList);
 //Assert(aStr='',aStr);

 finally
 sys.FreeAndNil(aClient);
 sys.FreeAndNil(aServer);
 sys.FreeAndNil(FList);
 end;
end;

procedure TIdTestTcpClient.TestTimeouts;
const
  cServerPort = 20200;
var
  FServer: TIdTcpServer;
  FClient: TIdTcpClient;
  LResult: string;
  //LStart: Integer;
  //LEnd: Integer;
begin
  FServerShouldEcho := False;
  FServer := TIdTcpServer.Create;
  try
    FServer.DefaultPort := cServerPort;
    FServer.OnExecute := DoServerExecute;
    FServer.Active := true;
    FClient := TIdTcpClient.Create;
    try
      FClient.Connect('127.0.0.1', cServerPort);
      FClient.ReadTimeout := 500;

      LResult:=FClient.IOHandler.Readln;
      Assert(LResult = '');
      Assert(FClient.IOHandler.ReadLnTimedOut);

      FServerShouldEcho := True;
      FClient.IOHandler.WriteLn('Hello, World!');
      LResult := FClient.IOHandler.ReadLn;
      Assert(LResult = 'Hello, World!', LResult);
{
  Make some test regarding the readtimeout property and the atimeout param
  of some methods.

  So the methods needing tests include:
    WriteChar
    ReadChar
    WriteString
    ReadString
    WriteBytes
    ReadBytes
    WriteInteger
    ReadInteger

  Try for some of them the timeout stuff. the ReadTimeout property is the default.
  It defaults to -1 (IdTimeoutInfinite). When ReadTimeout is set, that's the
  default for all read* methods. when a specific read method has a ATimeout property,
  try setting that one to see if it overrides the ReadTimeout property.

  the length of timeout should also be measured.
  eg set timeout to 3000. read. check that the read method blocked for between 2 and 4 seconds.
  this ensures that timeouts aren't occurring instantly.
  needs a portable timing mechanism? gettickcount on windows?
}
    finally
      Sys.FreeAndNil(FClient);
    end;
  finally
    Sys.FreeAndNil(FServer);
  end;
end;

initialization
  TIdTest.RegisterTest(TIdTestTcpClient);
end.