{ 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 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   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), ' [-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.