
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8094 8e941d3f-bd1b-0410-a28a-d453659cc2b4
1432 lines
49 KiB
ObjectPascal
1432 lines
49 KiB
ObjectPascal
{ testdbwriter
|
|
|
|
Copyright (C) 2012-2014 Reinier Olislagers
|
|
|
|
This library is free software; you can redistribute it and/or modify it
|
|
under the terms of the GNU Library General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or (at your
|
|
option) any later version with the following modification:
|
|
|
|
As a special exception, the copyright holders of this library give you
|
|
permission to link this library with independent modules to produce an
|
|
executable, regardless of the license terms of these independent modules,and
|
|
to copy and distribute the resulting executable under terms of your choice,
|
|
provided that you also meet, for each linked independent module, the terms
|
|
and conditions of the license of that module. An independent module is a
|
|
module which is not derived from or based on this library. If you modify
|
|
this library, you may extend this exception to your version of the library,
|
|
but you are not obligated to do so. If you do not wish to do so, delete this
|
|
exception statement from your version.
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
|
|
for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; if not, write to the Free Software Foundation,
|
|
Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
}
|
|
|
|
{
|
|
Purpose:
|
|
This unit contains a database TestListener for use with the fpcUnit testing
|
|
framework. It puts test results into a database, which can be useful for
|
|
regression testing, reporting etc.
|
|
|
|
To set the database to be used, you can use a testdbwriter.ini file, The
|
|
format of this file is similar to that used in the FPC db test framework.
|
|
Please see testdbwriter.ini.txt for an example.
|
|
|
|
If you don't use testdbwriter.ini, the code will try to create and use a
|
|
Firebird embedded database.
|
|
|
|
You can also override the selection from your test application by setting
|
|
the DB* properties.
|
|
|
|
This code uses simple insert/select SQL statements so it should be
|
|
compatible with most RDBMS. Varchar size is a factor though; 800 is chosen
|
|
to avoid Firebird "key size too big for index" errors.
|
|
|
|
The database must support autoincrement/autonumber fields or equivalents
|
|
(eg. sequences/generators and triggers). Firebird embedded database support
|
|
is included; if the database does not exist, it can be created. Please see
|
|
the testdbwriter_firebird.sql file for Firebird database statements you can
|
|
adapt to your own database system.
|
|
Firebird notes: the index sizes in the DDL requires page_size 16384.
|
|
|
|
A PostgreSQL SQL file is also included.
|
|
|
|
No autocreate support for sqlite yet; patches welcome.
|
|
|
|
}
|
|
// todo: change flat testsuite into testsuitepath; add normal column testsuitename
|
|
// todo: add regressionflat modification to postgresql (using testsuiteflat instead of regular testsuite)
|
|
// todo: verify/add views in firebird db to postgres schema as well
|
|
// todo: performance optimalizations => triggers instead of ctes etc
|
|
|
|
// Enable to get logging/writelns in order to troubleshoot
|
|
// database problems/program correctness. Note: uses writeln; only for console applications.
|
|
{.$DEFINE DEBUGCONSOLE}
|
|
|
|
|
|
{$IF FPC_FULLVERSION<20601}
|
|
// FPC < 2.6.1 doesn't yet have LocalTimeToUniversal.
|
|
{$ERROR "This version of FPC does not support getting UTC time!"}
|
|
// Use UTC time or what the local clock tells us? For consistency, UTC is best:
|
|
// It will eliminate inconsistency between *nix hosts (mostly UTC clocks)
|
|
// and Windows hosts (mostly local clocks)
|
|
{$ENDIF}
|
|
|
|
unit testdbwriter;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$IFDEF MSWINDOWS}
|
|
{$r *.rc} //include SQL for Firebird embedded
|
|
{$ELSE}
|
|
// unfortunately, the installers on e.g. Linux do not have windres as a install dependency
|
|
// so we make do with the (possibly stale) .res file
|
|
{$r *.res} //include SQL for Firebird embedded
|
|
{$ENDIF}
|
|
|
|
interface
|
|
uses
|
|
Classes
|
|
,SysUtils
|
|
,fpcUnit
|
|
,TestUtils
|
|
,sqldb
|
|
,db
|
|
{ Even if we use sqlconnector we need to add the relevant db unit to the uses clause in the project.
|
|
In order to make it simpler for the tester to share programs across db systems, we choose to add the units
|
|
here; if needed, other units can be added by the test writer in his test program. }
|
|
,mysql40conn, mysql41conn, mysql50conn, mysql51conn, mysql55conn
|
|
,ibconnection
|
|
,mssqlconn
|
|
,odbcconn
|
|
{$IFNDEF WIN64}
|
|
,oracleconnection
|
|
{$ENDIF}
|
|
,pqconnection
|
|
,sqlite3conn
|
|
,inifiles
|
|
,ibase60dyn
|
|
,dateutils
|
|
,resource;
|
|
|
|
const
|
|
TDBW_INVALID_DB_ID=0; //Used as a marker for an invalid database ID
|
|
TDBW_DBVERSION='20121202'; //Update this whenever the database schema changes
|
|
|
|
// Some known database connector names to be used for FDBType; the
|
|
// values are the connectiondef name for the sqldb connector
|
|
TDBW_FIREBIRDCONN_NAME='Firebird';
|
|
TDBW_MSSQLCONN_NAME='MSSQLServer';
|
|
TDBW_ODBCCONN_NAME='ODBC';
|
|
TDBW_ORACLECONN_NAME='Oracle';
|
|
TDBW_POSTGRESQLCONN_NAME='PostGreSQL';
|
|
TDBW_SQLITECONN_NAME='SQLite3';
|
|
TDBW_SYBASECONN_NAME='Sybase';
|
|
|
|
type
|
|
|
|
{ TDBResultsWriter }
|
|
TDBResultsWriter = class(TNoRefCountObject, ITestListener)
|
|
protected
|
|
FApplicationName: string;
|
|
FComment: string; //User-specified comment for test run
|
|
FConn: TSQLConnector;
|
|
FCPU: string; //Processor used (name uses FPC terminology)
|
|
FDBProfile: string; //Name of the profile in the ini file the user chose. Only useful in error messages to user
|
|
FDBSetup: boolean; //Track if database is set up including adding test run data
|
|
FDBType: string; //Output database type chosen by user
|
|
FDBHostname: string; //Host/IP address of database server. Empty string for embedded dbs
|
|
FDBName: string; //Name/path for database
|
|
FDBUser: string; //Username
|
|
FDBPassword: string; //Password needed for username
|
|
FDBCharset: string; //Character encoding for db connection
|
|
FOS: string; //Operating system used (name uses FPC terminology)
|
|
FRevisionID: string; //Unique identifier for source code being tested
|
|
FRootSuite: TStringList; //This list of testsuite names will be prepended to the test suite hierarchy when tests are run
|
|
FTestSuitePath: TStringList; //Stack of suite names (and IDs) up to the current test suite. Will include FRootSuite
|
|
FTran: TSQLTransaction;
|
|
|
|
// Prepared queries for performance as well as one general purpose query:
|
|
FQuery: TSQLQuery; //General purpose query
|
|
FAddTestRun: TSQLQuery;
|
|
FGetApplicationName: TSQLQuery;
|
|
FGetCPU: TSQLQuery;
|
|
FGetExceptionMessage: TSQLQuery;
|
|
FGetExceptionClass: TSQLQuery;
|
|
FGetMethodName: TSQLQuery;
|
|
FGetOS: TSQLQuery;
|
|
FGetResultValue: TSQLQuery;
|
|
FGetSourceLocation: TSQLQuery;
|
|
FGetSourceUnit: TSQLQuery;
|
|
FGetTestSuite: TSQLQuery;
|
|
FGetTestSuiteParent: TSQLQuery;
|
|
FGetTestName: TSQLQuery;
|
|
FGetTestResultID: TSQLQuery;
|
|
FInsertExceptionMessage: TSQLQuery;
|
|
FInsertSourceLocation: TSQLQuery;
|
|
FInsertTest: TSQLQuery;
|
|
FInsertTestResult: TSQLQuery;
|
|
FInsertTestSuiteParent: TSQLQuery;
|
|
FUpdateTestResult: TSQLQuery;
|
|
FUpdateTestResultException: TSQLQuery;
|
|
FUpdateTestResultExceptionError: TSQLQuery;
|
|
FUpdateTestResultOK: TSQLQuery;
|
|
|
|
FResultErrorID: integer; //Cached result value ID
|
|
FResultFailedID: integer; //Cached result value ID
|
|
FResultIgnoredID: integer; //Cached result value ID
|
|
FResultOKID: integer; //Cached result value ID
|
|
FStartRun: TDateTime; //Start of running entire test run, in UTC if possible. Used to identify test run record.
|
|
FStartTest: TDateTime; //Start of running test suite
|
|
FTestResultID: integer; //DB identifier of current test
|
|
FTestRunID: integer; //DB identifier of the test run - the list of tests we ran with their results and messages
|
|
FTestResultOK: boolean; //Test framework doesn't seem to track succesful tests?!?! So we need to do it ourselves
|
|
FTestSuiteID: integer; //DB identifier of current test
|
|
{ Looks up PK based on the parameters given; if not present, inserts using InsertSQL}
|
|
function AddOrFindKey(GetQuery: TSQLQuery; InsertSQL: string; ParamValue: String): integer; virtual;
|
|
{ Creates a new database. Called automatically; only implemented for Firebird embedded/sqlite }
|
|
function CreateDatabase: boolean; virtual;
|
|
{ Create database tables. Called automatically; only implemented for Firebird embedded }
|
|
procedure CreateTables; virtual;
|
|
{ Sets up database connection, if necessary create embedded db, and set up queries }
|
|
procedure DatabaseSetup; virtual;
|
|
{ Looks up exception class and message in db; if necessary adds them.
|
|
Returns EXCEPTIONMESSAGES.ID }
|
|
function DBGetExceptionMessageID(ExceptionClass, ExceptionMessage: string): integer; virtual;
|
|
{ Looks up method name in db; if necessary adds it.}
|
|
function DBGetMethodNameID(Name: string): integer; virtual;
|
|
{ Looks up result value ID in db; if necessary adds it }
|
|
function DBGetResultValueID(ResultText: string): integer; virtual;
|
|
{ Looks up source location ID; if necessary adds data }
|
|
function DBGetSourceLocationID(SourceUnit: string; Line: integer):integer; virtual;
|
|
{ Looks up test suite name/test name combination in db; if necessary adds name}
|
|
function DBGetTestID(TestName: string): integer; virtual;
|
|
{ Looks up test suite in db that has no parent; if necessary adds it}
|
|
function DBGetTestSuiteID(TestSuiteName: string): integer;
|
|
{ Looks up test suite with parent in db; if necessary adds it with given hierarchical depth}
|
|
function DBGetTestSuiteID(TestSuiteName: string; Depth, ParentSuiteID: integer): integer;
|
|
{ Generate a unique ID, used to identify newly inserted records in autonumber tables }
|
|
function GetUniqueID: string; virtual;
|
|
function GetTestRunID: string; virtual;
|
|
{$IFDEF DEBUGCONSOLE}
|
|
procedure GetLogEvent(Sender: TSQLConnection;
|
|
EventType: TDBEventType; const Msg: String);
|
|
{$ENDIF DEBUGCONSOLE}
|
|
{ Insert test run info at the beginning of a test run. Needs db setup; called when processing first test suite. }
|
|
procedure InsertTestRun; virtual;
|
|
{ Reads testdbwriter.ini to get database connection details. Defaults to firebird embedded if no testdbwriter.ini found}
|
|
procedure ReadSettings; virtual;
|
|
{ Configures an auxiliary Firebird connection for Firebird-specific functionality }
|
|
procedure SetupIBConnection(var IBConn: TIBConnection); virtual;
|
|
{ Helper method to set up a class level query object }
|
|
procedure SetupQuery(var Query: TSQLQuery; SQL: string);
|
|
public
|
|
{ Name of the application that is running the tests. Optional: if nothing chosen, the current paramstr(0) will be used }
|
|
property ApplicationName: string write FApplicationName;
|
|
{ Comment regarding the test run }
|
|
property Comment: string write FComment;
|
|
{ CPU on which the tests are run. Note: default already set by testdbwriter based on compiler defines}
|
|
property CPU: string read FCPU write FCPU;
|
|
{ Character set/encoding used by test storage database. Optional, overrides values set by ini file (if any) }
|
|
property DatabaseCharset: string read FDBCharset write FDBCharset;
|
|
{ Host ip address/name for test storage database. Leave blank for embedded. Optional, overrides values set by ini file (if any)}
|
|
property DatabaseHostname: string read FDBHostname write FDBHostname;
|
|
{ Name of the test storage database. Optional, overrides values set by ini file (if any)}
|
|
property DatabaseName: string read FDBName write FDBName;
|
|
{ Connector type for test storage database. Use connector name. Optional, overrides values set by ini file (if any).
|
|
If no type ends up being selected, a Firebird embedded database will be created/used }
|
|
property DatabaseType: string read FDBType write FDBType;
|
|
{ Password for test storage database. Optional, overrides values set by ini file (if any)}
|
|
property DatabasePassword: string read FDBPassword write FDBPassword;
|
|
{ Username for test storage database. Optional, overrides values set by ini file (if any)}
|
|
property DatabaseUser: string read FDBUser write FDBUser;
|
|
{ Operating system on which the tests are run. Note: default already set by testdbwriter based on compiler defines }
|
|
property OS: string read FOS write FOS;
|
|
{ Unique identifier for the revision/version for the source code under test }
|
|
property RevisionID: string write FRevisionID;
|
|
{ Unique ID for test run in database }
|
|
property TestRunID: string read GetTestRunID;
|
|
{ You can use this to specify a root hierarchy of testsuite names. The test suites will be added below this hierarchy.
|
|
This is useful e.g. for later comparing the same tests against different environments/backends/databases/configurations }
|
|
property TestSuiteRoot: TStringList read FRootSuite;
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
|
|
{ ITestListener interface requirements }
|
|
{ Called when a test results in a failure. Updates test record with failure info. }
|
|
procedure AddFailure(ATest: TTest; AFailure: TTestFailure);
|
|
{ Called when a test results in an error. Updates test record with error info. }
|
|
procedure AddError(ATest: TTest; AError: TTestFailure);
|
|
{ Called at the beginning of a test within a testsuite. Inserts new test record. }
|
|
procedure StartTest(ATest: TTest);
|
|
{ Called at the end of a test within a testsuite. Updates test record with elapsed time etc. }
|
|
procedure EndTest(ATest: TTest);
|
|
{ Called at the beginning of a testsuite. Inserts testsuite name if needed.}
|
|
procedure StartTestSuite(ATestSuite: TTestSuite);
|
|
{ Called at the end of a testsuite. Virtually empty procedure here.}
|
|
procedure EndTestSuite(ATestSuite: TTestSuite);
|
|
end;
|
|
|
|
|
|
implementation
|
|
|
|
const
|
|
RESULT_ERROR='Error'; //unknown if this should be used
|
|
RESULT_FAILED='Failed'; //Used in RESULTVALUES table
|
|
RESULT_IGNORED='Ignored'; //Used in RESULTVALUES table
|
|
RESULT_OK='OK'; //Used in RESULTVALUES table
|
|
|
|
{ TDBResultsWriter }
|
|
|
|
function TDBResultsWriter.CreateDatabase: boolean;
|
|
var
|
|
IBConn:TIBConnection;
|
|
begin
|
|
result:=false;
|
|
// For now, only Firebird embedded. SQLite creates missing dbs automatically.
|
|
if (FConn.ConnectorType=TDBW_FIREBIRDCONN_NAME) and
|
|
(FConn.HostName='') and
|
|
(not FileExists(FDBName)) then
|
|
begin
|
|
if FConn.DatabaseName='' then Exception.Create('No database name specified. This is required for embedded databases.');
|
|
IBConn:=TIBConnection.Create(nil);
|
|
try
|
|
SetupIBConnection(IBConn);
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('Creating Firebird db:');
|
|
writeln('ibconn.username: '+ibconn.UserName);
|
|
writeln('ibconn.dbname: '+ibconn.DatabaseName);
|
|
writeln('ibconn.dialect: '+inttostr(ibconn.Dialect));
|
|
{$ENDIF DEBUGCONSOLE}
|
|
IBConn.CreateDB;
|
|
finally
|
|
IBConn.Free;
|
|
end;
|
|
result:=true;
|
|
end;
|
|
end;
|
|
|
|
|
|
function TDBResultsWriter.AddOrFindKey(GetQuery: TSQLQuery; InsertSQL:string; ParamValue: string): integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
GetQuery.Params[0].AsString:=ParamValue;
|
|
try
|
|
GetQuery.Open;
|
|
if GetQuery.RecordCount>0 then
|
|
result:=GetQuery.Fields[0].AsInteger;
|
|
GetQuery.Close;
|
|
except
|
|
//ignore
|
|
end;
|
|
if result=TDBW_INVALID_DB_ID then
|
|
begin
|
|
// Doesn't exist, so add and retrieve PK
|
|
try
|
|
FQuery.SQL.Text:=InsertSQL;
|
|
FQuery.Params[0].AsString:=ParamValue;
|
|
FQuery.ExecSQL;
|
|
except
|
|
//ignore duplicate key failure or violations due to NOT NULL fields
|
|
end;
|
|
GetQuery.Params[0].AsString:=ParamValue;
|
|
try
|
|
GetQuery.Open;
|
|
if GetQuery.RecordCount>0 then
|
|
result:=GetQuery.Fields[0].AsInteger;
|
|
GetQuery.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TDBResultsWriter.SetupQuery(var Query: TSQLQuery; SQL: string);
|
|
begin
|
|
Query:=TSQLQuery.Create(nil);
|
|
Query.SQL.Text:=SQL;
|
|
Query.DataBase:=FConn;
|
|
end;
|
|
|
|
procedure TDBResultsWriter.CreateTables;
|
|
// Creates required tables in database.
|
|
// Uses include file generated from Flamerobin DDL dump
|
|
var
|
|
FBScript:TSQLScript;
|
|
ScriptResource: TResourceStream;
|
|
ScriptText: TStringList;
|
|
IBConn:TIBConnection;
|
|
IBTran:TSQLTransaction;
|
|
TranWasStarted: boolean;
|
|
dummypchar:pchar;
|
|
begin
|
|
// We currently only support firebird sql
|
|
if (FConn.ConnectorType=TDBW_FIREBIRDCONN_NAME) then
|
|
begin
|
|
TranWasStarted:=FTran.Active;
|
|
if TranWasStarted then
|
|
begin
|
|
// We want FTran to see our DDL later...
|
|
FTran.Commit;
|
|
end;
|
|
FBScript:=TSQLScript.Create(nil);
|
|
IBConn:=TIBConnection.Create(nil);
|
|
IBTran:=TSQLTransaction.Create(nil);
|
|
dummypchar:=(PChar(RT_RCDATA));
|
|
ScriptResource:=TResourceStream.Create(hInstance,'SQLSCRIPT',PChar(RT_RCDATA));
|
|
ScriptText:=TStringList.Create;
|
|
try
|
|
SetupIBConnection(IBConn);
|
|
IBConn.Transaction:=IBTran;
|
|
ScriptText.LoadFromStream(ScriptResource);
|
|
FBScript.UseCommit:=true; //needed for Firebird DDL+DML statements
|
|
FBScript.UseSetTerm:=true; //needed for Firebird SP definitions
|
|
FBScript.DataBase:=IBConn;
|
|
FBScript.Transaction:=IBTran;
|
|
FBScript.Script:=ScriptText;
|
|
IBTran.StartTransaction;
|
|
try
|
|
FBScript.Execute;
|
|
//todo: investigate: fb is half created; no triggers; procedure recalc indexes exists but is empty
|
|
except
|
|
on E: EIBDatabaseError do
|
|
begin
|
|
// Only tell the user something is up in debug mode:
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('TestDBWriter: CreateTables: error running script: GDS error '+inttostr(E.GDSErrorCode)+'('+E.Message+')');
|
|
{$ENDIF DEBUGCONSOLE}
|
|
halt(16);
|
|
end;
|
|
on F: Exception do
|
|
begin
|
|
// Only tell the user something is up in debug mode:
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('TestDBWriter: CreateTables: error running script: '+F.ClassName+'/'+F.Message);
|
|
{$ENDIF DEBUGCONSOLE}
|
|
halt(17);
|
|
end;
|
|
end;
|
|
|
|
IBTran.Commit;
|
|
finally
|
|
FBScript.Free;
|
|
IBConn.Free;
|
|
IBTran.Free;
|
|
ScriptText.Free;
|
|
ScriptResource.Free;
|
|
end;
|
|
FConn.Transaction:=FTran; //apparently this info gets lost, perhaps only on FPC 2.6.0!??
|
|
FTran.StartTransaction;
|
|
//Indicate database schema version, useful for upgrade scripts
|
|
FConn.ExecuteDirect('INSERT INTO OPTIONS (OPTIONNAME, OPTIONVALUE,REMARKS) VALUES (''DBVERSION'','''+TDBW_DBVERSION+''',''Database schema version'')');
|
|
FTran.Commit;
|
|
if TranWasStarted then
|
|
begin
|
|
FTran.StartTransaction;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
raise Exception.CreateFmt('No support for autocreate tables for database type %s', [FDBType]);
|
|
end;
|
|
end;
|
|
|
|
procedure TDBResultsWriter.DatabaseSetup;
|
|
var
|
|
CreatedDB: boolean;
|
|
begin
|
|
FDBSetup:=true; //try setup only once. If it fails, doing it again won't help.
|
|
|
|
// Translate some connector names as a courtesy:
|
|
FConn:=TSQLConnector.Create(nil);
|
|
case lowercase(FDBType) of
|
|
'interbase', 'firebird', 'firebirdembedded': FConn.ConnectorType:=
|
|
TDBW_FIREBIRDCONN_NAME;
|
|
'mssql', 'sql', 'sqlserver': FConn.ConnectorType:=TDBW_MSSQLCONN_NAME;
|
|
'odbc': FConn.ConnectorType:=TDBW_ODBCCONN_NAME;
|
|
'oracle': FConn.ConnectorType:=TDBW_ORACLECONN_NAME;
|
|
'pq', 'postgres', 'postgresql': FConn.ConnectorType:=TDBW_POSTGRESQLCONN_NAME;
|
|
'sqlite', 'sqlite3': FConn.ConnectorType:=TDBW_SQLITECONN_NAME;
|
|
'sybase', 'sybasease': FConn.ConnectorType:=TDBW_SYBASECONN_NAME;
|
|
else FConn.ConnectorType:=FDBType;
|
|
end;
|
|
|
|
// Make sure Firebird embedded gets loaded (on old FPC versions):
|
|
if (FConn.ConnectorType=TDBW_FIREBIRDCONN_NAME) and (FDBHostname='') then
|
|
UseEmbeddedFirebird:=true;
|
|
|
|
{$IFDEF DEBUGCONSOLE}
|
|
FConn.LogEvents:=LogAllEvents;
|
|
FConn.OnLog:=@Self.GetLogEvent;
|
|
{$ENDIF DEBUGCONSOLE}
|
|
FConn.HostName:=FDBHostname;
|
|
FConn.DatabaseName:=FDBName;
|
|
FConn.UserName:=FDBUser;
|
|
FConn.Password:=FDBPassword;
|
|
FConn.Charset:=FDBCharset;
|
|
|
|
FTran:=TSQLTransaction.Create(nil);
|
|
FConn.Transaction:=FTran;
|
|
|
|
// Check for db/tables and create if necessary
|
|
// (only suppported on some systems):
|
|
CreatedDB:=CreateDatabase;
|
|
|
|
FConn.Open;
|
|
if CreatedDB then CreateTables;
|
|
|
|
//Apparently this info gets lost by the createtable calls!?
|
|
FConn.Transaction:=FTran;
|
|
FTran.StartTransaction;
|
|
|
|
SetupQuery(FQuery, ''); //general purpose query
|
|
|
|
SetupQuery(FGetApplicationName,
|
|
'SELECT a.ID '+
|
|
'FROM APPLICATIONS a '+
|
|
'WHERE '+
|
|
'a.NAME = :NAME ');
|
|
|
|
SetupQuery(FGetCPU,
|
|
'SELECT a.ID '+
|
|
'FROM CPU a '+
|
|
'WHERE '+
|
|
'a.CPUNAME = :CPUNAME ');
|
|
|
|
SetupQuery(FGetExceptionMessage,
|
|
'SELECT a.ID ' +
|
|
'FROM EXCEPTIONMESSAGES a ' +
|
|
'WHERE ' +
|
|
'a.EXCEPTIONCLASS = :EXCEPTIONCLASS AND ' +
|
|
'a.EXCEPTIONMESSAGE = :EXCEPTIONMESSAGE ');
|
|
|
|
SetupQuery(FGetExceptionClass,
|
|
'SELECT a.ID ' +
|
|
'FROM EXCEPTIONCLASSES a ' +
|
|
'WHERE ' +
|
|
'a.EXCEPTIONCLASS = :EXCEPTIONCLASS ');
|
|
|
|
SetupQuery(FGetMethodName,
|
|
'SELECT a.ID ' +
|
|
'FROM METHODNAMES a ' +
|
|
'WHERE ' +
|
|
'a.NAME = :NAME ');
|
|
|
|
SetupQuery(FGetOS,
|
|
'SELECT a.ID '+
|
|
'FROM OS a '+
|
|
'WHERE '+
|
|
'a.OSNAME = :OSNAME ');
|
|
|
|
SetupQuery(FGetResultValue,
|
|
'SELECT a.ID ' +
|
|
'FROM RESULTVALUES a ' +
|
|
'WHERE ' +
|
|
'a.NAME = :NAME ');
|
|
|
|
SetupQuery(FGetSourceLocation,
|
|
'SELECT a.ID ' +
|
|
'FROM SOURCELOCATIONS a ' +
|
|
'WHERE ' +
|
|
'a.SOURCEUNIT = :SOURCEUNIT AND ' +
|
|
'a.LINE = :LINE ');
|
|
|
|
SetupQuery(FGetSourceUnit,
|
|
'SELECT a.ID ' +
|
|
'FROM SOURCEUNITS a ' +
|
|
'WHERE ' +
|
|
'a.NAME = :NAME ');
|
|
|
|
// Gets only top-level test suites
|
|
SetupQuery(FGetTestSuite,
|
|
'SELECT a.ID ' +
|
|
'FROM TESTSUITES a ' +
|
|
'WHERE ' +
|
|
'a.NAME = :NAME AND ' +
|
|
'a.PARENTSUITE IS NULL ');
|
|
|
|
// Gets test suites with specified parent suite
|
|
SetupQuery(FGetTestSuiteParent,
|
|
'SELECT a.ID ' +
|
|
'FROM TESTSUITES a ' +
|
|
'WHERE ' +
|
|
'a.PARENTSUITE=:PARENTSUITE AND '+
|
|
'a.NAME = :NAME ');
|
|
|
|
SetupQuery(FGetTestName,
|
|
'SELECT a.ID ' +
|
|
'FROM TESTS a ' +
|
|
'WHERE ' +
|
|
'a.TESTSUITE = :TESTSUITE AND ' +
|
|
'a.NAME = :NAME ');
|
|
|
|
SetupQuery(FInsertExceptionMessage,
|
|
'INSERT INTO EXCEPTIONMESSAGES (EXCEPTIONCLASS,EXCEPTIONMESSAGE) VALUES (:'
|
|
+'EXCEPTIONCLASS,:EXCEPTIONMESSAGE) ');
|
|
|
|
SetupQuery(FInsertSourceLocation,
|
|
'INSERT INTO SOURCELOCATIONS (SOURCEUNIT,LINE) VALUES (:SOURCEUNIT,:LINE) '
|
|
);
|
|
|
|
SetupQuery(FGetTestResultID,
|
|
'SELECT ID FROM TESTRESULTS WHERE TESTRUN=:TESTRUN AND TEST=:TEST '+
|
|
'AND RESULTCOMMENT=:RESULTCOMMENT ');
|
|
|
|
SetupQuery(FInsertTest,
|
|
'INSERT INTO TESTS (TESTSUITE,NAME) VALUES (:TESTSUITE,:NAME) ');
|
|
|
|
SetupQuery(FInsertTestResult,
|
|
'INSERT INTO TESTRESULTS (TESTRUN,TEST,RESULTCOMMENT) VALUES (:TESTRUN,:TEST,:RESULTCOMMENT) ');
|
|
|
|
SetupQuery(FInsertTestSuiteParent,
|
|
'INSERT INTO TESTSUITES (PARENTSUITE,NAME,DEPTH) VALUES (:PARENTSUITE,:NAME,:DEPTH) ');
|
|
|
|
SetupQuery(FUpdateTestResult,
|
|
'UPDATE TESTRESULTS SET ELAPSEDTIME=:ELAPSEDTIME, RESULTCOMMENT=:RESULTCOMMENT '+
|
|
'WHERE ID=:ID ');
|
|
|
|
SetupQuery(FUpdateTestResultException,
|
|
'UPDATE TESTRESULTS SET RESULTVALUE=:RESULTVALUE, ' +
|
|
'EXCEPTIONMESSAGE=:EXCEPTIONMESSAGE WHERE ID=:ID ');
|
|
|
|
SetupQuery(FUpdateTestResultExceptionError,
|
|
'UPDATE TESTRESULTS SET RESULTVALUE=:RESULTVALUE, '+
|
|
'EXCEPTIONMESSAGE=:EXCEPTIONMESSAGE, METHODNAME=:METHODNAME, '+
|
|
'SOURCELOCATION=:SOURCELOCATION '+
|
|
'WHERE ID=:ID ');
|
|
|
|
SetupQuery(FUpdateTestResultOK,
|
|
'UPDATE TESTRESULTS SET ELAPSEDTIME=:ELAPSEDTIME, '+
|
|
'RESULTVALUE=:RESULTVALUE, RESULTCOMMENT=:RESULTCOMMENT WHERE ID=:ID ');
|
|
end;
|
|
|
|
|
|
function TDBResultsWriter.DBGetExceptionMessageID(ExceptionClass,
|
|
ExceptionMessage: string): integer;
|
|
var
|
|
ID: integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
ID:=AddOrFindKey(FGetExceptionClass,
|
|
'INSERT INTO EXCEPTIONCLASSES (EXCEPTIONCLASS) VALUES (:EXCEPTIONCLASS) ',
|
|
ExceptionClass);
|
|
if ID<>TDBW_INVALID_DB_ID then
|
|
begin
|
|
// We can't call AddOrFindKey due to integer parameters, so do it manually:
|
|
result:=TDBW_INVALID_DB_ID;
|
|
FGetExceptionMessage.Params[0].AsInteger:=ID;
|
|
FGetExceptionMessage.Params[1].AsString:=ExceptionMessage;
|
|
try
|
|
FGetExceptionMessage.Open;
|
|
if FGetExceptionMessage.recordcount>0 then
|
|
result:=FGetExceptionMessage.Fields[0].AsInteger;
|
|
FGetExceptionMessage.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
if result=TDBW_INVALID_DB_ID then
|
|
begin
|
|
// Doesn't exist yet
|
|
try
|
|
FInsertExceptionMessage.Params[0].AsInteger:=ID;
|
|
FInsertExceptionMessage.Params[1].AsString:=ExceptionMessage;
|
|
FInsertExceptionMessage.ExecSQL;
|
|
except
|
|
//ignore duplicate key failure
|
|
end;
|
|
|
|
FGetExceptionMessage.Params[0].AsInteger:=ID;
|
|
FGetExceptionMessage.Params[1].AsString:=ExceptionMessage;
|
|
try
|
|
FGetExceptionMessage.Open;
|
|
if FGetExceptionMessage.recordcount>0 then
|
|
result:=FGetExceptionMessage.Fields[0].AsInteger;
|
|
FGetExceptionMessage.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetMethodNameID(Name: string): integer;
|
|
var
|
|
ID: integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
ID:=AddOrFindKey(FGetMethodName,
|
|
'INSERT INTO METHODNAMES (NAME) VALUES (:NAME) ',
|
|
Name);
|
|
result:=ID;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetResultValueID(ResultText: string): integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
case ResultText of
|
|
RESULT_ERROR:
|
|
begin
|
|
if FResultErrorID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FResultErrorID:=AddOrFindKey(FGetResultValue,
|
|
'INSERT INTO RESULTVALUES (NAME) VALUES (:NAME) ',
|
|
ResultText);
|
|
end;
|
|
result:=FResultErrorID;
|
|
end;
|
|
RESULT_FAILED:
|
|
begin
|
|
if FResultFailedID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FResultFailedID:=AddOrFindKey(FGetResultValue,
|
|
'INSERT INTO RESULTVALUES (NAME) VALUES (:NAME) ',
|
|
ResultText);
|
|
end;
|
|
result:=FResultFailedID;
|
|
end;
|
|
RESULT_IGNORED:
|
|
begin
|
|
if FResultIgnoredID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FResultIgnoredID:=AddOrFindKey(FGetResultValue,
|
|
'INSERT INTO RESULTVALUES (NAME) VALUES (:NAME) ',
|
|
ResultText);
|
|
end;
|
|
result:=FResultIgnoredID;
|
|
end;
|
|
RESULT_OK:
|
|
begin
|
|
if FResultOKID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FResultOKID:=AddOrFindKey(FGetResultValue,
|
|
'INSERT INTO RESULTVALUES (NAME) VALUES (:NAME) ',
|
|
ResultText);
|
|
end;
|
|
result:=FResultOKID;
|
|
end
|
|
else
|
|
// Not cached, but we can still look it up
|
|
result:=AddOrFindKey(FGetResultValue,
|
|
'INSERT INTO RESULTVALUES (NAME) VALUES (:NAME) ',
|
|
ResultText);
|
|
end;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetSourceLocationID(SourceUnit: string;
|
|
Line: integer): integer;
|
|
var
|
|
ID: integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
ID:=AddOrFindKey(FGetSourceUnit,
|
|
'INSERT INTO SOURCEUNITS (NAME) VALUES (:SOURCEUNIT) ',
|
|
SourceUnit);
|
|
if ID<>TDBW_INVALID_DB_ID then
|
|
begin
|
|
// We can't call AddOrFindKey due to integer parameters, so do it manually:
|
|
result:=TDBW_INVALID_DB_ID;
|
|
FGetSourceLocation.Params[0].AsInteger:=ID;
|
|
FGetSourceLocation.Params[1].AsInteger:=Line;
|
|
try
|
|
FGetSourceLocation.Open;
|
|
if FGetSourceLocation.recordcount>0 then
|
|
result:=FGetSourceLocation.Fields[0].AsInteger;
|
|
FGetSourceLocation.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
if result=TDBW_INVALID_DB_ID then
|
|
begin
|
|
// Doesn't exist, so add it
|
|
try
|
|
FInsertSourceLocation.Params[0].AsInteger:=ID;
|
|
FInsertSourceLocation.Params[1].AsInteger:=Line;
|
|
FInsertSourceLocation.ExecSQL;
|
|
except
|
|
//ignore duplicate key failure
|
|
end;
|
|
|
|
FGetSourceLocation.Params[0].AsInteger:=ID;
|
|
FGetSourceLocation.Params[1].AsInteger:=Line;
|
|
try
|
|
FGetSourceLocation.Open;
|
|
if FGetSourceLocation.recordcount>0 then
|
|
result:=FGetSourceLocation.Fields[0].AsInteger;
|
|
FGetSourceLocation.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetTestID(TestName: string): integer;
|
|
// We already have FTestSuiteID
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
assert(FTestSuiteID<>TDBW_INVALID_DB_ID,'Test suite ID must be valid.');
|
|
// We can't call AddOrFindKey due to integer parameters, so do it manually:
|
|
result:=TDBW_INVALID_DB_ID;
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('debug: Getting test id for test '+TestName+' in test suite id: '+inttostr(FTestSuiteID));
|
|
{$ENDIF DEBUGCONSOLE}
|
|
FGetTestName.Params[0].AsInteger:=FTestSuiteID;
|
|
FGetTestName.Params[1].AsString:=TestName;
|
|
try
|
|
FGetTestName.Open;
|
|
if FGetTestName.RecordCount>0 then
|
|
result:=FGetTestName.Fields[0].AsInteger;
|
|
FGetTestName.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
|
|
if result=TDBW_INVALID_DB_ID then
|
|
begin
|
|
// Doesn't exist, so add it
|
|
try
|
|
FInsertTest.Params[0].AsInteger:=FTestSuiteID;
|
|
FInsertTest.Params[1].AsString:=TestName;
|
|
FInsertTest.ExecSQL;
|
|
except
|
|
//ignore duplicate key failure
|
|
end;
|
|
|
|
FGetTestName.Params[0].AsInteger:=FTestSuiteID;
|
|
FGetTestName.Params[1].AsString:=TestName;
|
|
try
|
|
FGetTestName.Open;
|
|
if FGetTestName.RecordCount>0 then
|
|
result:=FGetTestName.Fields[0].AsInteger;
|
|
FGetTestName.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetTestSuiteID(TestSuiteName: string): integer;
|
|
// Looks up top-level test suite (parentsuite is null)
|
|
var
|
|
ID: integer;
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
ID:=AddOrFindKey(FGetTestSuite,
|
|
'INSERT INTO TESTSUITES (PARENTSUITE, NAME, DEPTH) VALUES (NULL, :NAME, 0) ',
|
|
TestSuiteName);
|
|
result:=ID;
|
|
end;
|
|
|
|
function TDBResultsWriter.DBGetTestSuiteID(TestSuiteName: string; Depth, ParentSuiteID: integer): integer;
|
|
// Looks up test suite (with given depth) that is a chiled of ParentSuiteID, returns test suite ID
|
|
begin
|
|
result:=TDBW_INVALID_DB_ID;
|
|
assert(ParentSuiteID<>TDBW_INVALID_DB_ID,'Test suite ID must be valid.');
|
|
if ParentSuiteID<>TDBW_INVALID_DB_ID then
|
|
begin
|
|
// We can't call AddOrFindKey due to integer parameters, so do it manually:
|
|
result:=TDBW_INVALID_DB_ID;
|
|
FGetTestSuiteParent.Params[0].AsInteger:=ParentSuiteID;
|
|
FGetTestSuiteParent.Params[1].AsString:=TestSuiteName;
|
|
try
|
|
FGetTestSuiteParent.Open;
|
|
if FGetTestSuiteParent.RecordCount>0 then
|
|
result:=FGetTestSuiteParent.Fields[0].AsInteger;
|
|
FGetTestSuiteParent.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
|
|
if result=TDBW_INVALID_DB_ID then
|
|
begin
|
|
// Doesn't exist, so add it
|
|
try
|
|
FInsertTestSuiteParent.Params[0].AsInteger:=ParentSuiteID;
|
|
FInsertTestSuiteParent.Params[1].AsString:=TestSuiteName;
|
|
FInsertTestSuiteParent.Params[2].AsInteger:=Depth;
|
|
FInsertTestSuiteParent.ExecSQL;
|
|
except
|
|
//ignore duplicate key failure
|
|
end;
|
|
|
|
FGetTestSuiteParent.Params[0].AsInteger:=ParentSuiteID;
|
|
FGetTestSuiteParent.Params[1].AsString:=TestSuiteName;
|
|
try
|
|
FGetTestSuiteParent.Open;
|
|
if FGetTestSuiteParent.RecordCount>0 then
|
|
result:=FGetTestSuiteParent.Fields[0].AsInteger;
|
|
FGetTestSuiteParent.Close;
|
|
except
|
|
result:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TDBResultsWriter.GetUniqueID: string;
|
|
begin
|
|
Result:='In progress; ID: '+FormatDateTime('ssz',Now())+'/'+inttostr(random(32767));
|
|
end;
|
|
|
|
function TDBResultsWriter.GetTestRunID: string;
|
|
// Return a string in case we ever use GUIDs etc.
|
|
begin
|
|
result:=inttostr(FTestRunID);
|
|
end;
|
|
|
|
{$IFDEF DEBUGCONSOLE}
|
|
procedure TDBResultsWriter.GetLogEvent(Sender: TSQLConnection;
|
|
EventType: TDBEventType; const Msg: String);
|
|
// The procedure is called by TSQLConnection and saves the received log messages
|
|
// in the FConnectionLog stringlist
|
|
var
|
|
Source: string;
|
|
begin
|
|
// Nicely right aligned...
|
|
Source:='';
|
|
case EventType of
|
|
detCustom: Source:='Custom: ';
|
|
detExecute: Source:='Execute: ';
|
|
detCommit: Source:='Commit: ';
|
|
detRollBack: Source:='Rollback:';
|
|
end;
|
|
if Source<>'' then
|
|
begin
|
|
writeln(Source + ' ' + Msg);
|
|
end;
|
|
end;
|
|
{$ENDIF DEBUGCONSOLE}
|
|
|
|
procedure TDBResultsWriter.InsertTestRun;
|
|
var
|
|
ApplicationID: integer; //Applications.ID needed for FK value in TestRuns.ApplicationID
|
|
CPUID: integer; //CPU.ID needed for FK value in TestRuns.CPU
|
|
i: integer;
|
|
OSID: integer; //OS.ID needed for FK value in TestRuns.OS
|
|
RandomID: string; //For increasing record uniqueness in db
|
|
TestSuiteID: integer; //ID of current testsuite name
|
|
begin
|
|
// We use this to insert a new run at the beginning of the test run
|
|
// We identify the record by the DATETIMERAN field, as well as a temporary
|
|
// random value in RUNCOMMENT (multiple writers could be updating the db
|
|
// at the same time)
|
|
RandomID:=GetUniqueID;
|
|
// When the run is done, we update the other fields.
|
|
if FApplicationName<>'' then
|
|
ApplicationID:=AddOrFindKey(FGetApplicationName,
|
|
'INSERT INTO APPLICATIONS (NAME) VALUES (:NAME) ',
|
|
FApplicationName);
|
|
if FCPU<>'' then
|
|
CPUID:=AddOrFindKey(FGetCPU,
|
|
'INSERT INTO CPU (CPUNAME) VALUES (:CPUNAME) ',
|
|
FCPU);
|
|
if FOS<>'' then
|
|
OSID:=AddOrFindKey(FGetOS,
|
|
'INSERT INTO OS (OSNAME) VALUES (:OSNAME) ',
|
|
FOS);
|
|
SetupQuery(FAddTestRun,
|
|
'INSERT INTO TESTRUNS (DATETIMERAN,APPLICATIONID,RUNCOMMENT,CPU,OS) ' +
|
|
'VALUES (:DATETIMERAN,:APPLICATIONID,:RUNCOMMENT,:CPU,:OS) ');
|
|
// Now set up test run:
|
|
FAddTestRun.Params[0].AsDateTime:=FStartRun;
|
|
if FApplicationName='' then
|
|
begin
|
|
FAddTestRun.Params[1].Clear;
|
|
FAddTestRun.Params[1].DataType:=ftInteger;
|
|
end
|
|
else
|
|
begin
|
|
FAddTestRun.Params[1].AsInteger:=ApplicationID;
|
|
end;
|
|
FAddTestRun.Params[2].AsString:=RandomID; //temporarily, in comment field
|
|
if FCPU='' then
|
|
begin
|
|
FAddTestRun.Params[3].Clear;
|
|
FAddTestRun.Params[3].DataType:=ftInteger;
|
|
end
|
|
else
|
|
begin
|
|
FAddTestRun.Params[3].AsInteger:=CPUID;
|
|
end;
|
|
if FOS='' then
|
|
begin
|
|
FAddTestRun.Params[4].Clear;
|
|
FAddTestRun.Params[4].DataType:=ftInteger;
|
|
end
|
|
else
|
|
begin
|
|
FAddTestRun.Params[4].AsInteger:=OSID;
|
|
end;
|
|
FAddTestRun.ExecSQL;
|
|
|
|
FTestRunID:=TDBW_INVALID_DB_ID;
|
|
FQuery.SQL.Text:='SELECT ID FROM TESTRUNS '+
|
|
'WHERE DATETIMERAN=:DATETIMERAN AND RUNCOMMENT=:RUNCOMMENT';
|
|
FQuery.Params[0].AsDateTime:=FStartRun;
|
|
FQuery.Params[1].AsString:=RandomID;
|
|
FQuery.Open;
|
|
if FQuery.RecordCount>0 then
|
|
FTestRunID:=FQuery.Fields[0].AsInteger;
|
|
FQuery.Close;
|
|
assert(FTestRunID<>TDBW_INVALID_DB_ID, 'Constructor: test run ID must have been '
|
|
+'created.');
|
|
|
|
// If testsuite prefixes are specified, first clean them up a bit.
|
|
// This only needs to be done once, so running it here makes sense.
|
|
if FRootSuite.Count>0 then
|
|
begin
|
|
// Don't allow empty names:
|
|
for i:=FRootSuite.Count-1 downto 0 do
|
|
begin
|
|
if FRootSuite[i]='' then
|
|
FRootSuite.Delete(i);
|
|
end;
|
|
end;
|
|
|
|
// Add all non-empty testsuite prefixes/names to the testsuite stack
|
|
// that will be used in further processing
|
|
// and look up their IDs
|
|
if FRootSuite.Count>0 then
|
|
begin
|
|
TestSuiteID:=TDBW_INVALID_DB_ID;
|
|
for i:=0 to FRootSuite.Count-1 do
|
|
begin
|
|
if TestSuiteID=TDBW_INVALID_DB_ID then
|
|
TestSuiteID:=DBGetTestSuiteID(FRootSuite[i])
|
|
else
|
|
TestSuiteID:=DBGetTestSuiteID(FRootSuite[i],i,TestSuiteID);
|
|
assert(TestSuiteID<>TDBW_INVALID_DB_ID,'InsertTestRun: TestSuiteID for '+FRootSuite[i]+' must not be invalid. Aborting.');
|
|
{$IFDEF DEBUGCONSOLE}
|
|
// Assertions won't stop executions but apparently restart the test?
|
|
if TestSuiteID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
writeln('Halt: InsertTestRun: TestSuiteID for '+FRootSuite[i]+' must not be invalid. Aborting.');
|
|
halt(17);
|
|
end;
|
|
//todo: debug
|
|
writeln('InsertTestRun: Adding test suite'+FRootSuite[i]+' with id '+inttostr(TestSuiteID)+' to FTestSuitePath');
|
|
{$ENDIF}
|
|
FTestSuitePath.AddObject(FRootSuite[i],TObject(PtrInt(TestSuiteID)));
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TDBResultsWriter.ReadSettings;
|
|
var
|
|
IniFile : TIniFile;
|
|
begin
|
|
//Defaults:
|
|
FDBType:=TDBW_FIREBIRDCONN_NAME;
|
|
FDBHostname:='';
|
|
FDBName:='testdbwriter.fdb';
|
|
FDBUser:='SYSDBA';
|
|
FDBPassword:='masterkey';
|
|
FDBCharset:='UTF8';
|
|
// Read ini file in program directory if possible
|
|
IniFile := TIniFile.Create(ExtractFilePath(ParamStr(0))+'testdbwriter.ini');
|
|
try
|
|
FDBProfile:=IniFile.ReadString('Database','profile','firebirdembedded');
|
|
FDBType:=LowerCase(IniFile.ReadString(FDBProfile,'type','firebird')); //Normalize to lowercase
|
|
FDBHostname := IniFile.ReadString(FDBProfile,'hostname','');
|
|
FDBName := IniFile.ReadString(FDBProfile,'name','testdbwriter.fdb');
|
|
FDBUser := IniFile.ReadString(FDBProfile,'user','SYSDBA');
|
|
FDBPassword := IniFile.ReadString(FDBProfile,'password','masterkey');
|
|
FDBCharset := IniFile.ReadString(FDBProfile,'charset','UTF8');
|
|
finally
|
|
IniFile.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TDBResultsWriter.SetupIBConnection(var IBConn: TIBConnection);
|
|
begin
|
|
IBConn.UserName:=FConn.UserName;
|
|
IBConn.Password:=FConn.Password;
|
|
IBConn.DatabaseName:=FConn.DatabaseName;
|
|
IBConn.CharSet:=FConn.Charset;
|
|
IBConn.Params.Add('PAGE_SIZE=16384'); //important for max index size=>max column size
|
|
end;
|
|
|
|
|
|
constructor TDBResultsWriter.Create;
|
|
begin
|
|
FRootSuite:=TStringList.Create;
|
|
FTestSuitePath:=TStringList.Create;
|
|
// Let's include setup in our run timings... only fair.
|
|
FStartRun:=Now();
|
|
FStartRun:=LocalTimeToUniversal(FStartRun);
|
|
Randomize;
|
|
|
|
// Invalidate cached values
|
|
FResultErrorID:=TDBW_INVALID_DB_ID;
|
|
FResultFailedID:=TDBW_INVALID_DB_ID;
|
|
FResultIgnoredID:=TDBW_INVALID_DB_ID;
|
|
FResultOKID:=TDBW_INVALID_DB_ID;
|
|
|
|
FTestRunID:=TDBW_INVALID_DB_ID;
|
|
FTestResultID:=TDBW_INVALID_DB_ID;
|
|
FTestSuiteID:=TDBW_INVALID_DB_ID;
|
|
|
|
FDBSetup:=false; //Flag to notify test run code to set up the db if required
|
|
|
|
// Strip out extension so you get the same result on Windows, *nix etc:
|
|
FApplicationName:=ChangeFileExt(ExtractFileName(Paramstr(0)),'');
|
|
FCPU:={$I %FPCTARGETCPU%};
|
|
FOS:={$I %FPCTARGETOS%};
|
|
|
|
ReadSettings; //Read INI file settings (which can be overridden by DB* properties)
|
|
end;
|
|
|
|
|
|
destructor TDBResultsWriter.Destroy;
|
|
var
|
|
ElapsedTime:TDateTime;
|
|
EndTime: TDateTime;
|
|
begin
|
|
EndTime:=Now();
|
|
EndTime:=LocalTimeToUniversal(EndTime);
|
|
ElapsedTime:=EndTime-FStartRun;
|
|
|
|
// Update final test run data - but don't stop the destructor on errors
|
|
if FDBSetup then
|
|
begin
|
|
try
|
|
Assert(FTestRunID<>TDBW_INVALID_DB_ID,'Test run id must be valid due to assignment in constructor.');
|
|
FQuery.SQL.Text:='UPDATE TESTRUNS SET '+
|
|
'TOTALELAPSEDTIME=:TOTALELAPSEDTIME, RUNCOMMENT=:RUNCOMMENT, REVISIONID=:REVISIONID '+
|
|
'WHERE ID=:ID ';
|
|
FQuery.Params[0].AsDateTime:=ElapsedTime;
|
|
if FComment='' then
|
|
begin
|
|
FQuery.Params[1].Clear; //null
|
|
FQuery.Params[1].Datatype:=ftstring;
|
|
end
|
|
else
|
|
FQuery.Params[1].AsString:=FComment;
|
|
if FRevisionID='' then
|
|
begin
|
|
FQuery.Params[2].Clear; //null
|
|
FQuery.Params[2].Datatype:=ftstring;
|
|
end
|
|
else
|
|
FQuery.Params[2].AsString:=FRevisionID;
|
|
FQuery.Params[3].AsInteger:=FTestRunID;
|
|
FQuery.ExecSQL;
|
|
FTran.Commit;
|
|
|
|
FAddTestRun.Free;
|
|
FGetApplicationName.Free;
|
|
FGetCPU.Free;
|
|
FGetExceptionMessage.Free;
|
|
FGetExceptionClass.Free;
|
|
FGetMethodName.Free;
|
|
FGetOS.Free;
|
|
FGetResultValue.Free;
|
|
FGetSourceLocation.Free;
|
|
FGetSourceUnit.Free;
|
|
FGetTestSuite.Free;
|
|
FGetTestSuiteParent.Free;
|
|
FGetTestName.Free;
|
|
FGetTestResultID.Free;
|
|
FInsertExceptionMessage.Free;
|
|
FInsertSourceLocation.Free;
|
|
FInsertTest.Free;
|
|
FInsertTestResult.Free;
|
|
FInsertTestSuiteParent.Free;
|
|
FUpdateTestResult.Free;
|
|
FUpdateTestResultException.Free;
|
|
FUpdateTestResultExceptionError.Free;
|
|
FUpdateTestResultOK.Free;
|
|
FTran.Free;
|
|
FConn.Close;
|
|
FConn.Free;
|
|
except
|
|
on E: Exception do begin
|
|
// We need to swallow the exception because we're closing down.
|
|
// Only tell the user something is up in debug mode:
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('TestDBWriter: ignored problem in destructor: '+E.ClassName+'/'+E.Message);
|
|
{$ENDIF DEBUGCONSOLE}
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
FRootSuite.Free;
|
|
FTestSuitePath.Free;
|
|
|
|
inherited Destroy;
|
|
end;
|
|
|
|
|
|
procedure TDBResultsWriter.AddFailure(ATest: TTest; AFailure: TTestFailure);
|
|
var
|
|
MessageID: integer;
|
|
ResultID: integer;
|
|
begin
|
|
assert(FTestResultID<>TDBW_INVALID_DB_ID,'AddFailure: test id must be assigned.');
|
|
FTestResultOK:=false;
|
|
MessageID:=TDBW_INVALID_DB_ID;
|
|
if (AFailure.ExceptionClassName<>'') or (AFailure.ExceptionMessage<>'') then
|
|
MessageID:=DBGetExceptionMessageID(AFailure.ExceptionClassName,AFailure.ExceptionMessage);
|
|
if AFailure.IsIgnoredTest then
|
|
begin
|
|
// Ignored
|
|
ResultID:=DBGetResultValueID(RESULT_IGNORED);
|
|
FUpdateTestResultException.Params[0].AsInteger:=ResultID;
|
|
FUpdateTestResultException.Params[1].AsInteger:=MessageID;
|
|
FUpdateTestResultException.Params[2].AsInteger:=FTestResultID;
|
|
FUpdateTestResultException.ExecSQL;
|
|
end
|
|
else
|
|
begin
|
|
// Really failed
|
|
ResultID:=DBGetResultValueID(RESULT_FAILED);
|
|
FUpdateTestResultException.Params[0].AsInteger:=ResultID;
|
|
if MessageID=TDBW_INVALID_DB_ID then
|
|
FUpdateTestResultException.Params[1].Clear //null
|
|
else
|
|
FUpdateTestResultException.Params[1].AsInteger:=MessageID;
|
|
FUpdateTestResultException.Params[2].AsInteger:=FTestResultID;
|
|
FUpdateTestResultException.ExecSQL;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TDBResultsWriter.AddError(ATest: TTest; AError: TTestFailure);
|
|
var
|
|
MethodNameID: integer;
|
|
MessageID: integer;
|
|
ResultID: integer;
|
|
SourceLocationID: integer;
|
|
begin
|
|
assert(FTestResultID<>TDBW_INVALID_DB_ID,'AddError: test id must be assigned.');
|
|
FTestResultOK:=false;
|
|
MessageID:=TDBW_INVALID_DB_ID;
|
|
MethodNameID:=TDBW_INVALID_DB_ID;
|
|
SourceLocationID:=TDBW_INVALID_DB_ID;
|
|
|
|
if (AError.ExceptionClassName<>'') or (AError.ExceptionMessage<>'') then
|
|
MessageID:=DBGetExceptionMessageID(AError.ExceptionClassName,AError.ExceptionMessage);
|
|
if (AError.SourceUnitName<>'') and (AError.LineNumber<>0) then
|
|
SourceLocationID:=DBGetSourceLocationID(AError.SourceUnitName,AError.LineNumber);
|
|
if AError.FailedMethodName<>'' then
|
|
MethodNameID:=DBGetMethodNameID(AError.FailedMethodName);
|
|
|
|
ResultID:=DBGetResultValueID(RESULT_ERROR);
|
|
FUpdateTestResultExceptionError.Params[0].AsInteger:=ResultID;
|
|
if MessageID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FUpdateTestResultExceptionError.Params[1].Clear;
|
|
FUpdateTestResultExceptionError.Params[1].DataType:=ftInteger;
|
|
end
|
|
else
|
|
FUpdateTestResultExceptionError.Params[1].AsInteger:=MessageID;
|
|
if MethodNameID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FUpdateTestResultExceptionError.Params[2].Clear;
|
|
FUpdateTestResultExceptionError.Params[2].DataType:=ftInteger;
|
|
end
|
|
else
|
|
FUpdateTestResultExceptionError.Params[2].AsInteger:=MethodNameID;
|
|
if SourceLocationID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
FUpdateTestResultExceptionError.Params[3].Clear;
|
|
FUpdateTestResultExceptionError.Params[3].DataType:=ftInteger;
|
|
end
|
|
else
|
|
FUpdateTestResultExceptionError.Params[3].AsInteger:=SourceLocationID;
|
|
FUpdateTestResultExceptionError.Params[4].AsInteger:=FTestResultID;
|
|
FUpdateTestResultExceptionError.ExecSQL;
|
|
end;
|
|
|
|
|
|
procedure TDBResultsWriter.StartTest(ATest: TTest);
|
|
var
|
|
RandomID: string;
|
|
TestID: integer;
|
|
begin
|
|
// Empty etc test suite names may lead to invalid FTestSuiteIDs which should be ignored
|
|
if FTestSuiteID<>TDBW_INVALID_DB_ID then
|
|
begin
|
|
{$IFDEF DEBUGCONSOLE}
|
|
// Assertions won't stop executions but apparently restart the test?
|
|
if FTestRunID=TDBW_INVALID_DB_ID then
|
|
begin
|
|
writeln('Halt: Valid FTestRunID must be retrieved from db before using.');
|
|
halt(16);
|
|
end;
|
|
{$ENDIF}
|
|
assert(FTestRunID<>TDBW_INVALID_DB_ID,'Valid testrun ID must be retrieved from db before using.');
|
|
FTestResultOK:=true; //Keep track of whether test succeeded or not
|
|
TestID:=TDBW_INVALID_DB_ID; //Notify db code we need to get a new one
|
|
TestID:=DBGetTestID(ATest.TestName);
|
|
assert(TestID<>TDBW_INVALID_DB_ID,'Valid test ID must be retrieved from db before using.');
|
|
// If doing e.g. performance tests, the same test name may be run multiple
|
|
// times per run, so keep track of what we inserted:
|
|
RandomID:=GetUniqueID;
|
|
|
|
try
|
|
FInsertTestResult.Params[0].AsInteger:=FTestRunID;
|
|
FInsertTestResult.Params[1].AsInteger:=TestID;
|
|
FInsertTestResult.Params[2].AsString:=RandomID;
|
|
FInsertTestResult.ExecSQL;
|
|
|
|
FGetTestResultID.Params[0].AsInteger:=FTestRunID;
|
|
FGetTestResultID.Params[1].AsInteger:=TestID;
|
|
FGetTestResultID.Params[2].AsString:=RandomID;
|
|
FGetTestResultID.Open;
|
|
if FGetTestResultID.RecordCount>0 then
|
|
FTestResultID:=FGetTestResultID.Fields[0].AsInteger;
|
|
FGetTestResultID.Close;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('testdbwriter: error inserting test start data.');
|
|
writeln('Exception: '+E.ClassName+'/'+E.Message);
|
|
writeln('Aborting.');
|
|
{$ENDIF}
|
|
halt(15);
|
|
end;
|
|
end;
|
|
|
|
FStartTest:=Now();
|
|
FStartTest:=LocalTimeToUniversal(FStartTest);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TDBResultsWriter.EndTest(ATest: TTest);
|
|
var
|
|
ElapsedTime: TDateTime;
|
|
EndTime: TDateTime;
|
|
ResultID: integer;
|
|
begin
|
|
EndTime:=Now();
|
|
EndTime:=LocalTimeToUniversal(EndTime);
|
|
ElapsedTime:=EndTime-FStartTest;
|
|
|
|
// Update test record depending on result
|
|
Assert(FTestResultID<>TDBW_INVALID_DB_ID,'We must have a valid test ID from the StartTest code.');
|
|
if FTestResultOK then
|
|
begin
|
|
ResultID:=DBGetResultValueID(RESULT_OK);
|
|
Assert(ResultID<>TDBW_INVALID_DB_ID,'Result ID for OK must be available.');
|
|
FUpdateTestResultOK.Params[0].AsDateTime:=ElapsedTime;
|
|
FUpdateTestResultOK.Params[1].AsInteger:=ResultID;
|
|
FUpdateTestResultOK.Params[2].Clear; //we used this as temp ID
|
|
FUpdateTestResultOK.Params[2].DataType:=ftString;
|
|
FUpdateTestResultOK.Params[3].AsInteger:=FTestResultID;
|
|
FUpdateTestResultOK.ExecSQL;
|
|
end
|
|
else
|
|
begin
|
|
FUpdateTestResult.Params[0].AsDateTime:=ElapsedTime;
|
|
FUpdateTestResult.Params[1].Clear; //we used this as temp ID
|
|
FUpdateTestResult.Params[1].DataType:=ftString;
|
|
FUpdateTestResult.Params[2].AsInteger:=FTestResultID;
|
|
FUpdateTestResult.ExecSQL;
|
|
end;
|
|
FTestResultID:=TDBW_INVALID_DB_ID; //Notify db code we need to add a new one
|
|
end;
|
|
|
|
procedure TDBResultsWriter.StartTestSuite(ATestSuite: TTestSuite);
|
|
begin
|
|
if not FDBSetup then
|
|
begin
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('*** debug: dbsetup begin');
|
|
{$ENDIF DEBUGCONSOLE}
|
|
// FDBSetup indicates first time a test suite is entered. Not only set up
|
|
// db, but also insert test run information:
|
|
|
|
// If something went wrong here we should just quite; it makes no sense
|
|
// to run test results without getting them into the db.
|
|
// Apparently the test suite ignores exceptions, so explicit testing here
|
|
try
|
|
// Wrong username/password etc will be picked up here:
|
|
DatabaseSetup;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('testdbwriter: error setting up db connection.');
|
|
writeln('(Are your connection details correct?)');
|
|
writeln('Exception: '+E.ClassName+'/'+E.Message);
|
|
writeln('Aborting.');
|
|
{$ENDIF}
|
|
halt(13);
|
|
end;
|
|
end;
|
|
try
|
|
// This is the first time we try to insert data, so permissions errors,
|
|
// missing table problems etc can occur here:
|
|
InsertTestRun;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('testdbwriter: error inserting test run info.');
|
|
writeln('(Does the db have the right tables and are permissions OK?)');
|
|
writeln('Exception: '+E.ClassName+'/'+E.Message);
|
|
writeln('Aborting.');
|
|
{$ENDIF}
|
|
halt(14);
|
|
end;
|
|
end;
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('*** debug: dbsetup end');
|
|
{$ENDIF}
|
|
end;
|
|
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('*** debug: test suite begin');
|
|
{$ENDIF}
|
|
//todo: debug
|
|
// Only honor non-empty test suite names. This means that
|
|
// TDBW_INVALID_DB_ID may be passed on to other procedures
|
|
// where it should be ignored.
|
|
FTestSuiteID:=TDBW_INVALID_DB_ID;
|
|
if ATestSuite.TestName<>'' then
|
|
begin
|
|
// Retrieve test suite based on parent - if any:
|
|
if (FTestSuitePath.Count>0) then
|
|
FTestSuiteID:=ptrint(FTestSuitePath.Objects[FTestSuitePath.Count-1]);
|
|
if FTestSuiteID=TDBW_INVALID_DB_ID then
|
|
FTestSuiteID:=DBGetTestSuiteID(ATestSuite.TestName)
|
|
else
|
|
FTestSuiteID:=DBGetTestSuiteID(ATestSuite.TestName,FTestSuitePath.Count,FTestSuiteID); //Depth is 0 based
|
|
assert(FTestSuiteID<>TDBW_INVALID_DB_ID,'StartTestSuite: FTestSuiteID for testsuite '+ATestSuite.TestName+' must not be invalid.');
|
|
FTestSuitePath.AddObject(ATestSuite.TestName,TObject(ptrint(FTestSuiteID)));
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('StartTestSuite: Added test suite'+ATestSuite.TestName+' with id '+inttostr(FTestSuiteID)+' to FTestSuitePath, now count: '+inttostr(FTestSuitePath.Count));
|
|
{$ENDIF}
|
|
if (FTestSuiteID=TDBW_INVALID_DB_ID) then
|
|
begin
|
|
writeln('Halt: StartTestSuite: FTestSuiteID for testsuite '+ATestSuite.TestName+' must not be invalid.');
|
|
halt(18);
|
|
end;
|
|
end;
|
|
{$IFDEF DEBUGCONSOLE}
|
|
writeln('*** debug: test suite end');
|
|
writeln('');
|
|
{$ENDIF}
|
|
end;
|
|
|
|
|
|
procedure TDBResultsWriter.EndTestSuite(ATestSuite: TTestSuite);
|
|
begin
|
|
// Skip back up one item in the stack:
|
|
if FTestSuitePath.Count>0 then
|
|
FTestSuitePath.Delete(FTestSuitePath.Count-1);
|
|
FTestSuiteID:=TDBW_INVALID_DB_ID;
|
|
end;
|
|
|
|
|
|
end.
|
|
|