Thrift-1391: Improved Delphi XE test cases
authorJake Farrell <jfarrell@apache.org>
Thu, 17 Nov 2011 00:17:29 +0000 (00:17 +0000)
committerJake Farrell <jfarrell@apache.org>
Thu, 17 Nov 2011 00:17:29 +0000 (00:17 +0000)
Client: delphi
Patch: Jens Geyer

Improved TestClient:
 - collects test results and reports errors
 - added or completed a number of tests, like listTest, mapmapTest, multiExceptionTest and others
 - exception content test also included

git-svn-id: https://svn.apache.org/repos/asf/thrift/trunk@1202948 13f79535-47bb-0310-9956-ffa450edef68

lib/delphi/test/TestClient.pas

index e8edd82..9ca90d9 100644 (file)
@@ -26,6 +26,7 @@ uses
   DateUtils,\r
   Generics.Collections,\r
   TestConstants,\r
+  Thrift,\r
   Thrift.Protocol.JSON,\r
   Thrift.Protocol,\r
   Thrift.Transport,\r
@@ -51,8 +52,13 @@ type
     FNumIteration : Integer;\r
     FConsole : TThreadConsole;\r
 \r
-    FErrors, FSuccesses : Integer;\r
+    // test reporting, will be refactored out into separate class later\r
+    FTestGroup : string;\r
+    FSuccesses : Integer;\r
+    FErrors : TStringList;\r
+    procedure StartTestGroup( const aGroup : string);\r
     procedure Expect( aTestResult : Boolean; const aTestInfo : string);\r
+    procedure ReportResults;\r
     \r
     procedure ClientTest;\r
     procedure JSONProtocolReadWriteTest;\r
@@ -74,6 +80,14 @@ type
 \r
 implementation\r
 \r
+function BoolToString( b : Boolean) : string;\r
+// overrides global BoolToString()\r
+begin\r
+  if b\r
+  then result := 'true'\r
+  else result := 'false';\r
+end;\r
+\r
 \r
 { TTestClient }\r
 \r
@@ -260,6 +274,8 @@ var
   ret : TNumberz;\r
   uid : Int64;\r
   mm : IThriftDictionary<Integer, IThriftDictionary<Integer, Integer>>;\r
+  pos : IThriftDictionary<Integer, Integer>;\r
+  neg : IThriftDictionary<Integer, Integer>;\r
   m2 : IThriftDictionary<Integer, Integer>;\r
   k2 : Integer;\r
   insane : IInsanity;\r
@@ -276,12 +292,17 @@ var
   arg0 : ShortInt;\r
   arg1 : Integer;\r
   arg2 : Int64;\r
-  multiDict : IThriftDictionary<SmallInt, string>;\r
+  arg3 : IThriftDictionary<SmallInt, string>;\r
   arg4 : TNumberz;\r
   arg5 : Int64;\r
   StartTick : Cardinal;\r
   k : Integer;\r
   proc : TThreadProcedure;\r
+  hello, goodbye : IXtruct;\r
+  crazy : IInsanity;\r
+  looney : IInsanity;\r
+  first_map : IThriftDictionary<TNumberz, IInsanity>;\r
+  second_map : IThriftDictionary<TNumberz, IInsanity>;\r
 \r
 begin\r
   client := TThriftTest.TClient.Create( FProtocol);\r
@@ -298,70 +319,90 @@ begin
     end;\r
   end;\r
 \r
-  Console.Write('testException()');\r
+  // in-depth exception test\r
+  // (1) do we get an exception at all?\r
+  // (2) do we get the right exception?\r
+  // (3) does the exception contain the expected data?\r
+  StartTestGroup( 'testException');\r
   try\r
     client.testException('Xception');\r
+    Expect( FALSE, 'testException(''Xception''): must trow an exception');\r
   except\r
-    on E: TXception do\r
-    begin\r
-      Console.WriteLine( ' = ' + IntToStr(E.ErrorCode) + ', ' + E.Message_ );\r
+    on e:TXception do begin\r
+      Expect( e.ErrorCode = 1001,                  'error code');\r
+      Expect( e.Message_  = 'This is an Xception', 'error message');\r
+      Console.WriteLine( ' = ' + IntToStr(e.ErrorCode) + ', ' + e.Message_ );\r
     end;\r
+    on e:TTransportException do Expect( FALSE, 'Unexpected : "'+e.ToString+'"');\r
+    on e:Exception do Expect( FALSE, 'Unexpected exception type "'+e.ClassName+'"');\r
   end;\r
 \r
-  Console.Write('testVoid()');\r
+  // simple things\r
+  StartTestGroup( 'simple Thrift calls');\r
   client.testVoid();\r
-  Console.WriteLine(' = void');\r
+  Expect( TRUE, 'testVoid()');  // success := no exception\r
 \r
-  Console.Write('testString(''Test'')');\r
   s := client.testString('Test');\r
-  Console.WriteLine(' := ''' + s + '''');\r
+  Expect( s = 'Test', 'testString(''Test'') = "'+s+'"');\r
 \r
-  Console.Write('testByte(1)');\r
   i8 := client.testByte(1);\r
-  Console.WriteLine(' := ' + IntToStr( i8 ));\r
+  Expect( i8 = 1, 'testByte(1) = ' + IntToStr( i8 ));\r
 \r
-  Console.Write('testI32(-1)');\r
   i32 := client.testI32(-1);\r
-  Console.WriteLine(' := ' + IntToStr(i32));\r
+  Expect( i32 = -1, 'testI32(-1) = ' + IntToStr(i32));\r
 \r
-  Console.Write('testI64(-34359738368)');\r
+  Console.WriteLine('testI64(-34359738368)');\r
   i64 := client.testI64(-34359738368);\r
-  Console.WriteLine(' := ' + IntToStr( i64));\r
+  Expect( i64 = -34359738368, 'testI64(-34359738368) = ' + IntToStr( i64));\r
 \r
-  Console.Write('testDouble(5.325098235)');\r
+  Console.WriteLine('testDouble(5.325098235)');\r
   dub := client.testDouble(5.325098235);\r
-  Console.WriteLine(' := ' + FloatToStr( dub));\r
+  Expect( abs(dub-5.325098235) < 1e-14, 'testDouble(5.325098235) = ' + FloatToStr( dub));\r
 \r
-  Console.Write('testStruct({''Zero'', 1, -3, -5})');\r
+  // structs\r
+  StartTestGroup( 'testStruct');\r
+  Console.WriteLine('testStruct({''Zero'', 1, -3, -5})');\r
   o := TXtructImpl.Create;\r
   o.String_thing := 'Zero';\r
   o.Byte_thing := 1;\r
   o.I32_thing := -3;\r
   o.I64_thing := -5;\r
   i := client.testStruct(o);\r
-  Console.WriteLine(' := {''' +\r
-    i.String_thing + ''', ' +\r
-    IntToStr( i.Byte_thing) + ', ' +\r
-    IntToStr( i.I32_thing) + ', ' +\r
-    IntToStr( i.I64_thing) + '}');\r
-\r
-  Console.Write('testNest({1, {''Zero'', 1, -3, -5}, 5})');\r
+  Expect( i.String_thing = 'Zero', 'i.String_thing = "'+i.String_thing+'"');\r
+  Expect( i.Byte_thing = 1, 'i.Byte_thing = '+IntToStr(i.Byte_thing));\r
+  Expect( i.I32_thing = -3, 'i.I32_thing = '+IntToStr(i.I32_thing));\r
+  Expect( i.I64_thing = -5, 'i.I64_thing = '+IntToStr(i.I64_thing));\r
+  Expect( i.__isset_String_thing, 'i.__isset_String_thing = '+BoolToString(i.__isset_String_thing));\r
+  Expect( i.__isset_Byte_thing, 'i.__isset_Byte_thing = '+BoolToString(i.__isset_Byte_thing));\r
+  Expect( i.__isset_I32_thing, 'i.__isset_I32_thing = '+BoolToString(i.__isset_I32_thing));\r
+  Expect( i.__isset_I64_thing, 'i.__isset_I64_thing = '+BoolToString(i.__isset_I64_thing));\r
+\r
+  // nested structs\r
+  StartTestGroup( 'testNest');\r
+  Console.WriteLine('testNest({1, {''Zero'', 1, -3, -5}, 5})');\r
   o2 := TXtruct2Impl.Create;\r
   o2.Byte_thing := 1;\r
   o2.Struct_thing := o;\r
   o2.I32_thing := 5;\r
   i2 := client.testNest(o2);\r
   i := i2.Struct_thing;\r
-  Console.WriteLine(' := {' + IntToStr( i2.Byte_thing) + ', {''' +\r
-    i.String_thing + ''', ' +\r
-    IntToStr( i.Byte_thing) + ', ' +\r
-    IntToStr( i.I32_thing) + ', ' +\r
-    IntToStr( i.I64_thing) + '}, ' +\r
-    IntToStr( i2.I32_thing) + '}');\r
-\r
-\r
+  Expect( i.String_thing = 'Zero', 'i.String_thing = "'+i.String_thing+'"');\r
+  Expect( i.Byte_thing = 1,  'i.Byte_thing = '+IntToStr(i.Byte_thing));\r
+  Expect( i.I32_thing = -3,  'i.I32_thing = '+IntToStr(i.I32_thing));\r
+  Expect( i.I64_thing = -5,  'i.I64_thing = '+IntToStr(i.I64_thing));\r
+  Expect( i2.Byte_thing = 1, 'i2.Byte_thing = '+IntToStr(i2.Byte_thing));\r
+  Expect( i2.I32_thing = 5,  'i2.I32_thing = '+IntToStr(i2.I32_thing));\r
+  Expect( i.__isset_String_thing, 'i.__isset_String_thing = '+BoolToString(i.__isset_String_thing));\r
+  Expect( i.__isset_Byte_thing,  'i.__isset_Byte_thing = '+BoolToString(i.__isset_Byte_thing));\r
+  Expect( i.__isset_I32_thing,  'i.__isset_I32_thing = '+BoolToString(i.__isset_I32_thing));\r
+  Expect( i.__isset_I64_thing,  'i.__isset_I64_thing = '+BoolToString(i.__isset_I64_thing));\r
+  Expect( i2.__isset_Byte_thing, 'i2.__isset_Byte_thing');\r
+  Expect( i2.__isset_I32_thing,  'i2.__isset_I32_thing');\r
+\r
+  // map<type1,type2>: A map of strictly unique keys to values.\r
+  // Translates to an STL map, Java HashMap, PHP associative array, Python/Ruby dictionary, etc.\r
+  StartTestGroup( 'testMap');\r
   mapout := TThriftDictionaryImpl<Integer,Integer>.Create;\r
-\r
   for j := 0 to 4 do\r
   begin\r
     mapout.AddOrSetValue( j, j - 10);\r
@@ -370,33 +411,30 @@ begin
   first := True;\r
   for key in mapout.Keys do\r
   begin\r
-    if first then\r
-    begin\r
-      first := False;\r
-    end else\r
-    begin\r
-      Console.Write( ', ' );\r
-    end;\r
+    if first\r
+    then first := False\r
+    else Console.Write( ', ' );\r
     Console.Write( IntToStr( key) + ' => ' + IntToStr( mapout[key]));\r
   end;\r
-  Console.Write('})');\r
+  Console.WriteLine('})');\r
 \r
   mapin := client.testMap( mapout );\r
-  Console.Write(' = {');\r
-  first := True;\r
+  Expect( mapin.Count = mapout.Count, 'testMap: mapin.Count = mapout.Count');\r
+  for j := 0 to 4 do\r
+  begin\r
+    Expect( mapout.ContainsKey(j), 'testMap: mapout.ContainsKey('+IntToStr(j)+') = '+BoolToString(mapout.ContainsKey(j)));\r
+  end;\r
   for key in mapin.Keys do\r
   begin\r
-    if first then\r
-    begin\r
-      first := False;\r
-    end else\r
-    begin\r
-      Console.Write( ', ' );\r
-    end;\r
-    Console.Write( IntToStr( key) + ' => ' + IntToStr( mapin[key]));\r
+    Expect( mapin[key] = mapout[key], 'testMap: '+IntToStr(key) + ' => ' + IntToStr( mapin[key]));\r
+    Expect( mapin[key] = key - 10, 'testMap: mapin['+IntToStr(key)+'] = '+IntToStr( mapin[key]));\r
   end;\r
-  Console.WriteLine('}');\r
 \r
+\r
+  // set<type>: An unordered set of unique elements.\r
+  // Translates to an STL set, Java HashSet, set in Python, etc.\r
+  // Note: PHP does not support sets, so it is treated similar to a List\r
+  StartTestGroup( 'testSet');\r
   setout := THashSetImpl<Integer>.Create;\r
   for j := -2 to 2 do\r
   begin\r
@@ -406,59 +444,74 @@ begin
   first := True;\r
   for j in setout do\r
   begin\r
-    if first then\r
-    begin\r
-      first := False;\r
-    end else\r
-    begin\r
-      Console.Write(', ');\r
-    end;\r
+    if first\r
+    then first := False\r
+    else Console.Write(', ');\r
     Console.Write(IntToStr( j));\r
   end;\r
-  Console.Write('})');\r
-\r
-  Console.Write(' = {');\r
+  Console.WriteLine('})');\r
 \r
-  first := True;\r
   setin := client.testSet(setout);\r
-  for j in setin do\r
+  Expect( setin.Count = setout.Count, 'testSet: setin.Count = setout.Count');\r
+  Expect( setin.Count = 5, 'testSet: setin.Count = '+IntToStr(setin.Count));\r
+  for j := -2 to 2 do // unordered, we can't rely on the order => test for known elements only\r
   begin\r
-    if first then\r
-    begin\r
-      first := False;\r
-    end else\r
+    Expect( setin.Contains(j), 'testSet: setin.Contains('+IntToStr(j)+') => '+BoolToString(setin.Contains(j)));\r
+  end;\r
+\r
+  // list<type>: An ordered list of elements.\r
+  // Translates to an STL vector, Java ArrayList, native arrays in scripting languages, etc.\r
+  StartTestGroup( 'testList');\r
+  listout := TThriftListImpl<Integer>.Create;\r
+  listout.Add( +1);\r
+  listout.Add( -2);\r
+  listout.Add( +3);\r
+  listout.Add( -4);\r
+  listout.Add( 0);\r
+  Console.Write('testList({');\r
+  first := True;\r
+  for j in listout do\r
     begin\r
-      Console.Write(', ');\r
-    end;\r
+    if first\r
+    then first := False\r
+    else Console.Write(', ');\r
     Console.Write(IntToStr( j));\r
   end;\r
-  Console.WriteLine('}');\r
-\r
-  Console.Write('testEnum(ONE)');\r
+  Console.WriteLine('})');\r
+\r
+  listin := client.testList(listout);\r
+  Expect( listin.Count = listout.Count, 'testList: listin.Count = listout.Count');\r
+  Expect( listin.Count = 5, 'testList: listin.Count = '+IntToStr(listin.Count));\r
+  Expect( listin[0] = +1, 'listin[0] = '+IntToStr( listin[0]));\r
+  Expect( listin[1] = -2, 'listin[1] = '+IntToStr( listin[1]));\r
+  Expect( listin[2] = +3, 'listin[2] = '+IntToStr( listin[2]));\r
+  Expect( listin[3] = -4, 'listin[3] = '+IntToStr( listin[3]));\r
+  Expect( listin[4] = 0,  'listin[4] = '+IntToStr( listin[4]));\r
+\r
+  // enums\r
   ret := client.testEnum(TNumberz.ONE);\r
-  Console.WriteLine(' = ' + IntToStr( Integer( ret)));\r
+  Expect( ret = TNumberz.ONE, 'testEnum(ONE) = '+IntToStr(Ord(ret)));\r
 \r
-  Console.Write('testEnum(TWO)');\r
   ret := client.testEnum(TNumberz.TWO);\r
-  Console.WriteLine(' = ' + IntToStr( Integer( ret)));\r
+  Expect( ret = TNumberz.TWO, 'testEnum(TWO) = '+IntToStr(Ord(ret)));\r
 \r
-  Console.Write('testEnum(THREE)');\r
   ret := client.testEnum(TNumberz.THREE);\r
-  Console.WriteLine(' = ' + IntToStr( Integer( ret)));\r
+  Expect( ret = TNumberz.THREE, 'testEnum(THREE) = '+IntToStr(Ord(ret)));\r
 \r
-  Console.Write('testEnum(FIVE)');\r
   ret := client.testEnum(TNumberz.FIVE);\r
-  Console.WriteLine(' = ' + IntToStr( Integer( ret)));\r
+  Expect( ret = TNumberz.FIVE, 'testEnum(FIVE) = '+IntToStr(Ord(ret)));\r
 \r
-  Console.Write('testEnum(EIGHT)');\r
   ret := client.testEnum(TNumberz.EIGHT);\r
-  Console.WriteLine(' = ' + IntToStr( Integer( ret)));\r
+  Expect( ret = TNumberz.EIGHT, 'testEnum(EIGHT) = '+IntToStr(Ord(ret)));\r
+\r
 \r
-  Console.Write('testTypedef(309858235082523)');\r
+  // typedef\r
   uid := client.testTypedef(309858235082523);\r
-  Console.WriteLine(' = ' + IntToStr( uid));\r
+  Expect( uid = 309858235082523, 'testTypedef(309858235082523) = '+IntToStr(uid));\r
+\r
 \r
-  Console.Write('testMapMap(1)');\r
+  // maps of maps\r
+  StartTestGroup( 'testMapMap(1)');\r
   mm := client.testMapMap(1);\r
   Console.Write(' = {');\r
   for key in mm.Keys do\r
@@ -473,6 +526,20 @@ begin
   end;\r
   Console.WriteLine('}');\r
 \r
+  // verify result data\r
+  Expect( mm.Count = 2, 'mm.Count = '+IntToStr(mm.Count));\r
+  pos := mm[4];\r
+  neg := mm[-4];\r
+  for j := 1 to 4 do\r
+  begin\r
+    Expect( pos[j]  = j,  'pos[j]  = '+IntToStr(pos[j]));\r
+    Expect( neg[-j] = -j, 'neg[-j] = '+IntToStr(neg[-j]));\r
+  end;\r
+\r
+\r
+\r
+  // insanity\r
+  StartTestGroup( 'testInsanity');\r
   insane := TInsanityImpl.Create;\r
   insane.UserMap := TThriftDictionaryImpl<TNumberz, Int64>.Create;\r
   insane.UserMap.AddOrSetValue( TNumberz.FIVE, 5000);\r
@@ -483,7 +550,6 @@ begin
   truck.I64_thing := 8;\r
   insane.Xtructs := TThriftListImpl<IXtruct>.Create;\r
   insane.Xtructs.Add( truck );\r
-  Console.Write('testInsanity()');\r
   whoa := client.testInsanity( insane );\r
   Console.Write(' = {');\r
   for key64 in whoa.Keys do\r
@@ -530,32 +596,140 @@ begin
   end;\r
   Console.WriteLine('}');\r
 \r
+  // verify result data\r
+  Expect( whoa.Count = 2, 'whoa.Count = '+IntToStr(whoa.Count));\r
+  //\r
+  first_map  := whoa[1];\r
+  second_map := whoa[2];\r
+  Expect( first_map.Count = 2, 'first_map.Count = '+IntToStr(first_map.Count));\r
+  Expect( second_map.Count = 1, 'second_map.Count = '+IntToStr(second_map.Count));\r
+  //\r
+  looney := second_map[TNumberz.SIX];\r
+  Expect( Assigned(looney), 'Assigned(looney) = '+BoolToString(Assigned(looney)));\r
+  Expect( not looney.__isset_UserMap, 'looney.__isset_UserMap = '+BoolToString(looney.__isset_UserMap));\r
+  Expect( not looney.__isset_Xtructs, 'looney.__isset_Xtructs = '+BoolToString(looney.__isset_Xtructs));\r
+  //\r
+  for ret in [TNumberz.SIX, TNumberz.THREE] do begin\r
+    crazy := first_map[ret];\r
+    Console.WriteLine('first_map['+intToStr(Ord(ret))+']');\r
+\r
+    Expect( crazy.__isset_UserMap, 'crazy.__isset_UserMap = '+BoolToString(crazy.__isset_UserMap));\r
+    Expect( crazy.__isset_Xtructs, 'crazy.__isset_Xtructs = '+BoolToString(crazy.__isset_Xtructs));\r
+\r
+    Expect( crazy.UserMap.Count = 2, 'crazy.UserMap.Count = '+IntToStr(crazy.UserMap.Count));\r
+    Expect( crazy.UserMap[TNumberz.FIVE] = 5, 'crazy.UserMap[TNumberz.FIVE] = '+IntToStr(crazy.UserMap[TNumberz.FIVE]));\r
+    Expect( crazy.UserMap[TNumberz.EIGHT] = 8, 'crazy.UserMap[TNumberz.EIGHT] = '+IntToStr(crazy.UserMap[TNumberz.EIGHT]));\r
+\r
+    Expect( crazy.Xtructs.Count = 2, 'crazy.Xtructs.Count = '+IntToStr(crazy.Xtructs.Count));\r
+    goodbye := crazy.Xtructs[0];  // lists are ordered, so we are allowed to assume this order\r
+         hello   := crazy.Xtructs[1];\r
+\r
+    Expect( goodbye.String_thing = 'Goodbye4', 'goodbye.String_thing = "'+goodbye.String_thing+'"');\r
+    Expect( goodbye.Byte_thing = 4, 'goodbye.Byte_thing = '+IntToStr(goodbye.Byte_thing));\r
+    Expect( goodbye.I32_thing = 4, 'goodbye.I32_thing = '+IntToStr(goodbye.I32_thing));\r
+    Expect( goodbye.I64_thing = 4, 'goodbye.I64_thing = '+IntToStr(goodbye.I64_thing));\r
+    Expect( goodbye.__isset_String_thing, 'goodbye.__isset_String_thing = '+BoolToString(goodbye.__isset_String_thing));\r
+    Expect( goodbye.__isset_Byte_thing, 'goodbye.__isset_Byte_thing = '+BoolToString(goodbye.__isset_Byte_thing));\r
+    Expect( goodbye.__isset_I32_thing, 'goodbye.__isset_I32_thing = '+BoolToString(goodbye.__isset_I32_thing));\r
+    Expect( goodbye.__isset_I64_thing, 'goodbye.__isset_I64_thing = '+BoolToString(goodbye.__isset_I64_thing));\r
+\r
+    Expect( hello.String_thing = 'hello', 'hello.String_thing = "'+hello.String_thing+'"');\r
+    Expect( hello.Byte_thing = 2, 'hello.Byte_thing = '+IntToStr(hello.Byte_thing));\r
+    Expect( hello.I32_thing = 2, 'hello.I32_thing = '+IntToStr(hello.I32_thing));\r
+    Expect( hello.I64_thing = 2, 'hello.I64_thing = '+IntToStr(hello.I64_thing));\r
+    Expect( hello.__isset_String_thing, 'hello.__isset_String_thing = '+BoolToString(hello.__isset_String_thing));\r
+    Expect( hello.__isset_Byte_thing, 'hello.__isset_Byte_thing = '+BoolToString(hello.__isset_Byte_thing));\r
+    Expect( hello.__isset_I32_thing, 'hello.__isset_I32_thing = '+BoolToString(hello.__isset_I32_thing));\r
+    Expect( hello.__isset_I64_thing, 'hello.__isset_I64_thing = '+BoolToString(hello.__isset_I64_thing));\r
+  end;\r
+\r
+\r
+  // multi args\r
+  StartTestGroup( 'testMulti');\r
   arg0 := 1;\r
   arg1 := 2;\r
   arg2 := High(Int64);\r
-\r
-  multiDict := TThriftDictionaryImpl<SmallInt, string>.Create;\r
-  multiDict.AddOrSetValue( 1, 'one');\r
-\r
+  arg3 := TThriftDictionaryImpl<SmallInt, string>.Create;\r
+  arg3.AddOrSetValue( 1, 'one');\r
   arg4 := TNumberz.FIVE;\r
   arg5 := 5000000;\r
   Console.WriteLine('Test Multi(' + IntToStr( arg0) + ',' +\r
     IntToStr( arg1) + ',' + IntToStr( arg2) + ',' +\r
-    multiDict.ToString + ',' + IntToStr( Integer( arg4)) + ',' +\r
+    arg3.ToString + ',' + IntToStr( Integer( arg4)) + ',' +\r
       IntToStr( arg5) + ')');\r
 \r
-  Console.WriteLine('Test Oneway(1)');\r
+  i := client.testMulti( arg0, arg1, arg2, arg3, arg4, arg5);\r
+  Expect( i.String_thing = 'Hello2', 'testMulti: i.String_thing = "'+i.String_thing+'"');\r
+  Expect( i.Byte_thing = arg0, 'testMulti: i.Byte_thing = '+IntToStr(i.Byte_thing));\r
+  Expect( i.I32_thing = arg1, 'testMulti: i.I32_thing = '+IntToStr(i.I32_thing));\r
+  Expect( i.I64_thing = arg2, 'testMulti: i.I64_thing = '+IntToStr(i.I64_thing));\r
+  Expect( i.__isset_String_thing, 'testMulti: i.__isset_String_thing = '+BoolToString(i.__isset_String_thing));\r
+  Expect( i.__isset_Byte_thing, 'testMulti: i.__isset_Byte_thing = '+BoolToString(i.__isset_Byte_thing));\r
+  Expect( i.__isset_I32_thing, 'testMulti: i.__isset_I32_thing = '+BoolToString(i.__isset_I32_thing));\r
+  Expect( i.__isset_I64_thing, 'testMulti: i.__isset_I64_thing = '+BoolToString(i.__isset_I64_thing));\r
+\r
+  // multi exception\r
+  StartTestGroup( 'testMultiException(1)');\r
+  try\r
+    i := client.testMultiException( 'need more pizza', 'run out of beer');\r
+    Expect( i.String_thing = 'run out of beer', 'i.String_thing = "' +i.String_thing+ '"');\r
+    Expect( i.__isset_String_thing, 'i.__isset_String_thing = '+BoolToString(i.__isset_String_thing));\r
+    Expect( not i.__isset_Byte_thing, 'i.__isset_Byte_thing = '+BoolToString(i.__isset_Byte_thing));\r
+    Expect( not i.__isset_I32_thing, 'i.__isset_I32_thing = '+BoolToString(i.__isset_I32_thing));\r
+    Expect( not i.__isset_I64_thing, 'i.__isset_I64_thing = '+BoolToString(i.__isset_I64_thing));\r
+  except\r
+    on e:Exception do Expect( FALSE, 'Unexpected exception "'+e.ClassName+'"');\r
+  end;\r
+\r
+  StartTestGroup( 'testMultiException(Xception)');\r
+  try\r
+    i := client.testMultiException( 'Xception', 'second test');\r
+    Expect( FALSE, 'testMultiException(''Xception''): must trow an exception');\r
+  except\r
+    on x:TXception do begin\r
+      Expect( x.__isset_ErrorCode, 'x.__isset_ErrorCode = '+BoolToString(x.__isset_ErrorCode));\r
+      Expect( x.__isset_Message_,  'x.__isset_Message_ = '+BoolToString(x.__isset_Message_));\r
+      Expect( x.ErrorCode = 1001, 'x.ErrorCode = '+IntToStr(x.ErrorCode));\r
+      Expect( x.Message_ = 'This is an Xception', 'x.Message = "'+x.Message_+'"');\r
+    end;\r
+    on e:Exception do Expect( FALSE, 'Unexpected exception "'+e.ClassName+'"');\r
+  end;\r
+\r
+  StartTestGroup( 'testMultiException(Xception2)');\r
+  try\r
+    i := client.testMultiException( 'Xception2', 'third test');\r
+    Expect( FALSE, 'testMultiException(''Xception2''): must trow an exception');\r
+  except\r
+    on x:TXception2 do begin\r
+      Expect( x.__isset_ErrorCode, 'x.__isset_ErrorCode = '+BoolToString(x.__isset_ErrorCode));\r
+      Expect( x.__isset_Struct_thing,  'x.__isset_Struct_thing = '+BoolToString(x.__isset_Struct_thing));\r
+      Expect( x.ErrorCode = 2002, 'x.ErrorCode = '+IntToStr(x.ErrorCode));\r
+      Expect( x.Struct_thing.String_thing = 'This is an Xception2', 'x.Struct_thing.String_thing = "'+x.Struct_thing.String_thing+'"');\r
+      Expect( x.Struct_thing.__isset_String_thing, 'x.Struct_thing.__isset_String_thing = '+BoolToString(x.Struct_thing.__isset_String_thing));\r
+      Expect( not x.Struct_thing.__isset_Byte_thing, 'x.Struct_thing.__isset_Byte_thing = '+BoolToString(x.Struct_thing.__isset_Byte_thing));\r
+      Expect( not x.Struct_thing.__isset_I32_thing, 'x.Struct_thing.__isset_I32_thing = '+BoolToString(x.Struct_thing.__isset_I32_thing));\r
+      Expect( not x.Struct_thing.__isset_I64_thing, 'x.Struct_thing.__isset_I64_thing = '+BoolToString(x.Struct_thing.__isset_I64_thing));\r
+    end;\r
+    on e:Exception do Expect( FALSE, 'Unexpected exception "'+e.ClassName+'"');\r
+  end;\r
+\r
+\r
+  // oneway functions\r
+  StartTestGroup( 'Test Oneway(1)');\r
   client.testOneway(1);\r
+  Expect( TRUE, 'Test Oneway(1)');  // success := no exception\r
 \r
-  Console.Write('Test Calltime()');\r
+  // call time\r
+  StartTestGroup( 'Test Calltime()');\r
   StartTick := GetTIckCount;\r
-\r
   for k := 0 to 1000 - 1 do\r
   begin\r
     client.testVoid();\r
   end;\r
   Console.WriteLine(' = ' + FloatToStr( (GetTickCount - StartTick) / 1000 ) + ' ms a testVoid() call' );\r
 \r
+  // no more tests here\r
+  StartTestGroup( '');\r
 end;\r
 \r
 \r
@@ -580,6 +754,8 @@ const
 begin\r
   stm  := TStringStream.Create;\r
   try\r
+    StartTestGroup( 'JsonProtocolTest');  // no more tests here\r
+\r
     // prepare binary data\r
     SetLength( binary, $100);\r
     for i := Low(binary) to High(binary) do binary[i] := i;\r
@@ -642,6 +818,18 @@ begin
   finally\r
     stm.Free;\r
     prot := nil;  //-> Release\r
+    StartTestGroup( '');  // no more tests here\r
+  end;\r
+end;\r
+\r
+\r
+procedure TClientThread.StartTestGroup( const aGroup : string);\r
+begin\r
+  FTestGroup := aGroup;\r
+  if FTestGroup <> '' then begin\r
+    Console.WriteLine('');\r
+    Console.WriteLine( aGroup+' tests');\r
+    Console.WriteLine( StringOfChar('-',60));\r
   end;\r
 end;\r
 \r
@@ -650,16 +838,46 @@ procedure TClientThread.Expect( aTestResult : Boolean; const aTestInfo : string)
 begin\r
   if aTestResult  then begin\r
     Inc(FSuccesses);\r
-    Console.WriteLine( aTestInfo+' = OK');\r
+    Console.WriteLine( aTestInfo+': passed');\r
   end\r
   else begin\r
-    Inc(FErrors);\r
-    Console.WriteLine( aTestInfo+' = FAILED');\r
-    ASSERT( FALSE);  // we have a failed test!\r
+    FErrors.Add( FTestGroup+': '+aTestInfo);\r
+    Console.WriteLine( aTestInfo+': *** FAILED ***');\r
+\r
+    // We have a failed test!\r
+    // -> issue DebugBreak ONLY if a debugger is attached,\r
+    // -> unhandled DebugBreaks would cause Windows to terminate the app otherwise\r
+    if IsDebuggerPresent then asm int 3 end;\r
   end;\r
 end;\r
 \r
 \r
+procedure TClientThread.ReportResults;\r
+var nTotal : Integer;\r
+    sLine : string;\r
+begin\r
+  // prevent us from stupid DIV/0 errors\r
+  nTotal := FSuccesses + FErrors.Count;\r
+  if nTotal = 0 then begin\r
+    Console.WriteLine('No results logged');\r
+    Exit;\r
+  end;\r
+\r
+  Console.WriteLine('');\r
+  Console.WriteLine( StringOfChar('=',60));\r
+  Console.WriteLine( IntToStr(nTotal)+' tests performed');\r
+  Console.WriteLine( IntToStr(FSuccesses)+' tests succeeded ('+IntToStr(round(100*FSuccesses/nTotal))+'%)');\r
+  Console.WriteLine( IntToStr(FErrors.Count)+' tests failed ('+IntToStr(round(100*FErrors.Count/nTotal))+'%)');\r
+  Console.WriteLine( StringOfChar('=',60));\r
+  if FErrors.Count > 0 then begin\r
+    Console.WriteLine('FAILED TESTS:');\r
+    for sLine in FErrors do Console.WriteLine('- '+sLine);\r
+    Console.WriteLine( StringOfChar('=',60));\r
+  end;\r
+  Console.WriteLine('');\r
+end;\r
+\r
+\r
 constructor TClientThread.Create(ATransport: ITransport; AProtocol : IProtocol; ANumIteration: Integer);\r
 begin\r
   inherited Create( True );\r
@@ -667,11 +885,17 @@ begin
   FTransport := ATransport;\r
   FProtocol := AProtocol;\r
   FConsole := TThreadConsole.Create( Self );\r
+\r
+  // error list: keep correct order, allow for duplicates\r
+  FErrors := TStringList.Create;\r
+  FErrors.Sorted := FALSE;\r
+  FErrors.Duplicates := dupAccept;\r
 end;\r
 \r
 destructor TClientThread.Destroy;\r
 begin\r
-  FConsole.Free;\r
+  FreeAndNil( FConsole);\r
+  FreeAndNil( FErrors);\r
   inherited;\r
 end;\r
 \r
@@ -680,12 +904,21 @@ var
   i : Integer;\r
   proc : TThreadProcedure;\r
 begin\r
-  for i := 0 to FNumIteration - 1 do\r
-  begin\r
-    ClientTest;\r
-    JSONProtocolReadWriteTest;\r
+  // perform all tests\r
+  try\r
+    for i := 0 to FNumIteration - 1 do\r
+    begin\r
+      ClientTest;\r
+      JSONProtocolReadWriteTest;\r
+    end;\r
+  except\r
+    on e:Exception do Expect( FALSE, 'unexpected exception: "'+e.message+'"');\r
   end;\r
 \r
+  // report the outcome\r
+  ReportResults;\r
+\r
+  // shutdown\r
   proc := procedure\r
   begin\r
     if FTransport <> nil then\r