/// regression tests for mORMot's cross-platform units // - this unit is a part of the freeware Synopse mORMot framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit SynCrossPlatformTests; { This file is part of Synopse mORMot framework. Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez Synopse Informatique - https://synopse.info *** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.1 The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL 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. The Original Code is Synopse mORMot framework. The Initial Developer of the Original Code is Arnaud Bouchez. Portions created by the Initial Developer are Copyright (C) 2022 the Initial Developer. All Rights Reserved. Contributor(s): Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** Should compile with Delphi for any platform, or with FPC or Kylix } {$i SynCrossPlatform.inc} // define e.g. HASINLINE interface uses SysUtils, Classes, Variants, TypInfo, {$ifdef ISDELPHI2010} System.Generics.Collections, {$endif} {$ifndef NEXTGEN} Contnrs, {$endif} mORMotClient, // as generated by mORMotWrappers.pas ! SynCrossPlatformJSON, SynCrossPlatformCrypto, SynCrossPlatformSpecific, SynCrossPlatformRest; type /// the prototype of an individual test // - to be used with TSynTest descendants TSynTestEvent = procedure of object; {$M+} { we need the RTTI for the published methods of this object class } /// generic class for performing simple tests // - purpose of this ancestor is to have RTTI for its published methods, // which will contain the tests TSynTest = class protected fFailureMsg: string; fCurrentTest: Integer; public /// the test case name Ident: string; /// the registered tests, i.e. all published methods of this class Tests: TPublishedMethodDynArray; /// how many Check() call did pass Passed: cardinal; /// how many Check() call did failed Failed: cardinal; /// create the test instance // - this constructor will add all published methods to the internal // test list, accessible via the Count/TestName/TestMethod properties constructor Create(const aIdent: string=''); /// run all tests procedure Run(LogToConsole: boolean); /// validate a test procedure Check(test: Boolean; const Msg: string=''); overload; published end; /// regression tests of our CrossPlatform units TSynCrossPlatformTests = class(TSynTest) published procedure Iso8601DateTime; procedure Base64Encoding; procedure JSON; procedure Model; procedure Cryptography; end; /// regression tests of our CrossPlatform units TSynCrossPlatformClient = class(TSynTest) protected fAuthentication: TSQLRestServerAuthenticationClass; fClient: TSQLRestClientHTTP; public constructor Create(aAuthentication: TSQLRestServerAuthenticationClass); reintroduce; destructor Destroy; override; published procedure Connection; procedure ORM; procedure ORMBatch; procedure Services; procedure CleanUp; end; {$M-} implementation type TSQLRecordPeopleSimple = class(TSQLRecord) private fData: TSQLRawBlob; fFirstName: RawUTF8; fLastName: RawUTF8; fYearOfBirth: integer; fYearOfDeath: word; published property FirstName: RawUTF8 read fFirstName write fFirstName; property LastName: RawUTF8 read fLastName write fLastName; property Data: TSQLRawBlob read fData write fData; property YearOfBirth: integer read fYearOfBirth write fYearOfBirth; property YearOfDeath: word read fYearOfDeath write fYearOfDeath; end; TMainNested = class(TCollectionItem) private fNumber: double; fIdent: RawUTF8; published property Ident: RawUTF8 read fIdent write fIdent; property Number: double read fNumber write fNumber; end; TMain = class(TPersistent) private fName: RawUTF8; fNested: TCollection; fList: TStringList; public constructor Create; destructor Destroy; override; published property Name: RawUTF8 read fName write fName; property Nested: TCollection read fNested; property List: TStringList read fList; end; { TSynTest } procedure TSynTest.Check(test: Boolean; const Msg: string=''); begin if test then inc(Passed) else begin inc(Failed); if Msg<>'' then fFailureMsg := fFailureMsg+'['+Msg+'] '; end; end; constructor TSynTest.Create(const aIdent: string); begin Ident := aIdent; GetPublishedMethods(self,Tests); end; procedure TSynTest.Run(LogToConsole: boolean); var i: integer; BeforePassed,BeforeFailed: cardinal; startclass, startmethod: TDateTime; datetime: string; LogFile: text; procedure Log(const Fmt: string; const Args: array of const); var msg: string; begin msg := format(Fmt,Args); if LogToConsole then writeln(msg) else writeln(LogFile,msg); if not LogToConsole then Flush(LogFile); end; begin startclass := Now; datetime := DateTimeToIso8601(startclass); if not LogToConsole then begin assign(LogFile,ExtractFilePath(ParamStr(0))+ FormatDateTime('yyyy mm dd hh nn ss',startclass)+'.txt'); rewrite(LogFile); end; Log(#13#10' %s'#13#10'%s',[Ident,StringOfChar('-',length(Ident)+2)]); for i := 0 to high(Tests) do begin Log(#13#10' %d. Running "%s"',[i+1,Tests[i].Name]); startmethod := Now; BeforePassed := Passed; BeforeFailed := Failed; try fCurrentTest := i; TSynTestEvent(Tests[i].Method)(); except on E: Exception do Check(False,format('Exception %s raised with message "%s"',[E.ClassName,E.Message])); end; if Failed<>BeforeFailed then Log(' !!! %d test(s) failed / %d %s',[Failed-BeforeFailed, Failed-BeforeFailed+Passed-BeforePassed,fFailureMsg]) else Log(' %d tests passed in %s',[Passed-BeforePassed, FormatDateTime('nn:ss:zzz',Now-startmethod)]); fFailureMsg := ''; end; Log(#13#10' Tests failed: %d / %d'#13#10' Time elapsed: %s'#13#10#13#10' %s', [Failed,Failed+Passed,FormatDateTime('nn:ss:zzz',Now-startclass),datetime]); if not LogToConsole then close(LogFile); end; { TSynCrossPlatformTests } procedure TSynCrossPlatformTests.Base64Encoding; var b,c: TByteDynArray; i: integer; begin check(b=nil); for i := 0 to 100 do begin SetLength(b,i); if i>0 then b[i-1] := i; check(Base64JSONStringToBytes(BytesToBase64JSONString(b),c)); check(length(c)=i); check(CompareMem(Pointer(b),pointer(c),i)); end; end; procedure TSynCrossPlatformTests.Cryptography; var c: array of byte; s: string; begin SetLength(c,5); c[4] := $96; Check(crc32(0,c)=$DF4EC16C,'crc32'); Check(crc32ascii(0,'abcdefghijklmnop')=$943AC093); SetLength(c,3); c[0] := ord('a'); c[1] := ord('b'); c[2] := ord('c'); s := SHA256(c); check(s='ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); check(SHA256('abc')=s); end; procedure TSynCrossPlatformTests.Iso8601DateTime; procedure Test(D: TDateTime); var s: string; procedure One(D: TDateTime); var E: TDateTime; V: TTimeLog; begin s := DateTimeToIso8601(D); E := Iso8601ToDateTime(s); Check(Abs(D-E)<(1/SecsPerDay)); // we allow 1 sec error Check(DateTimeToJSON(D)='"'+s+'"'); V := DateTimeToTTimeLog(D); E := TTimeLogToDateTime(V); Check(Abs(D-E)<(1/SecsPerDay)); Check(UrlDecode(UrlEncode(s))=s); end; begin One(D); Check(length(s)=19); One(Trunc(D)); Check(length(s)=10); One(Frac(D)); Check(length(s)=9); end; var D: TDateTime; i: integer; s: string; T: TTimeLog; begin s := '2014-06-28T11:50:22'; D := Iso8601ToDateTime(s); Check(Abs(D-41818.49331)<(1/SecsPerDay)); Check(DateTimeToIso8601(D)=s); T := DateTimeToTTimeLog(D); Check(T=135181810838); D := Now/20+Random*20; // some starting random date/time for i := 1 to 2000 do begin Test(D); D := D+Random*57; // go further a little bit: change date/time end; end; procedure TSynCrossPlatformTests.JSON; var doc: variant; js,json2,inlined: string; i: integer; obj1,obj2: TMain; item: TMainNested; begin doc := JSONVariant('{"test":1234,"name":"Joh\"n\r","zero":0.0}'); check(doc.test=1234); check(doc.name='Joh"n'#13); check(doc.name2=null); check(doc.zero=0); js := doc; check(js='{"test":1234,"name":"Joh\"n\r","zero":0}'); {$ifdef FPC} TJSONVariantData(doc)['name2'] := 3.1415926; TJSONVariantData(doc)['name'] := 'John'; {$else} doc.name2 := 3.1415926; doc.name := 'John'; {$endif} js := doc; check(js='{"test":1234,"name":"John","zero":0,"name2":3.1415926}'); doc := JSONVariant('[{ID:1,"Username":"xx","FirstName":"System",Active:-1}]'); check(TJSONVariantData(doc).Kind=jvArray); check(TJSONVariantData(doc).Count=1); check(TJSONVariantData(doc).Values[0].ID=1); check(TJSONVariantData(doc).Values[0].Username='xx'); check(TJSONVariantData(doc).Values[0].Active=-1); check(IsRowID('id')); check(IsRowID('iD')); check(IsRowID('rowid')); check(IsRowID('RowID')); check(not IsRowID('iz')); check(not IsRowID('i2')); check(not IsRowID('rawid')); check(not IsRowID('')); check(FormatBind('',[])=''); for i := 1 to 1000 do begin js := IntToStr(i); inlined := ':('+js+'):'; check(FormatBind(js,[])=js); check(FormatBind(js,[i])=js); check(FormatBind('?',[i])=inlined); check(FormatBind('a?a',[i])='a'+inlined+'a'); check(FormatBind('a?',[i])='a'+inlined); check(FormatBind('?a',[i])=inlined+'a'); check(FormatBind('ab?',[i])='ab'+inlined); check(FormatBind('?ab',[i])=inlined+'ab'); check(FormatBind('ab?ab',[i])='ab'+inlined+'ab'); check(FormatBind('abc?abc',[i])='abc'+inlined+'abc'); check(FormatBind('abc?abc',[i,1])='abc'+inlined+'abc'); check(FormatBind(js+'?',[i])=js+inlined); check(FormatBind('?'+js,[i])=inlined+js); check(FormatBind('ab?ab',[js])='ab:("'+js+'"):ab'); check(FormatBind('ab?ab',[variant(js)])='ab:("'+js+'"):ab'); check(FormatBind('ab?ab',[variant(i)])='ab'+inlined+'ab'); check(FormatBind('ab?ab?',[variant(i)])='ab'+inlined+'ab:(null):'); check(FormatBind('ab?ab??cd',[i,i,js])='ab'+inlined+'ab'+inlined+ ':("'+js+'"):cd'); end; RegisterClassForJSON([TMainNested]); // for JSONToNewObject() obj1 := TMain.Create; obj2 := TMain.Create; try for i := 1 to 100 do begin obj1.Name := IntToStr(i); item := obj1.Nested.Add as TMainNested; item.Ident := obj1.Name; item.Number := i/2; check(obj1.Nested.Count=i); obj1.list.Add(obj1.Name); js := ObjectToJSON(obj1); check(js<>''); if i=1 then check(js='{"Name":"1","Nested":[{"Ident":"1","Number":0.5}],"List":["1"]}'); JSONToObject(obj2,js); check(obj2.Nested.Count=i); json2 := ObjectToJSON(obj2); check(json2=js); js := ObjectToJSON(item,true); item := TMainNested(JSONToNewObject(js)); check(item<>nil); json2 := ObjectToJSON(item,true); check(json2=js); item.Free; end; finally obj2.Free; obj1.Free; end; js := 'one,two,3'; i := 1; check(GetNextCSV(js,i,json2)); check(json2='one'); check(GetNextCSV(js,i,json2)); check(json2='two'); check(GetNextCSV(js,i,json2)); check(json2='3'); check(not GetNextCSV(js,i,json2)); check(not GetNextCSV(js,i,json2)); js := 'one'; i := 1; check(GetNextCSV(js,i,json2)); check(json2='one'); check(not GetNextCSV(js,i,json2)); js := ''; i := 1; check(not GetNextCSV(js,i,json2)); doc := JsonVariant('{}'); js := doc; check(js='{}'); end; procedure TSynCrossPlatformTests.Model; var mdel: TSQLModel; people: TSQLRecordPeopleSimple; i: integer; js: string; fields: TSQLFieldBits; begin mdel := TSQLModel.Create([TSQLRecordPeopleSimple],'test/'); Check(mdel.Root='test'); Check(length(mdel.Info)=1); Check(mdel.Info[0].Table=TSQLRecordPeopleSimple); Check(mdel.Info[0].Name='PeopleSimple'); Check(length(mdel.Info[0].Prop)=6); people := TSQLRecordPeopleSimple.Create; try for i := 1 to 1000 do begin people.ID := i; people.FirstName := IntToStr(i); people.LastName := people.FirstName+people.FirstName; people.YearOfBirth := i+500; people.YearOfDeath := people.YearOfBirth+40; js := ObjectToJSON(people); check(js=Format('{"ID":%d,"FirstName":"%d","LastName":"%d%d",'+ '"Data":"","YearOfBirth":%d,"YearOfDeath":%d}',[i,i,i,i,i+500,i+540])); end; finally people.Free; end; Check(PInteger(@mdel.Info[0].SimpleFields)^=$37); Check(PInteger(@mdel.Info[0].BlobFields)^=8); fields := mdel.Info[0].FieldNamesToFieldBits('',false); Check(PInteger(@fields)^=$37); fields := mdel.Info[0].FieldNamesToFieldBits('*',false); Check(PInteger(@fields)^=PInteger(@mdel.Info[0].AllFields)^); fields := mdel.Info[0].FieldNamesToFieldBits('id,firstname',false); Check(PInteger(@fields)^=3); fields := mdel.Info[0].FieldNamesToFieldBits('RowID , firstname ',false); Check(PInteger(@fields)^=3); Check(mdel.Info[0].FieldBitsToFieldNames(fields)='RowID,FirstName'); fields := mdel.Info[0].FieldNamesToFieldBits('firstname,id,toto',false); Check(PInteger(@fields)^=3); Check(mdel.Info[0].FieldBitsToFieldNames(fields)='RowID,FirstName'); mdel.Free; end; { TMain } constructor TMain.Create; begin inherited; fNested := TCollection.Create(TMainNested); fList := TStringList.Create; end; destructor TMain.Destroy; begin fList.Free; fNested.Free; inherited; end; { TSynCrossPlatformClient } constructor TSynCrossPlatformClient.Create( aAuthentication: TSQLRestServerAuthenticationClass); begin inherited Create; fAuthentication := aAuthentication; end; destructor TSynCrossPlatformClient.Destroy; begin CleanUp; inherited; end; procedure TSynCrossPlatformClient.CleanUp; begin FreeAndNil(fClient); check(fClient=nil); end; procedure TSynCrossPlatformClient.Connection; var doremotelog: boolean; dofilelog: boolean; begin doremotelog := false; dofilelog := false; if fAuthentication=TSQLRestServerAuthenticationDefault then begin fClient := GetClient('localhost','User','synopse'); if dofilelog then fClient.LogToFile(LOG_VERBOSE); if doremotelog then fClient.LogToRemoteServer(LOG_VERBOSE,'localhost'); end else begin fClient := TSQLRestClientHTTP.Create('localhost',SERVER_PORT,GetModel,true); if dofilelog then fClient.LogToFile(LOG_VERBOSE); if doremotelog then fClient.LogToRemoteServer(LOG_VERBOSE,'localhost'); check(fClient.Connect); check(fClient.ServerTimeStamp<>0); if fAuthentication<>nil then fClient.SetUser(fAuthentication,'User','synopse'); end; end; procedure TSynCrossPlatformClient.ORM; procedure TestPeople(people: TSQLRecordPeople; var id: integer); begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=id); Check(people.FirstName=''); Check(people.LastName=''); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(people.Sexe=sFemale); end; var people: TSQLRecordPeople; Call: TSQLRestURIParams; i,id: integer; list: TObjectList; {$ifdef ISDELPHI2010} peoples: TObjectList; {$endif ISDELPHI2010} begin fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople); Check(fClient.InternalState>0); Check(Call.OutStatus=HTTP_SUCCESS); people := TSQLRecordPeople.Create; try Check(people.InternalState=0); for i := 1 to 200 do begin people.FirstName := 'First'+IntToStr(i); people.LastName := 'Last'+IntToStr(i); people.YearOfBirth := i+1800; people.YearOfDeath := i+1825; people.Sexe := TPeopleSexe(i and 1); Check(fClient.Add(people,true)=i); Check(people.InternalState=fClient.InternalState); end; finally people.Free; end; people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'','',[]); try Check(people.InternalState=0); id := 0; while people.FillOne do begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=id); Check(people.FirstName='First'+IntToStr(id)); Check(people.LastName='Last'+IntToStr(id)); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(ord(people.Sexe)=id and 1); end; Check(id=200); finally people.Free; end; people := TSQLRecordPeople.CreateAndFillPrepare(fClient, 'YearOFBIRTH,Yearofdeath,id','',[]); try Check(people.InternalState=0); id := 0; while people.FillOne do TestPeople(people,id); Check(id=200); finally people.Free; end; list := fClient.RetrieveList(TSQLRecordPeople,'YearOFBIRTH,Yearofdeath,id','',[]); try id := 0; for i := 0 to list.Count-1 do TestPeople(TSQLRecordPeople(list[i]),id); Check(id=200); finally list.Free; end; {$ifdef ISDELPHI2010} peoples := fClient.RetrieveList('YearOFBIRTH,yearofdeath,id','',[]); try id := 0; for i := 0 to peoples.Count-1 do TestPeople(peoples[i],id); Check(id=200); finally peoples.Free; end; {$endif ISDELPHI2010} people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'', 'yearofbirth=?',[1900]); try Check(people.InternalState=0); id := 0; while people.FillOne do begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=100); Check(people.FirstName='First100'); Check(people.LastName='Last100'); Check(people.YearOfBirth=1900); Check(people.YearOfDeath=1925); end; Check(id=1); finally people.Free; end; for i := 1 to 200 do if i and 15=0 then fClient.Delete(TSQLRecordPeople,i) else if i mod 82=0 then begin people := TSQLRecordPeople.Create; try id := i+1; people.ID := i; people.FirstName := 'First'+IntToStr(id); people.LastName := 'Last'+IntToStr(id); people.YearOfBirth := id+1800; people.YearOfDeath := id+1825; Check(people.InternalState=0); Check(fClient.Update(people,'YEarOFBIRTH,YEarOfDeath')); Check(people.InternalState=fClient.InternalState); finally people.Free; end; end; for i := 1 to 200 do begin people := TSQLRecordPeople.Create(fClient,i); try if i and 15=0 then Check(people.ID=0) else begin Check(people.InternalState=fClient.InternalState); if i mod 82=0 then id := i+1 else id := i; Check(people.ID=i); Check(people.FirstName='First'+IntToStr(i)); Check(people.LastName='Last'+IntToStr(i)); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(ord(people.Sexe)=i and 1); end; finally people.Free; end; end; end; procedure TSynCrossPlatformClient.ORMBatch; var people: TSQLRecordPeople; Call: TSQLRestURIParams; res: TIDDynArray; {$ifndef ISDWS} blob: TSQLRawBlob; {$endif} i,id: integer; begin fClient.CallBackGet('DropTable',[],Call,TSQLRecordPeople); Check(fClient.InternalState>0); Check(Call.OutStatus=HTTP_SUCCESS); fClient.BatchStart(TSQLRecordPeople); people := TSQLRecordPeople.Create; try for i := 1 to 200 do begin Check(people.InternalState=0); people.FirstName := 'First'+IntToStr(i); people.LastName := 'Last'+IntToStr(i); people.YearOfBirth := i+1800; people.YearOfDeath := i+1825; people.Sexe := TPeopleSexe(i and 1); fClient.BatchAdd(people,true); end; finally people.Free; end; Check(fClient.BatchSend(res)=HTTP_SUCCESS); Check(length(res)=200); for i := 1 to length(res) do Check(res[i-1]=i); people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'','',[]); try Check(people.InternalState=0); id := 0; while people.FillOne do begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=id); Check(people.FirstName='First'+IntToStr(id)); Check(people.LastName='Last'+IntToStr(id)); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(ord(people.Sexe)=id and 1); end; Check(id=200); finally people.Free; end; people := TSQLRecordPeople.CreateAndFillPrepare(fClient, 'YearOFBIRTH,Yearofdeath,id','',[]); try id := 0; Check(people.InternalState=0); while people.FillOne do begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=id); Check(people.FirstName=''); Check(people.LastName=''); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(people.Sexe=sFemale); end; Check(id=200); finally people.Free; end; people := TSQLRecordPeople.CreateAndFillPrepare(fClient,'', 'yearofbirth=?',[1900]); try Check(people.InternalState=0); id := 0; while people.FillOne do begin Check(people.InternalState=fClient.InternalState); inc(id); Check(people.ID=100); Check(people.FirstName='First100'); Check(people.LastName='Last100'); Check(people.YearOfBirth=1900); Check(people.YearOfDeath=1925); end; Check(id=1); finally people.Free; end; fClient.BatchStart(nil); for i := 1 to 200 do if i and 15=0 then fClient.BatchDelete(TSQLRecordPeople,i) else if i mod 82=0 then begin people := TSQLRecordPeople.Create; try id := i+1; people.ID := i; people.FirstName := 'First'+IntToStr(id); people.LastName := 'Last'+IntToStr(id); people.YearOfBirth := id+1800; people.YearOfDeath := id+1825; Check(fClient.BatchUpdate(people,'YEarOFBIRTH,YEarOfDeath')>=0); Check(people.InternalState=0); finally people.Free; end; end; Check(fClient.BatchSend(res)=HTTP_SUCCESS); Check(length(res)=14); for i := 1 to 14 do Check(res[i-1]=HTTP_SUCCESS); for i := 1 to 200 do begin people := TSQLRecordPeople.Create(fClient,i); try if i and 15=0 then Check(people.ID=0) else begin Check(people.InternalState=fClient.InternalState); if i mod 82=0 then id := i+1 else id := i; Check(people.ID=i); Check(people.FirstName='First'+IntToStr(i)); Check(people.LastName='Last'+IntToStr(i)); Check(people.YearOfBirth=id+1800); Check(people.YearOfDeath=id+1825); Check(ord(people.Sexe)=i and 1); end; finally people.Free; end; end; {$ifndef ISDWS} exit; // Add(..,'Data') below is buggy, but RetrieveBlob() seems fine people := TSQLRecordPeople.Create; try people.FirstName := 'With'; people.LastName := 'Blob'; SetLength(blob,2); blob[0] := 1; blob[1] := 2; people.Data := blob; id := fClient.Add(people,true,false,'FirstName,LastName,Data'); Check(id=201); Check(people.InternalState=fClient.InternalState); blob := nil; finally people.Free; end; people := TSQLRecordPeople.Create(fClient,id); try Check(people.FirstName='With'); Check(people.LastName='Blob'); Check(people.Data=nil); Check(not fClient.RetrieveBlob(TSQLRecordPeople,id,'wrongfieldname',blob)); Check(blob=nil); Check(fClient.RetrieveBlob(TSQLRecordPeople,id,'data',blob)); Check(blob<>nil); finally people.Free; end; {$endif} end; procedure TSynCrossPlatformClient.Services; var calc: ICalculator; i,j: integer; sex: TPeopleSexe; name: string; rec: TTestCustomJSONArraySimpleArray; const SEX_TEXT: array[0..1] of RawUTF8 = ('Miss','Mister'); begin calc := TServiceCalculator.Create(fClient); check(calc.InstanceImplementation=sicShared); check(calc.ServiceName='Calculator'); for i := 1 to 200 do check(calc.Add(i,i+1)=i*2+1); for i := 1 to 200 do begin sex := TPeopleSexe(i and 1); name := 'Smith'; calc.ToText(i,'$',sex,name); check(sex=sFemale); check(name=format('$ %d for %s Smith',[i,SEX_TEXT[i and 1]])); end; Fillchar(rec,SizeOf(rec),0); for i := 1 to 100 do begin name := calc.RecordToText(rec); if i=1 then check(name='{"F":"","G":[],"H":{"H1":0,"H2":"","H3":{"H3a":false,"H3b":null}},"I":"","J":[]}'); check(length(Rec.F)=i); for j := 1 to length(Rec.F) do check(Rec.F[j]='!'); check(length(Rec.G)=i); for j := 0 to high(Rec.G) do check(Rec.G[j]=IntToStr(j+1)); check(Rec.H.H1=i); check(length(Rec.J)=i-1); for j := 0 to high(Rec.J) do begin Check(Rec.J[j].J1=j); Check(Rec.J[j].J2.D2=j); Check(Rec.J[j].J3=TRecordEnum(j mod (ord(high(TRecordEnum))+1))); end; end; end; end.