1 unit ToolsUnit;
2 
3 {$IFDEF FPC}
4   {$mode objfpc}{$H+}
5 {$ENDIF}
6 
7 interface
8 
9 uses
10   Classes, SysUtils, DB, testdecorator, fpcunit;
11 
12 Const
13   // Number of "N" test datasets (as opposed to FieldDatasets) that will be created
14   // The connectors should have these records prepared in their Create*Dataset procedures.
15   MaxDataSet = 35;
16   // Number of records in a trace dataset:
17   NForTraceDataset = 15;
18 
19 type
20 
21   { TDBConnector }
22 
23   TDBConnectorClass = class of TDBConnector;
24   TDBConnector = class(TPersistent)
25      private
26        FLogTimeFormat: TFormatSettings; //for error logging only
27        FFormatSettings: TFormatSettings;
28        FChangedFieldDataset : boolean;
GetCharSizenull29        function GetCharSize: integer;
30      protected
31        FChangedDatasets : array[0..MaxDataSet] of boolean;
32        FUsedDatasets : TFPList;
33        procedure SetTestUniDirectional(const AValue: boolean); virtual;
GetTestUniDirectionalnull34        function GetTestUniDirectional: boolean; virtual;
35        // These methods should be implemented by all descendents
36        // They are called each time a test needs a TDataset descendent
37        // n: the dataset index to return (also number of records in set)
38        // Presupposes that Create*Dataset(s) has been called already.
InternalGetNDatasetnull39        Function InternalGetNDataset(n : integer) : TDataset;  virtual; abstract;
InternalGetFieldDatasetnull40        Function InternalGetFieldDataset : TDataSet; virtual; abstract;
41 
42        // These methods should be implemented by all descendents
43        // They are called e.g. in the constructor. They can be used
44        // to create the tables on disk, or on a DB server
45        procedure CreateNDatasets; virtual; abstract;
46        procedure CreateFieldDataset; virtual; abstract;
47 
48        // These methods are called after each test in which a dataset is used
49        // by calling GetXXXDataset with Achange=true
50        // They should reset all data to their right/initial values.
51        procedure ResetNDatasets; virtual;
52        procedure ResetFieldDataset; virtual;
53 
54        // These methods are called e.g. in the destructor.
55        // They should clean up all mess, like tables on disk or on a DB server
56        procedure DropNDatasets; virtual; abstract;
57        procedure DropFieldDataset; virtual; abstract;
58 
59        // If logging is enabled, writes Message to log file and flushes
60        // Logging uses tab-separated columns
61        procedure LogMessage(Category,Message: string);
62      public
63        constructor Create; virtual;
64        destructor Destroy; override;
65 
66        procedure DataEvent(dataset :TDataset);
67 
GetNDatasetnull68        Function GetNDataset(n : integer) : TDataset;  overload;
GetNDatasetnull69        Function GetNDataset(AChange : Boolean; n : integer) : TDataset;  overload;
GetFieldDatasetnull70        Function GetFieldDataset : TDataSet; overload;
GetFieldDatasetnull71        Function GetFieldDataset(AChange : Boolean) : TDataSet; overload;
72 
73        // Gets a dataset that tracks calculation of calculated fields etc.
GetTraceDatasetnull74        Function GetTraceDataset(AChange : Boolean) : TDataset; virtual;
75 
76        // Run before a test is started
77        procedure StartTest(TestName: string);
78        // Run after a test is stopped
79        procedure StopTest(TestName: string);
80        property TestUniDirectional: boolean read GetTestUniDirectional write SetTestUniDirectional;
81        property FormatSettings: TFormatSettings read FFormatSettings;
82        property CharSize: integer read GetCharSize;
83      end;
84 
85   { TTestDataLink }
86 
87   TTestDataLink = class(TDataLink)
88      protected
89        procedure DataSetScrolled(Distance: Integer); override;
90        procedure DataSetChanged; override;
91 {$IFDEF fpc}
92        procedure DataEvent(Event: TDataEvent; Info: Ptrint); override;
93 {$ELSE}
94        procedure DataEvent(Event: TDataEvent; Info: longint); override;
95 {$ENDIF}
96      end;
97 
98   { TDBBasicsTestSetup }
99 
100   TDBBasicsTestSetup = class(TTestSetup)
101     protected
102       procedure OneTimeSetup; override;
103       procedure OneTimeTearDown; override;
104     end;
105 
106   { TDBBasicsTestCase }
107   TDBBasicsTestCase = class(TTestCase)
108     protected
109       procedure SetUp; override;
110       procedure TearDown; override;
111       // Verify whether all values in FieldDataset are present and correct
112       procedure CheckFieldDatasetValues(ADataSet: TDataSet);
113       // Verify whether all values in NDataset are present and correct
114       procedure CheckNDatasetValues(ADataSet: TDataSet; n: integer);
115   end;
116 
117 
118 const
119   DataEventnames : Array [TDataEvent] of String[21] =
120     ('deFieldChange', 'deRecordChange', 'deDataSetChange', 'deDataSetScroll',
121      'deLayoutChange', 'deUpdateRecord', 'deUpdateState', 'deCheckBrowseMode',
122      'dePropertyChange', 'deFieldListChange', 'deFocusControl' ,'deParentScroll',
123      'deConnectChange', 'deReconcileError', 'deDisabledStateChange');
124 
125 
126 const
127   testValuesCount = 25;
128   testFloatValues : Array[0..testValuesCount-1] of double = (-maxSmallint-1,-maxSmallint,-256,-255,-128,-127,-1,0,1,127,128,255,256,maxSmallint,maxSmallint+1,0.123456,-0.123456,4.35,12.434E7,9.876e-5,123.45678,2.4,3.2,0.4,23);
129   testCurrencyValues : Array[0..testValuesCount-1] of currency = (-MaxLongInt-1,-MaxSmallint-1,-256,-255,-43.34,-2.5,-0.21,0,0.32,45.45,256,45,1234.56,12.34,0.12,MaxSmallInt+1,MaxLongInt+1,-6871947.67,68719476736,2748779069.44,922337203685.47,-92233720368547,99999999999999,-9223372036854.25,-9223372036854.7);
130   testFmtBCDValues : Array[0..testValuesCount-1] of string = ('-100','-65.5','-54.3333','-43.3334','-2.5','-0.234567','45.4','0.3','45.414585','127','128','255','256','45','0.3','45.4','127','128','255','256','45','1234.56789','43.23','43.500001','99.88');
131   testIntValues : Array[0..testValuesCount-1] of integer = (-maxInt,-maxInt+1,-maxSmallint-1,-maxSmallint,-256,-255,-128,-127,-1,0,1,127,128,255,256,maxSmallint,maxSmallint+1,MaxInt-1,MaxInt,100,130,150,-150,-132,234);
132   testWordValues : Array[0..testValuesCount-1] of Word = (1,2,3,4,5,6,7,8,0,1,127,128,255,256,maxSmallint,maxSmallint+1,maxSmallInt-1,maxSmallInt,65535,100,130,150,151,132,234);
133   testSmallIntValues : Array[0..testValuesCount-1] of smallint = (-maxSmallint,-maxSmallint+1,-256,-255,-128,-127,-1,0,1,127,128,255,256,maxSmallint,maxSmallint-1,100,110,120,130,150,-150,-132,234,231,42);
134   testLargeIntValues : Array[0..testValuesCount-1] of LargeInt = (-$7fffffffffffffff,-$7ffffffffffffffe,-maxInt-1,-maxInt+1,-maxSmallint,-maxSmallint+1,-256,-255,-128,-127,-1,0,1,127,128,255,256,maxSmallint,maxSmallint-1,maxSmallint+1,MaxInt-1,MaxInt,$7fffffffffffffff-1,$7fffffffffffffff,235253244);
135   testBooleanValues : Array[0..testValuesCount-1] of boolean = (true,false,false,true,true,false,false,true,false,true,true,true,false,false,false,false,true,true,true,true,false,true,true,false,false);
136   testStringValues : Array[0..testValuesCount-1] of string = (
137     '',
138     'a',
139     'ab',
140     'abc',
141     'abcd',
142     'abcde',
143     'abcdef',
144     'abcdefg',
145     'abcdefgh',
146     'abcdefghi',
147     'abcdefghij',
148     'lMnOpQrStU',
149     '1234567890',
150     '_!@#$%^&*(',
151     '_!@#$%^&*(',
152     ' ''quotes'' ',
153     ')-;:/?.<>',
154     '~`|{}- =',    // note that there's no \  (backslash) since some db's uses that as escape-character
155     '  WRaP  ',
156     'wRaP  ',
157     ' wRAP',
158     'this',
159 //    'is',
160     'fun',
161     'VB7^',
162     'vdfbst'
163   );
164 
165   testDateValues : Array[0..testValuesCount-1] of string = (
166     '2000-01-01',
167     '1999-12-31',
168     '2004-02-29',
169     '2004-03-01',
170     '1991-02-28',
171     '1991-03-01',
172     '1997-11-29',
173     '2040-10-16',
174     '1977-09-29',
175     '1977-12-31',
176     '1917-12-29',
177     '1900-01-01',
178     '1899-12-31',
179     '1899-12-30',
180     '1899-12-29',
181     '1800-03-30',
182     '1754-06-04',
183     '1753-01-01',
184     '1650-05-10',
185     '0904-04-12',
186     '0199-07-09',
187     '0079-11-29',
188     '0031-11-02',
189     '0001-12-31',
190     '0001-01-01'
191   );
192 
193   testTimeValues : Array[0..testValuesCount-1] of string = (
194     '10:45:12.000',
195     '00:00:00.000',
196     '24:00:00.000',
197     '33:25:15.000',
198     '04:59:16.000',
199     '05:45:59.000',
200     '11:45:12.000',
201     '12:45:12.000',
202     '14:45:14.000',
203     '14:45:52.000',
204     '15:35:12.000',
205     '16:35:42.000',
206     '16:45:12.000',
207     '18:45:22.000',
208     '19:45:12.000',
209     '16:45:12.010',
210     '13:55:12.200',
211     '13:46:12.543',
212     '15:35:12.000',
213     '17:25:12.530',
214     '19:45:12.003',
215     '10:54:12.999',
216     '12:25:12.000',
217     '20:15:12.758',
218     '23:59:59.000'
219   );
220 
221 
222 var dbtype,
223     dbconnectorname,
224     dbconnectorparams,
225     dbname,
226     dbuser,
227     dbhostname,
228     dbpassword,
229     dbcharset,
230     dblogfilename,
231     dbQuoteChars   : string;
232     dblogfile      : TextFile;
233     DataEvents     : string;
234     DBConnector    : TDBConnector;
235     testValues     : Array [TFieldType,0..testvaluescount -1] of string;
236 
237 
238 procedure InitialiseDBConnector;
239 procedure FreeDBConnector;
240 
DateTimeToTimeStringnull241 function DateTimeToTimeString(d: tdatetime) : string;
TimeStringToDateTimenull242 function TimeStringToDateTime(d: String): TDateTime;
StringToByteArraynull243 function StringToByteArray(const s: ansistring): Variant;
244 
245 
246 implementation
247 
248 uses
249   inifiles, FmtBCD, Variants;
250 
251 var DBConnectorRefCount: integer;
252 
253 { TDBConnector }
254 
255 constructor TDBConnector.Create;
256 begin
257   FFormatSettings.DecimalSeparator:='.';
258   FFormatSettings.ThousandSeparator:=#0;
259   FFormatSettings.DateSeparator:='-';
260   FFormatSettings.TimeSeparator:=':';
261   FFormatSettings.ShortDateFormat:='yyyy/mm/dd';
262   FFormatSettings.LongTimeFormat:='hh:nn:ss.zzz';
263 
264   // Set up time format for logging output:
265   // ISO 8601 type date string so logging is uniform across machines
266   FLogTimeFormat.DecimalSeparator:='.';
267   FLogTimeFormat.ThousandSeparator:=#0;
268   FLogTimeFormat.DateSeparator:='-';
269   FLogTimeFormat.TimeSeparator:=':';
270   FLogTimeFormat.ShortDateFormat:='yyyy-mm-dd';
271   FLogTimeFormat.LongTimeFormat:='hh:nn:ss';
272 
273 
274   FUsedDatasets := TFPList.Create;
275   CreateFieldDataset;
276   CreateNDatasets;
277 end;
278 
279 destructor TDBConnector.Destroy;
280 begin
281   if assigned(FUsedDatasets) then FUsedDatasets.Destroy;
282   DropNDatasets;
283   DropFieldDataset;
284   Inherited;
285 end;
286 
GetTestUniDirectionalnull287 function TDBConnector.GetTestUniDirectional: boolean;
288 begin
289   result := false;
290 end;
291 
292 procedure TDBConnector.SetTestUniDirectional(const AValue: boolean);
293 begin
294   raise exception.create('Connector does not support tests for unidirectional datasets');
295 end;
296 
297 procedure TDBConnector.DataEvent(dataset: TDataset);
298 begin
299   DataEvents := DataEvents + 'DataEvent' + ';';
300 end;
301 
302 procedure TDBConnector.ResetNDatasets;
303 begin
304   DropNDatasets;
305   CreateNDatasets;
306 end;
307 
308 procedure TDBConnector.ResetFieldDataset;
309 begin
310   DropFieldDataset;
311   CreateFieldDataset;
312 end;
313 
TDBConnector.GetNDatasetnull314 function TDBConnector.GetNDataset(n: integer): TDataset;
315 begin
316   Result := GetNDataset(False,n);
317 end;
318 
TDBConnector.GetNDatasetnull319 function TDBConnector.GetNDataset(AChange : Boolean; n: integer): TDataset;
320 begin
321   if AChange then FChangedDatasets[n] := True;
322   Result := InternalGetNDataset(n);
323   FUsedDatasets.Add(Result);
324 end;
325 
GetFieldDatasetnull326 function TDBConnector.GetFieldDataset: TDataSet;
327 begin
328   Result := GetFieldDataset(False);
329 end;
330 
GetFieldDatasetnull331 function TDBConnector.GetFieldDataset(AChange: Boolean): TDataSet;
332 begin
333   if AChange then FChangedFieldDataset := True;
334   Result := InternalGetFieldDataset;
335   FUsedDatasets.Add(Result);
336 end;
337 
TDBConnector.GetTraceDatasetnull338 function TDBConnector.GetTraceDataset(AChange: Boolean): TDataset;
339 begin
340   result := GetNDataset(AChange,NForTraceDataset);
341 end;
342 
343 procedure TDBConnector.StartTest(TestName: string);
344 begin
345   // Log if necessary
346   LogMessage('Test','Starting test '+TestName);
347 end;
348 
349 procedure TDBConnector.StopTest(TestName: string);
350 var i : integer;
351     ds : TDataset;
352 begin
353   LogMessage('Test','Stopping test '+TestName);
354   for i := 0 to FUsedDatasets.Count -1 do
355     begin
356     ds := tdataset(FUsedDatasets[i]);
357     if ds.active then ds.Close;
358     ds.Free;
359     end;
360   FUsedDatasets.Clear;
361   if FChangedFieldDataset then ResetFieldDataset;
362   for i := 0 to MaxDataSet do if FChangedDatasets[i] then
363     begin
364     ResetNDatasets;
365     fillchar(FChangedDatasets,sizeof(FChangedDatasets),ord(False));
366     break;
367     end;
368 end;
369 
370 procedure TDBConnector.LogMessage(Category,Message: string);
371 begin
372   if dblogfilename<>'' then //double check: only if logging enabled
373     begin
374     try
375       Message:=StringReplace(Message, #9, '\t', [rfReplaceAll, rfIgnoreCase]);
376       Message:=StringReplace(Message, LineEnding, '\n', [rfReplaceAll, rfIgnoreCase]);
377       writeln(dbLogFile, TimeToStr(Now(), FLogTimeFormat) + #9 +
378         Category + #9 +
379         Message);
380       Flush(dbLogFile); //in case tests crash
381     except
382       // ignore log file errors
383     end;
384     end;
385 end;
386 
TDBConnector.GetCharSizenull387 function TDBConnector.GetCharSize: integer;
388 begin
389   case LowerCase(dbcharset) of
390     'utf8','utf-8','utf8mb4':
391       Result := 4;
392     else
393       Result := 1;
394   end;
395 end;
396 
397 
398 { TTestDataLink }
399 
400 procedure TTestDataLink.DataSetScrolled(Distance: Integer);
401 begin
402   DataEvents := DataEvents + 'DataSetScrolled' + ':' + inttostr(Distance) + ';';
403   inherited DataSetScrolled(Distance);
404 end;
405 
406 procedure TTestDataLink.DataSetChanged;
407 begin
408   DataEvents := DataEvents + 'DataSetChanged;';
409   inherited DataSetChanged;
410 end;
411 
412 {$IFDEF FPC}
413 procedure TTestDataLink.DataEvent(Event: TDataEvent; Info: Ptrint);
414 {$ELSE}
415 procedure TTestDataLink.DataEvent(Event: TDataEvent; Info: Longint);
416 {$ENDIF}
417 begin
418   if Event <> deFieldChange then
419     DataEvents := DataEvents + DataEventnames[Event] + ':' + inttostr(info) + ';'
420   else
421     DataEvents := DataEvents + DataEventnames[Event] + ':' + TField(info).FieldName + ';';
422   inherited DataEvent(Event, Info);
423 end;
424 
425 
426 { TDBBasicsTestSetup }
427 
428 procedure TDBBasicsTestSetup.OneTimeSetup;
429 begin
430   InitialiseDBConnector;
431 end;
432 
433 procedure TDBBasicsTestSetup.OneTimeTearDown;
434 begin
435   FreeDBConnector;
436 end;
437 
438 { TDBBasicsTestCase }
439 
440 procedure TDBBasicsTestCase.SetUp;
441 begin
442   inherited SetUp;
443   DBConnector.StartTest(TestName);
444 end;
445 
446 procedure TDBBasicsTestCase.TearDown;
447 begin
448   DBConnector.StopTest(TestName);
449   inherited TearDown;
450 end;
451 
452 procedure TDBBasicsTestCase.CheckFieldDatasetValues(ADataSet: TDataSet);
453 var i: integer;
454 begin
455   with ADataSet do
456   begin
457     First;
458     for i := 0 to testValuesCount-1 do
459     begin
460       CheckEquals(i, FieldByName('ID').AsInteger, 'ID');
461       CheckEquals(testStringValues[i], FieldByName('FSTRING').AsString, 'FSTRING');
462       CheckEquals(testIntValues[i], FieldByName('FINTEGER').AsInteger, 'FINTEGER');
463       CheckEquals(testLargeIntValues[i], FieldByName('FLARGEINT').AsLargeInt, 'FLARGEINT');
464       Next;
465     end;
466     CheckTrue(Eof, 'Eof');
467   end;
468 end;
469 
470 procedure TDBBasicsTestCase.CheckNDatasetValues(ADataSet: TDataSet; n: integer);
471 var i: integer;
472 begin
473   with ADataSet do
474   begin
475     First;
476     for i := 1 to n do
477     begin
478       CheckEquals(i, FieldByName('ID').AsInteger, 'ID');
479       CheckEquals('TestName' + inttostr(i), FieldByName('NAME').AsString, 'NAME');
480       Next;
481     end;
482     CheckTrue(Eof, 'Eof');
483   end;
484 end;
485 
486 
487 procedure ReadIniFile;
488 
489 var IniFile : TIniFile;
490 
491 begin
492   IniFile := TIniFile.Create(GetCurrentDir + PathDelim + 'database.ini');
493   dbtype:='';
494   if ParamCount>0 then
495     dbtype := ParamStr(1);
496   if (dbtype='') or not IniFile.SectionExists(dbtype) then
497     dbtype := IniFile.ReadString('Database','Type','');
498   dbconnectorname := IniFile.ReadString(dbtype,'Connector','');
499   dbname := IniFile.ReadString(dbtype,'Name','');
500   dbuser := IniFile.ReadString(dbtype,'User','');
501   dbhostname := IniFile.ReadString(dbtype,'Hostname','');
502   dbpassword := IniFile.ReadString(dbtype,'Password','');
503   dbcharset := IniFile.ReadString(dbtype,'CharSet','');
504   dbconnectorparams := IniFile.ReadString(dbtype,'ConnectorParams','');
505   dblogfilename := IniFile.ReadString(dbtype,'LogFile','');
506   dbquotechars := IniFile.ReadString(dbtype,'QuoteChars','"');
507 
508   IniFile.Free;
509 end;
510 
511 procedure SetupLog;
512 begin
513   if dblogfilename<>'' then
514   begin
515     try
516       AssignFile(dblogfile,dblogfilename);
517       if not(FileExists(dblogfilename)) then
518       begin
519         ReWrite(dblogfile);
520         CloseFile(dblogfile);
521       end;
522       Append(dblogfile);
523     except
524       dblogfilename:=''; //rest of code relies on this as a log switch
525     end;
526   end;
527 end;
528 
529 procedure CloseLog;
530 begin
531   if dblogfilename<>'' then
532     begin
533     try
534       CloseFile(dbLogFile);
535     except
536       // Ignore log file errors
537     end;
538     end;
539 end;
540 
541 procedure InitialiseDBConnector;
542 
543 var DBConnectorClass : TPersistentClass;
544     i                : integer;
545     FormatSettings   : TFormatSettings;
546 begin
547   if DBConnectorRefCount>0 then exit;
548 
549   FormatSettings.DecimalSeparator:='.';
550   FormatSettings.ThousandSeparator:=#0;
551 
552   testValues[ftString] := testStringValues;
553   testValues[ftFixedChar] := testStringValues;
554   testValues[ftTime] := testTimeValues;
555   testValues[ftDate] := testDateValues;
556   testValues[ftBlob] := testStringValues;
557   testValues[ftMemo] := testStringValues;
558   testValues[ftWideString] := testStringValues;
559   testValues[ftWideMemo] := testStringValues;
560   testValues[ftFMTBcd] := testFmtBCDValues;
561   for i := 0 to testValuesCount-1 do
562     begin
563     testValues[ftBoolean,i] := BoolToStr(testBooleanValues[i], True);
564     testValues[ftFloat,i] := FloatToStr(testFloatValues[i],FormatSettings);
565     testValues[ftSmallint,i] := IntToStr(testSmallIntValues[i]);
566     testValues[ftInteger,i] := IntToStr(testIntValues[i]);
567     testValues[ftWord,i] := IntToStr(testWordValues[i]);
568     testValues[ftLargeint,i] := IntToStr(testLargeIntValues[i]);
569     testValues[ftCurrency,i] := CurrToStr(testCurrencyValues[i],FormatSettings);
570     testValues[ftBCD,i] := CurrToStr(testCurrencyValues[i],FormatSettings);
571     // For date '0001-01-01' other time-part like '00:00:00' causes "Invalid variant type cast", because of < MinDateTime constant
572     if (testDateValues[i]>'0001-01-01') and (testTimeValues[i]>='00:00:01') and (testTimeValues[i]<'24:00:00') then
573       testValues[ftDateTime,i] := testDateValues[i] + ' ' + testTimeValues[i]
574     else
575       testValues[ftDateTime,i] := testDateValues[i];
576     end;
577 
578   if dbconnectorname = '' then raise Exception.Create('There is no db connector specified');
579   DBConnectorClass := GetClass('T'+dbconnectorname+'DBConnector');
580   if assigned(DBConnectorClass) then
581     DBConnector := TDBConnectorClass(DBConnectorClass).create
582   else Raise Exception.Create('Unknown db connector specified: ' + 'T'+dbconnectorname+'DBConnector');
583   inc(DBConnectorRefCount);
584 end;
585 
586 procedure FreeDBConnector;
587 begin
588   dec(DBConnectorRefCount);
589   if DBConnectorRefCount=0 then
590     FreeAndNil(DBConnector);
591 end;
592 
DateTimeToTimeStringnull593 function DateTimeToTimeString(d: tdatetime): string;
594 var
595   millisecond: word;
596   second     : word;
597   minute     : word;
598   hour       : word;
599 begin
600   // Format the datetime in the format hh:nn:ss.zzz, where the hours can be bigger then 23.
601   DecodeTime(d,hour,minute,second,millisecond);
602   hour := hour + (trunc(d) * 24);
603   result := Format('%.2d:%.2d:%.2d.%.3d',[hour,minute,second,millisecond]);
604 end;
605 
TimeStringToDateTimenull606 function TimeStringToDateTime(d: String): TDateTime;
607 var
608   millisecond: word;
609   second     : word;
610   minute     : word;
611   hour       : word;
612   days       : word;
613 begin
614   // Convert the string in the format hh:nn:ss.zzz to a datetime.
615   hour := strtoint(copy(d,1,2));
616   minute := strtoint(copy(d,4,2));
617   second := strtoint(copy(d,7,2));
618   millisecond := strtoint(copy(d,10,3));
619 
620   days := hour div 24;
621   hour := hour mod 24;
622 
623   result := ComposeDateTime(days,EncodeTime(hour,minute,second,millisecond));
624 end;
625 
StringToByteArraynull626 function StringToByteArray(const s: ansistring): Variant;
627 var P: Pointer;
628     Len: integer;
629 begin
630   Len := Length(s) * SizeOf(AnsiChar);
631   Result := VarArrayCreate([0, Len-1], varByte);
632   P := VarArrayLock(Result);
633   try
634     Move(s[1], P^, Len);
635   finally
636     VarArrayUnlock(Result);
637   end;
638 end;
639 
640 
641 initialization
642   ReadIniFile;
643   SetupLog;
644   DBConnectorRefCount:=0;
645 
646 finalization
647   CloseLog;
648 end.
649 
650