fpc/packages/fcl-xml/tests/xmlts.pp
sergei c7259969ce + fcl-xml, implemented TDOMNode.BaseURI property.
* Moved element loading procedure from xmlread.pp to dom.pp, speeds things up a bit.

git-svn-id: trunk@20558 -
2012-03-21 22:19:27 +00:00

825 lines
22 KiB
ObjectPascal

{
This file is part of the Free Component Library (FCL)
FCL test runner for OASIS/NIST XML test suite
It is somewhat based on 'harness.js' script
(see http://xmlconf.sourceforge.net)
Copyright (c) 2006 by Sergei Gorelkin, sergei_gorelkin@mail.ru
See the file COPYING.FPC, included in this distribution,
for details about the copyright.
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.
**********************************************************************}
program xmlts;
{$IFDEF FPC}
{$MODE OBJFPC}{$H+}
{$ENDIF}
{$APPTYPE CONSOLE}
uses
SysUtils,
Classes,
DOM,
XMLRead,
XMLWrite,
UriParser;
const
harness = 'Pascal version';
version = '0.0.1 alpha :)';
parser = 'FCL XML parser';
parserName = parser;
os = 'Unknown OS';
runtime = 'FPC RTL';
{ Defines which tests to skip (sets for editions 1-4 and edition 5 are mutually exclusive) }
FifthEditionCompliant = True;
type
TDiagCategory = (dcInfo, dcNegfail, dcFail, dcPass);
TTestSuite = class
private
FTemplate: TXMLDocument;
FParser: TDOMParser;
FPassed, FFailCount: Integer;
FFalsePasses: Integer;
FRootUri: string;
FSuiteName: string;
FDoc: TXMLDocument;
FValidating: Boolean;
FSuiteTitle: DOMString;
FState: DOMString;
FSkipped: Integer;
FTotal: Integer;
table_valid: TDOMNode;
table_output: TDOMNode;
table_invalid: TDOMNode;
table_not_wf: TDOMNode;
table_informative: TDOMNode;
FValError: string;
FTestID: DOMString;
FErrLine, FErrCol: Integer;
procedure LoadTemplate(const Name: string);
procedure HandleTemplatePIs(Element: TDOMNode);
procedure Diagnose(Element, Table: TDOMNode; Category: TDiagCategory; const Error: DOMString);
procedure DiagnoseOut(const ErrorMsg: DOMString);
function CompareNodes(actual, correct: TDOMNode; out Msg: string): Boolean;
procedure ErrorHandler(Error: EXMLReadError);
public
constructor Create;
procedure Run(const Tests: string);
procedure RunTest(Element: TDOMElement);
destructor Destroy; override;
end;
{ obsolete, now TDOMNode.BaseURI does the job }
function GetBaseURI(Element: TDOMNode; const DocumentURI: string): string;
var
Ent: TDOMNode;
Uri1, Uri2, s: WideString;
begin
case Element.NodeType of
ELEMENT_NODE, TEXT_NODE, CDATA_SECTION_NODE,
PROCESSING_INSTRUCTION_NODE, COMMENT_NODE, DOCUMENT_TYPE_NODE:
if Assigned(Element.ParentNode)
then Result := GetBaseURI(Element.ParentNode, DocumentURI)
else Result := '';
ATTRIBUTE_NODE: begin
Result := '';
if Assigned(TDomAttr(Element).OwnerElement) then
begin
Result := GetBaseURI(TDomAttr(Element).OwnerElement, DocumentURI);
end;
end;
ENTITY_REFERENCE_NODE: begin
Ent := Element.OwnerDocument.DocType.Entities.GetNamedItem(Element.NodeName);
if Assigned(Ent) and (TDOMEntity(Ent).SystemID <> '') then
begin
Uri1 := TDOMEntity(Ent).SystemID;
if IsAbsoluteURI(Uri1) then
begin
Result := Uri1;
end else begin
Uri2 := GetBaseURI(Element.ParentNode, DocumentUri);
ResolveRelativeUri(Uri2, Uri1, s);
Result := s;
end;
end
else
begin
if Assigned(Element.ParentNode)
then Result := GetBaseURI(Element.ParentNode, DocumentURI)
else Result := '';
end;
end;
DOCUMENT_NODE: Result := DocumentURI;
else
Result := '';
end;
end;
{ TTestSuite }
constructor TTestSuite.Create;
begin
inherited Create;
FParser := TDOMParser.Create;
FParser.Options.PreserveWhitespace := True;
FParser.Options.ExpandEntities := True;
FParser.Options.IgnoreComments := True;
FParser.Options.CDSectionsAsText := True;
end;
procedure TTestSuite.ErrorHandler(Error: EXMLReadError);
begin
// allow fatal error position to override that of validation error
if (FErrLine < 0) or (Error.Severity = esFatal) then
begin
FErrLine := Error.Line;
FErrCol := Error.LinePos;
end;
if Error.Severity = esError then
begin
if FValError = '' then // fetch the _first_ message
FValError := Error.Message;
{ uncomment the line below to verify that the suite correctly handles
exception raised from the handler }
// Abort;
end;
end;
procedure TTestSuite.LoadTemplate(const Name: string);
var
tables: TDOMNodeList;
I: Integer;
id: DOMString;
el: TDOMElement;
begin
ReadXMLFile(FTemplate, Name);
tables := FTemplate.DocumentElement.GetElementsByTagName('table');
try
for I := 0 to tables.Count-1 do
begin
el := TDOMElement(tables[I]);
id := el['id'];
if id = 'valid' then
table_valid := el
else if ((id = 'invalid-negative') and FValidating) or ((id = 'invalid-positive') and not FValidating) then
table_invalid := el
else if id = 'valid-output' then
table_output := el
else if id = 'not-wf' then
table_not_wf := el
else if id = 'error' then
table_informative := el;
end;
finally
tables.Free;
end;
end;
destructor TTestSuite.Destroy;
begin
FDoc.Free;
FTemplate.Free;
FParser.Free;
inherited;
end;
procedure TTestSuite.HandleTemplatePIs(Element: TDOMNode);
var
Children: TDOMNodeList;
Child: TDOMNode;
NewChild: TDOMNode;
Remove: Boolean;
Index: Integer;
Data: DOMString;
begin
Children := element.childNodes;
Remove := False;
Index := 0;
repeat
Child := Children[Index];
if Child = nil then Break;
Inc(index);
// inside a rejected <?if ...?>...<?endif?>
if Remove and (child.nodeType <> PROCESSING_INSTRUCTION_NODE) then
begin
Element.removeChild(child);
Dec(Index);
Continue;
end;
if Child.hasChildNodes then
begin
HandleTemplatePIs(Child);
Continue;
end;
if Child.nodeType <> PROCESSING_INSTRUCTION_NODE then
Continue;
Data := Child.NodeValue;
if Child.NodeName = 'run-id' then
begin
newChild := nil;
if Data = 'name' then
newChild := FTemplate.createTextNode(parser)
else if Data = 'description' then
newChild := FTemplate.createTextNode (parserName)
else if Data = 'general-entities' then
newChild := FTemplate.createTextNode('included')
else if Data = 'parameter-entities' then
newChild := FTemplate.createTextNode ('included')
else if Data = 'type' then
begin
if FValidating then
Data := 'Validating'
else
Data := 'Non-Validating';
newChild := FTemplate.createTextNode(Data);
end
// ... test run description
else if Data = 'date' then
newChild := FTemplate.createTextNode(DateTimeToStr(Now))
else if Data = 'harness' then
newChild := FTemplate.createTextNode(harness)
else if Data = 'java' then
newChild := FTemplate.createTextNode(runtime)
else if Data = 'os' then
newChild := FTemplate.createTextNode(os)
else if Data = 'testsuite' then
newChild := FTemplate.createTextNode(FSuiteTitle)
else if Data = 'version' then
newChild := FTemplate.createTextNode(version)
// ... test result info
else if Data = 'failed' then
newChild := FTemplate.createTextNode(IntToStr(FFailCount))
else if Data = 'passed' then
newChild := FTemplate.createTextNode(IntToStr(FPassed))
else if Data = 'passed-negative' then
newChild := FTemplate.createTextNode(IntToStr(FFalsePasses))
else if Data = 'skipped' then
newChild := FTemplate.createTextNode(IntToStr(FSkipped))
else if Data = 'status' then
newChild := FTemplate.createTextNode (FState);
Element.replaceChild (newChild, child);
Continue;
end
// if/endif don't nest, and always have the same parent
// we rely on those facts here!
else if Child.NodeName = 'if' then
begin
Remove := not (((Data = 'validating') and FValidating) or
((Data = 'nonvalidating') and not FValidating));
element.removeChild(child);
Dec(Index);
Continue;
end
else if Child.NodeName = 'endif' then
begin
Remove := False;
element.removeChild(child);
Dec(Index);
Continue;
end;
until False;
Children.Free;
end;
procedure TTestSuite.Run(const Tests: string);
var
Cases: TDOMNodeList;
I: Integer;
begin
FRootURI := FilenameToURI(Tests);
writeln('Loading test suite from ', Tests);
ReadXMLFile(FDoc, Tests);
FSuiteTitle := FDoc.DocumentElement['PROFILE'];
Cases := FDoc.DocumentElement.GetElementsByTagName('TEST');
writeln;
writeln('Testing, validation = ', FValidating);
try
for I := 0 to Cases.Count-1 do
RunTest(Cases[I] as TDOMElement);
I := Cases.Count;
finally
Cases.Free;
end;
FPassed := FTotal-FFailCount;
Dec(FPassed, FSkipped);
writeln('Found ', I, ' basic test cases.');
writeln('Found ', FTotal, ' overall test cases.');
writeln('Skipped: ', FSkipped);
writeln('Passed: ', FPassed);
writeln('Failed: ', FFailCount);
writeln('Negative passes: ', FFalsePasses, ' (need examination).');
writeln;
if FPassed = 0 then
FState := 'N/A'
else if FPassed = FTotal - FSkipped then
FState := 'CONFORMS (provisionally)'
else
FState := 'DOES NOT CONFORM';
end;
procedure TTestSuite.RunTest(Element: TDOMElement);
var
s: string;
TestType: DOMString;
TempDoc, RefDoc: TXMLDocument;
table: TDOMNode;
Positive: Boolean;
outURI: string;
FailMsg: string;
ExceptionClass: TClass;
docNode, refNode: TDOMNode;
docMap, refMap: TDOMNamedNodeMap;
docN, refN: TDOMNotation;
I: Integer;
root: string;
xmlEdition: DOMString;
begin
FErrLine := -1;
FErrCol := -1;
FTestID := Element['ID'];
TestType := Element['TYPE'];
xmlEdition := Element['EDITION'];
if (xmlEdition <> '') and ((Pos(WideChar('5'), Element['EDITION']) = 0) = FifthEditionCompliant) then
begin
Inc(FSkipped);
Exit;
end;
root := Element.BaseURI;
ResolveRelativeURI(root, UTF8Encode(Element['URI']), s);
table := nil;
outURI := '';
Positive := False;
if TestType = 'not-wf' then
table := table_not_wf
else if TestType = 'error' then
table := table_informative
else if TestType = 'valid' then
begin
if Element.hasAttribute('OUTPUT') then
ResolveRelativeURI(root, UTF8Encode(Element['OUTPUT']), outURI);
table := table_valid;
Positive := True;
end
else if TestType = 'invalid' then
begin
table := table_invalid;
Positive := not FValidating;
end;
if TestType <> 'error' then
begin
Inc(FTotal);
if outURI <> '' then Inc(FTotal);
end;
FailMsg := '';
FValError := '';
TempDoc := nil;
try
try
FParser.Options.Validate := FValidating;
FParser.Options.Namespaces := (Element['NAMESPACE'] <> 'no');
FParser.OnError := {$IFDEF FPC}@{$ENDIF}ErrorHandler;
FParser.ParseUri(s, TempDoc);
except
on E: Exception do
if E.ClassType <> EAbort then
begin
ExceptionClass := E.ClassType;
FailMsg := E.Message;
FValError := '';
end;
end;
if table = table_informative then
begin
if FailMsg <> '' then
Diagnose(element, table, dcInfo, '(fatal) ' + FailMsg)
else if FValError <> '' then
Diagnose(element, table, dcInfo, '(error) ' + FValError)
else
Diagnose(Element, table, dcInfo, '');
Exit;
end;
if not Positive then // must have been failed
begin
if (FailMsg = '') and (FValError = '') then
begin
Inc(FFailCount);
Diagnose(element, table, dcNegfail, '');
end
else // FailMsg <> '' or FValError <> '' -> actually failed
begin
if FailMsg <> '' then // Fatal error
begin
{ outside not-wf category it is a test failure }
if (table <> table_not_wf) or (ExceptionClass <> EXMLReadError) then
begin
Inc(FFailCount);
Diagnose(Element, table, dcFail, FailMsg);
end
else
begin
Inc(FFalsePasses);
Diagnose(Element, table, dcPass, FailMsg);
end;
end
else
begin
{ outside invalid category it is a test failure }
if table = table_not_wf then
begin
Inc(FFailCount);
Diagnose(Element, table, dcFail, FValError);
end
else
begin
Inc(FFalsePasses);
Diagnose(Element, table, dcPass, FValError);
end;
end;
end;
Exit;
end
else // must have been succeeded
if (FailMsg <> '') or (FValError <> '') then
begin
Inc(FFailCount);
if FailMsg <> '' then
Diagnose(Element, table, dcFail, FailMsg)
else
Diagnose(Element, table, dcFail, FValError);
if (outURI <> '') and (FailMsg <> '') then
begin
Inc(FFailCount);
DiagnoseOut('[ input failed, no output to test ]');
end;
Exit;
end;
if outURI = '' then Exit;
try
// reference data must be parsed in non-validating mode because it contains DTDs
// only when Notations need to be reported
FParser.Options.Validate := False;
FParser.ParseUri(outURI, RefDoc);
try
docNode := TempDoc.FirstChild;
refNode := RefDoc.FirstChild;
repeat
if refNode = nil then
begin
if docNode <> nil then
begin
Inc(FFailCount);
DiagnoseOut('Extra data: ' + docNode.NodeName + ' / ' + docNode.NodeValue);
end;
Exit;
end;
if docNode = nil then
begin
Inc(FFailCount);
DiagnoseOut('Missing data: ' + refNode.NodeName + ' / ' + refNode.NodeValue);
Exit;
end;
if refNode.NodeType = DOCUMENT_TYPE_NODE then
begin
if docNode.NodeType <> DOCUMENT_TYPE_NODE then
begin
Inc(FFailCount);
DiagnoseOut('[ no doctype from parsing testcase ]');
Exit;
end;
refMap := TDOMDocumentType(refNode).Notations;
docMap := TDOMDocumentType(docNode).Notations;
for I := 0 to refMap.Length-1 do
begin
refN := TDOMNotation(refMap[I]);
docN := TDOMNotation(docMap.GetNamedItem(refMap[I].NodeName));
if not Assigned(docN) then
begin
Inc(FFailCount);
DiagnoseOut('missing notation declaration: ' + refN.NodeName);
Exit;
end;
if (refN.PublicID <> docN.PublicID) or (refN.SystemID <> docN.SystemID) then
begin
Inc(FFailCount);
DiagnoseOut('incorrect notation declaration: ' + refN.NodeName);
Exit;
end;
end;
refNode := refNode.NextSibling;
docNode := docNode.NextSibling;
Continue;
end;
if docNode.NodeType = DOCUMENT_TYPE_NODE then // skip DocType
docNode := docNode.NextSibling;
if not CompareNodes(docNode, refNode, FailMsg) then
begin
Inc(FFailCount);
DiagnoseOut(FailMsg);
Exit;
end;
docNode := docNode.NextSibling;
refNode := refNode.NextSibling;
until False;
finally
RefDoc.Free;
end;
except
on E: Exception do
begin
Inc(FFailCount);
DiagnoseOut('[ can''t read reference data: '+E.Message+' ]');
end;
end;
finally
TempDoc.Free;
end;
end;
procedure TTestSuite.Diagnose(Element, Table: TDOMNode; Category: TDiagCategory;
const Error: DOMString);
var
tr, td, txt, tmp: TDOMNode;
s: DOMString;
begin
tr := FTemplate.CreateElement('tr');
if Assigned(Element) then // column 1: section/chapter, if known
begin
s := TDOMElement(Element)['SECTIONS'];
td := FTemplate.CreateElement('td');
td.AppendChild(FTemplate.CreateTextNode(s));
tr.AppendChild(td);
end;
td := FTemplate.CreateElement('td'); // column 2: test ID
td.AppendChild(FTemplate.CreateTextNode(FTestID));
tr.AppendChild(td);
// third column is description
if Assigned(Element) then
begin
td := FTemplate.CreateElement('td');
txt := Element.FirstChild;
while Assigned(txt) do
begin
td.AppendChild(txt.CloneNode(true, FTemplate));
txt := txt.NextSibling;
end;
tr.AppendChild(td);
end;
// fourth column is reason
td := FTemplate.CreateElement('td');
if Element = nil then
s := Error
else if Category <> dcInfo then
begin
if Error <> '' then
begin
if FValError <> '' then
s := '(error) ' + Error
else
s := '(fatal) ' + Error;
end
else
s := '[wrongly accepted]';
end
else // informative
begin
if Error <> '' then
s := Error
else
s := '[accepted]';
end;
// TODO: use &nbsp if text is empty
txt := FTemplate.CreateTextNode(s);
if (Category <> dcPass) and (Category <> dcInfo) then
begin
tmp := FTemplate.CreateElement('em');
tmp.AppendChild(txt);
txt := tmp;
TDOMElement(td)['bgcolor'] := '#ffaacc';
end;
td.AppendChild(txt);
tr.AppendChild(td);
table.AppendChild(tr);
end;
procedure TTestSuite.DiagnoseOut(const ErrorMsg: DOMString);
var
tr, td, txt: TDOMNode;
begin
tr := FTemplate.CreateElement('tr');
td := FTemplate.CreateElement('td');
td.AppendChild(FTemplate.CreateTextNode(FTestID));
tr.AppendChild(td);
td := FTemplate.CreateElement('td');
txt := FTemplate.CreateElement('em');
txt.AppendChild(FTemplate.CreateTextNode(ErrorMsg));
td.AppendChild(txt);
TDOMElement(td)['bgcolor'] := '#ffaacc';
tr.AppendChild(td);
table_output.AppendChild(tr);
end;
procedure Canonicalize(node: TDOMNode);
var
child, work: TDOMNode;
Frag: TDOMDocumentFragment;
begin
child := node.FirstChild;
while Assigned(child) do
begin
if child.NodeType = CDATA_SECTION_NODE then
begin
work := node.OwnerDocument.CreateTextNode(child.NodeValue);
node.ReplaceChild(work, child);
child := work;
end
else if child.NodeType = COMMENT_NODE then
begin
work := child.NextSibling;
node.RemoveChild(child);
child := work;
Continue;
end
else if child.NodeType = ENTITY_REFERENCE_NODE then
begin
Frag := node.OwnerDocument.CreateDocumentFragment;
try
work := child.FirstChild;
while Assigned(work) do
begin
Frag.AppendChild(work.CloneNode(true));
work := work.NextSibling;
end;
work := Frag.FirstChild; // references may be nested
if work = nil then
work := Child.PreviousSibling;
node.ReplaceChild(Frag, child);
child := work;
finally
Frag.Free;
end;
Continue;
end;
if child.HasChildNodes then
Canonicalize(child);
child := child.NextSibling;
end;
end;
function TTestSuite.CompareNodes(actual, correct: TDOMNode;
out Msg: string): Boolean;
var
actAtts, refAtts: TDOMNamedNodeMap;
actList, refList: TDOMNodeList;
I: Integer;
s1, s2: DOMString;
begin
Msg := '';
Result := False;
if actual.NodeType <> correct.NodeType then
FmtStr(Msg, 'actual.NodeType (%d) != correct.NodeType (%d)', [actual.NodeType, correct.NodeType])
else if actual.NodeName <> correct.NodeName then
FmtStr(Msg, 'actual.NodeName (%s) != correct.NodeName (%s)', [actual.NodeName, correct.NodeName])
else if actual.NodeValue <> correct.NodeValue then
FmtStr(Msg, 'actual.NodeValue (%s) != correct.NodeValue (%s)', [actual.NodeValue, correct.NodeValue]);
if Msg <> '' then
Exit;
if actual.NodeType = ELEMENT_NODE then
begin
// first, compare attributes
actAtts := actual.Attributes;
refAtts := correct.Attributes;
if actAtts.Length <> refAtts.Length then
begin
FmtStr(Msg, 'Element ''%s'': attributes.length (%d) != %d', [actual.NodeName, actAtts.Length, refAtts.Length]);
Exit;
end;
for I := 0 to actAtts.Length -1 do
begin
s1 := refAtts.GetNamedItem(actAtts[I].NodeName).NodeValue;
s2 := actAtts[I].NodeValue;
if s1 <> s2 then
begin
FmtStr(Msg, 'Element ''%s'', attribute ''%s'': actual.AttValue (%s) != correct.AttValue (%s)', [actual.NodeName, actAtts[I].NodeName, s2, s1]);
Exit;
end;
end;
// next, compare children
actList := actual.ChildNodes;
refList := correct.ChildNodes;
try
if actList.Count <> refList.Count then
begin
FmtStr(Msg, 'Element ''%s'': actual.ChildNodeCount (%d) != correct.ChildNodeCount (%d)', [actual.NodeName, actList.Count, refList.Count]);
Exit;
end;
for I := 0 to actList.Count -1 do
if not CompareNodes(actList[I], refList[I], Msg) then
Exit;
finally
actList.Free;
refList.Free;
end;
end;
Result := True;
end;
var
i: Integer;
s: string;
SuiteName, ReportName, TemplateName: string;
Validation: Boolean;
begin
writeln('FCL driver for OASIS/NIST XML Test Suite');
writeln('Copyright (c) 2006 by Sergei Gorelkin');
TemplateName := ExtractFilePath(ParamStr(0)) + 'template.xml';
if ParamCount < 2 then
begin
writeln;
writeln('Usage: ', ParamStr(0), ' <suite> <report> [-t template][-v]');
writeln(' -t: specify report template');
writeln(' -v: validating mode');
Exit;
end;
SuiteName := ExpandFilename(ParamStr(1));
ReportName := ExpandFilename(ParamStr(2));
i := 3;
Validation := False;
while i <= ParamCount do
begin
s := Lowercase(ParamStr(i));
if s = '-v' then
Validation := True
else if s = '-t' then
TemplateName := ExpandFileName(ParamStr(i+1));
Inc(i);
end;
with TTestSuite.Create do
try
FSuiteName := SuiteName;
FValidating := Validation;
LoadTemplate(TemplateName);
if Assigned(FTemplate) then
begin
Run(FSuiteName);
HandleTemplatePIs(FTemplate.DocumentElement);
writeln('Writing report to: ', ReportName);
WriteXMLFile(FTemplate, ReportName);
end;
finally
Free;
end;
end.