{ This unit contains the TTestRunner class, a base class for the console test runner for fpcunit. This file is part of the Free Component Library (FCL) Copyright (C) 2006 Vincent Snijders Port to Pas2JS by Mattias Gaertner in 2017. 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. 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. **********************************************************************} unit ConsoleTestRunner; {$mode objfpc} interface uses {$IFDEF NODEJS} NodeJSApp, {$else} BrowserApp, {$endif} Classes, SysUtils, FPCUnit, TestRegistry, TestDecorator, //testutils, FPCUnitReport, //latextestreport, //xmltestreport, PlainTestReport //dom ; const Version = '0.3'; type TFormat = ( fPlain, //fLatex, //fXML, fPlainNoTiming ); var DefaultFormat : TFormat = fPlain; // fXML; DefaultRunAllTests : Boolean = False; type { TTestRunner } { TRunForm } TTestRunner = class({$IFDEF NODEJS}TNodeJSApplication{$ELSE}TBrowserApplication {$ENDIF}) private FRunFormClass: TRunFormClass; FLastTest : TTest; FShowProgress: boolean; FFileName: string; FStyleSheet: string; FLongOpts: TStrings; FFormatParam: TFormat; procedure DoRunAgain(Sender: TObject); protected property FileName: string read FFileName write FFileName; property LongOpts: TStrings read FLongOpts write FLongOpts; property ShowProgress: boolean read FShowProgress write FShowProgress; property StyleSheet: string read FStyleSheet write FStyleSheet; property FormatParam: TFormat read FFormatParam write FFormatParam; procedure DoRun; override; procedure DoTestRun(ATest: TTest); virtual; function GetShortOpts: string; virtual; procedure AppendLongOpts; virtual; procedure WriteCustomHelp; virtual; procedure ParseOptions; virtual; //procedure ExtendXmlDocument(Doc: TXMLDocument); virtual; function GetResultsWriter: TCustomResultsWriter; virtual; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; Property RunFormClass : TRunFormClass Read FRunFormClass Write FRunFormClass; end; implementation const ShortOpts = 'alhp'; DefaultLongOpts: array of string = ('all', 'list', 'progress', 'help', 'skiptiming', 'suite:', 'format:', 'file:', 'stylesheet:','sparse','no-addresses'); type { TDecoratorTestSuite } TDecoratorTestSuite = Class(TTestSuite) public Procedure FreeDecorators(T : TTest); Destructor Destroy; override; end; procedure TDecoratorTestSuite.FreeDecorators(T: TTest); Var I : Integer; begin If (T is TTestSuite) then for I:=0 to TTestSuite(t).ChildTestCount-1 do FreeDecorators(TTest(TTestSuite(t).Test[i])); if (T is TTestDecorator) and (TTestDecorator(T).Test is TDecoratorTestSuite) then T.Destroy; end; destructor TDecoratorTestSuite.Destroy; begin FreeDecorators(Self); // We need to find something for this. ClearTests; inherited Destroy; end; type { TProgressWriter } TProgressWriter = class({TNoRefCountObject, }ITestListener) private FSuccess: boolean; procedure WriteChar(c: char); public destructor Destroy; override; { ITestListener interface requirements } procedure AddFailure(ATest: TTest; AFailure: TTestFailure); override; procedure AddError(ATest: TTest; AError: TTestFailure); override; procedure StartTest(ATest: TTest); override; procedure EndTest(ATest: TTest); override; procedure StartTestSuite(ATestSuite: TTestSuite); override; procedure EndTestSuite(ATestSuite: TTestSuite); override; end; procedure TProgressWriter.WriteChar(c: char); begin write(c); // flush output, so that we see the char immediately, even it is written to file //Flush(output); end; destructor TProgressWriter.Destroy; begin // on destruction, just write the missing line ending writeln; inherited Destroy; end; procedure TProgressWriter.AddFailure(ATest: TTest; AFailure: TTestFailure); begin FSuccess := false; writechar('F'); if ATest=nil then; if AFailure=nil then; end; procedure TProgressWriter.AddError(ATest: TTest; AError: TTestFailure); begin FSuccess := false; writechar('E'); if ATest=nil then; if AError=nil then ; end; procedure TProgressWriter.StartTest(ATest: TTest); begin FSuccess := true; // assume success, until proven otherwise if ATest=nil then; end; procedure TProgressWriter.EndTest(ATest: TTest); begin if FSuccess then writechar('.'); if ATest=nil then ; end; procedure TProgressWriter.StartTestSuite(ATestSuite: TTestSuite); begin // do nothing if ATestSuite=nil then; end; procedure TProgressWriter.EndTestSuite(ATestSuite: TTestSuite); begin // do nothing if ATestSuite=nil then; end; { TTestRunner } procedure TTestRunner.DoRun; var I,P : integer; S,TN : string; TS : TDecoratorTestSuite; T : TTest; R : TRunForm; begin S := CheckOptions(GetShortOpts, LongOpts); if (S <> '') then Writeln(S); ParseOptions; //get a list of all registed tests if HasOption('l', 'list') then case FormatParam of //fLatex: Write(GetSuiteAsLatex(GetTestRegistry)); fPlain: Write(GetSuiteAsPlain(GetTestRegistry)); fPlainNoTiming: Write(GetSuiteAsPlain(GetTestRegistry)); else //Write(GetSuiteAsXml(GetTestRegistry)); Write(GetSuiteAsPlain(GetTestRegistry)); end; If Assigned(RunFormClass) then begin R:=RunFormClass.Create(Self); R.OnRun:=@DoRunAgain; R.Initialize; end; //run the tests if HasOption('suite') then begin S := ''; S := GetOptionValue('suite'); if S = '' then for I := 0 to GetTestRegistry.ChildTestCount - 1 do writeln(GetTestRegistry[i].TestName) else begin TS:=TDecoratorTestSuite.Create('SuiteList'); try while Not(S = '') Do begin P:=Pos(',',S); If P=0 then P:=Length(S)+1; TN:=Copy(S,1,P-1); Delete(S,1,P); if (TN<>'') then begin T:=GetTestRegistry.FindTest(TN); if Assigned(T) then TS.AddTest(T); end; end; if (TS.CountTestCases>1) then DoTestRun(TS) else if TS.CountTestCases=1 then DoTestRun(TS[0]) else Writeln('No tests selected.'); finally FreeAndNil(TS); end; end; end else if HasOption('a', 'all') or (DefaultRunAllTests and Not HasOption('l','list')) then DoTestRun(GetTestRegistry) ; Terminate; end; procedure TTestRunner.DoTestRun(ATest: TTest); var ResultsWriter: TCustomResultsWriter; ProgressWriter: TProgressWriter; TestResult: TTestResult; begin FLastTest:=aTest; ResultsWriter := GetResultsWriter; ResultsWriter.Filename := FileName; TestResult := TTestResult.Create; ProgressWriter:=nil; try if ShowProgress then begin ProgressWriter := TProgressWriter.Create; TestResult.AddListener(ProgressWriter); end else ProgressWriter := nil; TestResult.AddListener(ResultsWriter.TestListener); ATest.Run(TestResult); ResultsWriter.WriteResult(TestResult); finally FreeAndNil(TestResult); FreeAndNil(ResultsWriter); FreeAndNil(ProgressWriter); end; end; function TTestRunner.GetShortOpts: string; begin Result := ShortOpts; end; procedure TTestRunner.AppendLongOpts; var i: Integer; begin for i := low(DefaultLongOpts) to Length(DefaultLongOpts)-1 do LongOpts.Add(DefaultLongOpts[i]); end; procedure TTestRunner.WriteCustomHelp; begin // no custom help options in base class end; procedure TTestRunner.ParseOptions; begin if HasOption('h', 'help') or ((ParamCount = 0) and not DefaultRunAllTests) then begin writeln(Title); writeln(Version); writeln; writeln('Usage: '); writeln(' --format=latex output as latex source (only list implemented)'); writeln(' --format=plain output as plain ASCII source'); writeln(' --format=xml output as XML source (default)'); writeln(' --skiptiming Do not output timings (useful for diffs of testruns)'); writeln(' --sparse Produce Less output (errors/failures only)'); writeln(' --no-addresses Do not display address info'); writeln(' --stylesheet= add stylesheet reference'); writeln(' --file= output results to file'); writeln; writeln(' -l or --list show a list of registered tests'); writeln(' -a or --all run all tests'); writeln(' -p or --progress show progress'); writeln(' --suite=MyTestSuiteName run single test suite class'); WriteCustomHelp; writeln; writeln('The results can be redirected to an xml file,'); writeln('for example: ', ParamStr(0),' --all > results.xml'); end; //get the format parameter FormatParam := DefaultFormat; if HasOption('format') then begin //if CompareText(GetOptionValue('format'),'latex')=0 then // FormatParam := fLatex if CompareText(GetOptionValue('format'),'plain')=0 then FormatParam := fPlain else if CompareText(GetOptionValue('format'),'plainnotiming')=0 then FormatParam := fPlainNoTiming; //else if CompareText(GetOptionValue('format'),'xml')=0 then // FormatParam := fXML; end; ShowProgress := HasOption('p', 'progress'); if HasOption('file') then FileName := GetOptionValue('file'); if HasOption('stylesheet') then StyleSheet := GetOptionValue('stylesheet'); end; function TTestRunner.GetResultsWriter: TCustomResultsWriter; begin case FormatParam of //fLatex: Result := TLatexResultsWriter.Create(nil); fPlain: Result := TPlainResultsWriter.Create(nil); else begin Result := TPlainResultsWriter.Create(nil); //Result := TXmlResultsWriter.Create(nil); //ExtendXmlDocument(TXMLResultsWriter(Result).Document); end; end; Result.SkipTiming:=HasOption('skiptiming'); Result.Sparse:=HasOption('sparse'); Result.SkipAddressInfo:=HasOption('no-addresses'); end; constructor TTestRunner.Create(AOwner: TComponent); begin inherited Create(AOwner); FLongOpts := TStringList.Create; AppendLongOpts; end; destructor TTestRunner.Destroy; begin FreeAndNil(FLongOpts); inherited Destroy; end; procedure TTestRunner.DoRunAgain(Sender : TObject); begin if Assigned(FLastTest) then DoTestRun(FLastTest); end; initialization DefaultFormat:=fplain; DefaultRunAllTests:=True; end.