// $Id: harness.js,v 1.5 2000/07/20 07:56:06 carnold Exp $
//
// Copyright 1999-2000 by David Brownell
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU 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 General Public License
// for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//

//
// JavaScript harness for the OASIS/NIST XML conformance test suite,
// testing DOM-based XML parsers.
//
// This uses Windows Scripting Host 5.0 features, and uses the same
// kind of report template as the Java test harness.
//
// Run by:  "cscript harness.js [options...]"
//
// Bootstrap parser == MSXML.DOMDocument
//
//   Parameter          Description                                             Default
//
//   /suite=...         Test suite to execute                   xmlconf/xmlconf.xml
//   /parser=...        ProgID of parser under test             (bootstrap parser)
//   /desc=...          Desription of parser                    (bootstrap parser)
//   /vreport=...       Validation test output file             (disabled)
//   /nvreport=...      Non-validating test output file         (disabled)
//   /template=...      Report template                         template.html
//   /defparser=...     ProgID of parser reading test definitions  (bootstrap parser)
//
//   /debug             Invokes the debugger (must also specify //D to enable debugging)
//   /?                 Display options
//
// One expects something similar should work with Mozilla too, though
// lots of MSXML-isms exist (notably to create and populate a DOM
// tree, which W3C still hasn't got portable APIs to handle).
//
// Originally from Chris Lovett <clovett@microsoft.com>
// Substantially enhanced by David Brownell <david-b@pacbell.net>
// Modified by Curt Arnold <curta@hyprotech.com> to support command line
//    parameters and Xerces testing.
//


//
// Use the patched version of tests, until they get updated.  (Basically,
// all the relative URIs got broken late in the publication cycle.)
// Bugs in the testcases are NOT dealt with in the harness.
//
// var SUITE = "http://www.oasis-open.org/committees/xmltest/xmlconf.xml"
//
var SUITE = null;

//
// Where do we get the report template?
//
var TEMPLATE = "template.xml";


//
//   Path for validating parser report, or null
//
var VREPORT = null;

//
//   Path for nonvalidating parser report, or null
//
var NVREPORT = null;

//
// REPORTING SUPPORT ... we collect all the information needed by
// report template here, and emit an (X)HTML report later.
//
var template;           // filled in at init time

// used internally, also <?run-id name?>
var parser = null;

//
//   bootstrap parser, used to read test definitions
//        Must provide EntityReferences to
//        properly interpret Feb 2000 and earlier OASIS test suites
//
var defParser = "MSXML.DOMDocument";

// used internally, also <?run-id type?>
var validating;

// <?run-id description?>
var parserName = "";

//
//  Program identification and disclaimer
//
WScript.Echo("JavaScript/COM driver for the OASIS/NIST XML 1.0 Test Suite");
WScript.Echo("For more information visit http://xmlconf.sourceforge.net");
WScript.Echo("This program comes with ABSOLUTELY NO WARRANTY");
WScript.Echo("and is licensed under the GNU Public License, Version 2.0.");
WScript.Echo("");

var doValidationTests = true;
var doNonValidationTests = true;
//
//   process command line argumenst
//
var arguments = WScript.Arguments;
var argCount = arguments.count();
var argparts;

for (argIndex = 0; argIndex < argCount; argIndex++) {
    argparts = arguments.item(argIndex).split("=");
    switch (argparts[0].toLowerCase()) {
        case "/debug":
            debugger;
            break;

        case "/suite":
            SUITE = argparts[1];
            break;

        case "/parser":
            parser = argparts[1];
            break;

        case "/defparser":
            defParser = argparts[1];
            break;

        case "/desc":
            parserName = argparts[1];
            break;

        case "/vreport":
            VREPORT = argparts[1];
            break;

        case "/nvreport":
            NVREPORT = argparts[1];
            break;

        case "/template":
            TEMPLATE = argparts[1];
            break;

        case "/?":
            WScript.Echo("Usage: cscript harness.js /suite=... [/options...]");
            WScript.Echo("/?             Display this message.");
            WScript.Echo("//D            Enables debugging (in Windows Script Host).");
            WScript.Echo("/debug         Invokes the debugger; must specify //D to cscript.");
            WScript.Echo("/defparser=... ProgID of bootstrap parser [MSXML.DOMDocument]");
            WScript.Echo("/desc=...      Description of parser (parser ProgID)");
            WScript.Echo("/nvreport=...  Enables non-validating parser test report");
            WScript.Echo("/parser=...    ProgID of parser under test (defparser)");
            WScript.Echo("/suite=...     REQUIRED:  filename or URI of test database ('suite')");
            WScript.Echo("/template=...  Report template [template.xml]");
            WScript.Echo("/vreport=...   Enables validating parser test report");
            WScript.Quit(0);
            break;


        default:
            WScript.Echo("Unrecognized argument " + argparts[0]);
            WScript.Echo("Display command options by 'cscript harness.js /?'");
            WScript.Quit(1);
            break;
    }
}

WScript.Echo("Display command options by 'cscript harness.js /?'");
WScript.Echo("");

if (SUITE == null) {
    WScript.echo ("You need to specify the test /suite=... to use.");
    WScript.Quit(1);
}

if(NVREPORT == null && VREPORT == null) {
    WScript.echo ("You need to specify /nvreport=..., /vreport=..., or both.");
    WScript.Quit(1);
}

// fill in any implied defaults
if (parser == null)
    parser = defParser;
if (parserName == null)
    parserName = parser;

// for use later
var fso = new ActiveXObject ("Scripting.FileSystemObject");

// <?run-id general-entities?>          --> expanded
// <?run-id parameter-entities?>        --> expanded
// <?run-id date?>                      --> check on output

// <?run-id harness?>
var harness = "ECMAScript version";
// <?run-id version?>
var version = "$Id: harness.js,v 1.5 2000/07/20 07:56:06 carnold Exp $";

// <?run-id java?>
var runtime = "Microsoft "
        + ScriptEngine () + " Version "
        + ScriptEngineMajorVersion () + "."
        + ScriptEngineMinorVersion () + "."
        + ScriptEngineBuildVersion ();

// <?run-id os?>
var os = "Undetermined version of MS-Windows";

// <?run-id testsuite?>
var suite;

// <?run-id status?>
var status;
// for <?run-id passed?>
var total;
// for <?run-id passed?>
var passed;
// <?run-id passed-negative?>
var negpass;
// <?run-id failed?>
var failed;

    // tests get skipped if e.g. the parser doesn't read
    // a kind of entity that the test depends on
// <?run-id skipped?>
var skipped;

//
// These collect output for each section of the report.
//

// <?table valid?>
var table_valid;
// <?table valid output?>
var table_valid_output;
// <?table invalid positive?>, <?table invalid negative?>
var table_invalid;
// <?table not-wf?>
var table_not_wf;
// <?table error?>
var table_informative;


//
// Utility:  remove "noise" nodes (other than entity refs and
// doctype) which don't affect the output tests (and which we
// don't want to see in the test report).
//
function purify (node)
{
    if (node.nodeType == 10 /* NODE_DOCUMENT_TYPE */ )
        // NOTE:  MSXML seems to have children here -- bug
        return;

    var children = node.childNodes;
    var child;

    for (var index = 0;
            (child = children.item (index)) != null;
            index++) {

        // CDATA is just an alternate text representation using
        // different delimiters ("]]>") than normal text ("&", "<").
        if (child.nodeType == 4 /* NODE_CDATA_SECTION */) {
            node.replaceChild (
                child.ownerDocument.createTextNode (child.nodeValue),
                child);
            continue;
        }

        if (child.nodeType == 7 /* NODE_PROCESSING_INSTRUCTION */) {
            // NOTE:  MSMXL creates two PI nodes for non-PI data,
            // which it shouldn't do.
            //  <?xml encoding='...'?>  ... text decl
            //  <?xml version='1.0'?>   ... xml decl
            if (child.nodeName == "xml") {
                node.removeChild (child);
                index--;
            }
            continue;
        }
        if (child.nodeType == 8 /* NODE_COMMENT */) {
            node.removeChild (child);
            index--;
            continue;
        }
        if (child.hasChildNodes ()) {
            purify (child);
            continue;
        }
    }
    return node;
}


//
// ".../abc/xyz.xml" ==> ".../abc"
// (for resolving relative URIs)
//
function pruneURI (uri)
{
    var temp = uri.lastIndexOf ("/");
    if (temp == -1)
        return "";
    temp = uri.substring (0, temp);
    return temp;
}

//
//   this tries to determine if a URI is absolute
//
function isAbsoluteURI(uri)
{
    //
    //  search for the first forward or reverse slash
    //
    var slashPos = uri.search("[/\\\\]");
    switch (slashPos) {
        //
        //   no slash, is relative
        //
        case -1:
            return false;

        //
        //   initial slash, is absolute
        //
        case 0:
            return true;

        //
        //   preceding colon, is absolute
        //
        default:
            if(":" == uri.substring(slashPos-1,1))
                    return true;
    }
    return false;
}


//
// Return the node's base URI, suitable for appending "/foo/bar.xml"
//
// Constructs an "xml:base" URI as it's searching for the real root
// node of the this entity (or document).  If it's absolute, it'll
// be returned in certain cases. 
//
function getBaseURI (docBase, node)
{
    var xmlBase = null;
    var realBase = null;

outer:
    for ( ; node != null; node = node.parentNode) {
        var temp = null;
        switch (node.nodeType) {
            case 1: // NODE_ELEMENT
                //
                //   prepend this xml:base if we're still building a good guess
                //
                try {
                    var xmlbaseatt = node.getAttribute("xml:base");
                    if (xmlbaseatt != null && xmlbaseatt.length > 0) {
                        //
                        //   if there has been no deeper xmlbase
                        //      then this is the xmlbase
                        if (xmlBase == null)
                            xmlBase = xmlbaseatt;
                        //
                        //   otherwise prepend to any relative xml:base
                        //
                        else if (!isAbsoluteURI(xmlBase)) {
                            xmlbaseatt = xmlbaseatt.pruneURI(xmlbaseatt);
                            xmlbaseatt = xmlbaseatt.concat("/");
                            xmlBase = xmlbaseatt.concat(xmlBase);
                        }
                    }
                } catch(e) {
                    // Ignore this, Xerces throws it
                }
                break;

            case 5: // NODE_ENTITY_REFERENCE
                {
                    var entities = node.ownerDocument.doctype.entities;
                    var entity = entities.getNamedItem (node.nodeName);

                    // DOM internal error
                    if (entity == null)
                        throw "** Can't find entity!!";
                    if (entity.systemId == null || entity.systemId == "")
                        throw "** Bogus system ID!!";

                    if (realBase == null)
                        realBase = entity.systemId;
                    else {
                        // prepend to existing relative URI
                        var entityBase = entity.systemId;
                        entityBase = pruneURI(entityBase);
                        entityBase = entityBase.concat("/");
                        realBase = entityBase.concat(realBase);
                    }
                    if (isAbsoluteURI (realBase)) {
                        docBase = realBase;
                        realBase = "";
                        break outer;
                    }
                }
                break;
        } // end  switch on nodetype
    }

    // found entity ref sequence and an absolute URI at the top?
    docBase = pruneURI (docBase);
    if (realBase != null && isAbsoluteURI (docBase))
        return pruneURI (docBase + "/" + realBase);
        
    // ... how about absolute URIs in xml:base hint sequences?
    if (xmlBase != null && isAbsoluteURI (xmlBase))
        return xmlBase;
    
    // ... entity ref sequence with relative URI at top?
    // sometimes the out-of-band PWD-style info won't change.
    if (realBase != null)
        return pruneURI (docBase + "/" + realBase);

        //
        //   already pruned, doing it again would lose a part of the path
        //
        return docBase;
}


var xhtmlns = "http://www.w3.org/1999/xhtml";
//
// Save the diagnostic into the specified (X)HTML table.
//
function diagnose (element, table, label, id, parseError)
{
    var tr = template.createNode(1,"tr",xhtmlns);
    var td;
    var text;

    // first column is test section/chapter (if known)
    // hack:  if no element, skip
    if (element != null) {
        text = element.getAttribute ("SECTIONS")
        td = template.createNode(1,"td",xhtmlns);
        text = template.createTextNode (text);
        td.appendChild (text);
        tr.appendChild (td);
    }

    // second column is test ID
    td = template.createNode(1,"td",xhtmlns);
    text = template.createTextNode (id);
    td.appendChild (text);
    tr.appendChild (td);

    // third column is test description -- we drop any formatting
    // hack:  if no element, we skip this
        // NOTE:  "element.text" is specific to MSXML
    if (element != null) {
        td = template.createNode(1,"td",xhtmlns);
        text = template.createTextNode (element.text);
        td.appendChild (text);
        tr.appendChild (td);
    }

    // fourth column is the diagnostic reason
    // hack:  if no element, 5th arg is the diagnostic, not a parse error
    // hack:  if label is "INFORMATIVE", conformance is not at issue

        // MSXML doesn't have the variety of possible outcomes
        // that a SAX parser has (e.g. warnings, and oddities in
        // exception reporting), which simplifies programming.
        // It'd be really useful if validity errors were continuable,
        // however.

    td = template.createNode(1,"td",xhtmlns);
    if (element == null)
        text = parseError;
    else if (label != "INFORMATIVE") {
        if (parseError != null)
            text = "(fatal) " + parseError.reason;
        else
            text = "[wrongly accepted]";
    } else {
        if (parseError != null)
            text = parseError.reason;
        else
            text = "[accepted]";
    }
        // XXX if text starts as null or empty, use &nbsp;
        // or better yet its char ref equivalent ... empty
        // html table cells often render oddly

    text = template.createTextNode (text);

    // highight diagnostic as an error, unless it's a
    // "?PASS" (negative test that failed -- maybe for the right reason)
    // or is purely informative (conformance shouldn't be an issue)

    if (label != "?PASS" && label != "INFORMATIVE") {
        // italicize the element if it's an error, so output matches its
        // docs (errors highlighted on b/w hardcopy and to colorblind folk)
        var temp = template.createNode (1,"em",xhtmlns);
        temp.appendChild (text);
        text = temp;
        td.setAttribute ("bgcolor", "#ffaacc");
    }

    td.appendChild (text);
    tr.appendChild (td);


    // update that particular result table

        // XXX if we know the section & rule, insert it directly into
        // the appropriate part of that table, so it's always sorted

    table.appendChild (tr);
}


//
// Remove all entity refs ... need to do this before most other
// cleanup since this is the only way to get rid of "readonly"
// nodes that we'd otherwise be unable to modify or remove.
// (We always remove them, since any reported entity structure
// is irrelevant to testing processor conformance as well as
// most applications.)
//
function removeRefs (node)
{
    if (node.nodeType == 10 /* NODE_DOCUMENT_TYPE */ )
        // NOTE:  MSXML seems to have children here -- bug
        return;

    var children = node.childNodes;
    var child;

    for (var index = 0;
            (child = children.item (index)) != null;
            index++) {

        if (child.nodeType == 1 /* NODE_ELEMENT */) {
            var attrs = child.attributes;
            var attr;

            while ((attr = attrs.nextNode ()) != null) {
                // setting value will
                // (a) nuke any entity refs, so value is only text;
                // (b) clear the 'specified' flag if it's set
                attr.nodeValue = attr.nodeValue;
            }
            // FALLTHROUGH ... children need attention too
        }

        if (child.nodeType == 5 /* NODE_ENTITY_REFERENCE */) {
            var frag = node.ownerDocument.createDocumentFragment ();
            var kids = child.childNodes;
            var kid;

            // make clones of all children
            for (var i = 0; (kid = kids.item (i)) != null; i++)
            {
                // NOTE:  MSXML uses PIs to represent text declarations;
                // get rid of them ASAP, they're not PIs, but are only
                // declarations affecting entity parsing
                if (kid.nodeType == 7 && kid.nodeName == "xml")
                    continue;
                frag.appendChild (kid.cloneNode (true));
            }

            node.replaceChild (frag, child);
            index--;    // refs could have nested in the expansion
            continue;
        }

        if (child.hasChildNodes ()) {
            removeRefs (child);
            continue;
        }
    }
    return node;
}


//
// For output tests, under the flawed assumption that the parser
// we're testing can reference data correctly.  (Flaw:  that's part
// of what we're testing!!!)
//
// There must be no "noise" nodes in the input, and text must be
// normalized ... only text, PI, and elements should show up here.
// (Document comparison is separate, needs to address DocumentType
// issues like notations, and for validating parsers any unparsed
// entities.)
//
function compareNodes (actual, correct)
{
    if (actual.nodeType != correct.nodeType)
        return "actual.nodeType (" + actual.nodeType
                + ") != correct.nodeType (" + correct.nodeType + ")";
    if (actual.nodeName != correct.nodeName)
        return "actual.nodeName (" + actual.nodeName
                + ") != correct.nodeName (" + correct.nodeName + ")";
    if (actual.nodeValue != correct.nodeValue)
        return "actual.nodeValue (" + actual.nodeValue
                + ") != correct.nodeValue (" + correct.nodeValue + ")";

    if (actual.nodeType != 1)           // NODE_ELEMENT
        return null;

    //
    // first, compare attributes
    //
    var list1 = actual.attributes;
    var list2 = correct.attributes;

    if (list1.length != list2.length)
        return "Element '" + actual.nodeName
                + "': actual.attributes.length (" + list1.length
                + ") != correct.attributes.length (" + list2.length + ")";

    for (;;) {
        var a1 = list1.nextNode ();

        if (a1 == null)
            break;

        var a2 = list2.getNamedItem (a1.nodeName);

        if (a1.nodeValue != a2.nodeValue)
            return "Element '" + actual.nodeName
                    + "', attribute '" + a1.nodeName
                    + "': actual.nodeValue (" + a1.nodeValue
                    + ") != correct.nodeValue (" + a2.nodeValue + ")";
    }

    //
    // then, compare children
    //
    list1 = actual.childNodes;
    list2 = correct.childNodes;

    if (list1.length != list2.length)
        return "Element '" + actual.nodeName
                + "' actual.childNodes.length (" + list1.length
                + ") != correct.childNodes.length (" + list2.length + ")";

    for (var i = 0; i < list1.length; i++) {
        var error = compareNodes (list1.item (i), list2.item (i));
        if (error != null)
            return error;
    }
    return null;
}


//
// Run each test -- the core test (does it parse correctly?)
// and, if appropriate, an output test (was the result correct?)
//
function runTest (docBase, element)
{
    // where will output diagnostic go?
    var table;

    // fetch basic test attributes ...
    var id = element.getAttribute ("ID");
    var type = element.getAttribute ("TYPE");
    var output = element.getAttribute ("OUTPUT");
    var uri = element.getAttribute ("URI");

    // NOTE: OASIS/NIST omitted the OUTPUT3 data cases, which should
    // be used with validating parsers (which have special requirements
    // for ignorable whitespace and unparsed entities -- untested here).

    // ignoring ENTITIES for the moment (assuming they are always read)

    // is this a positive (must-succeed) test?
    var positive;

    if (type == "not-wf") {
        table = table_not_wf;
        positive = false;
    } else if (type == "error") {
        table = table_informative;
        positive = false;
    } else if (type == "valid") {
        table = table_valid;
        positive = true;
    } else if (type == "invalid") {
        table = table_invalid;
        if (validating)
            positive = false;
        else
            positive = true;
    }

    // update the relative URIs from the test database
    // (DTD specifies they're relative to the document in
    // which the URI is found)
    var base = getBaseURI (docBase, element)

    uri = base + "/" + uri;
    if (output == "" || type != "valid")
        output = null;
    else if (output != null)
        output = base + "/" + output;

    // report all results, but "error" cases don't affect conformance
    if (type != "error") {
        total += 1;
        if (output != null)
            total += 1;
    }

    // Parse the test case
    // NOTE:  this relies on MSXML-specific
    // methods to load XML text and detect errors
    var testdoc = new ActiveXObject (parser);
    testdoc.validateOnParse = validating;
    testdoc.async = false;

    // NOTE:  this is _essential_ to be very correct ... but
    // it's not the default !!!
    testdoc.preserveWhiteSpace = true;
    testdoc.load (uri);

    // report ... negative tests are easy, except
    // telling if they failed for the right reason
    if (!positive) {

        // always report informative cases
        if (type == "error") {
            diagnose (element, table, "INFORMATIVE", id, testdoc.parseError);

        // otherwise, succeeding is incorrect ...
        } else if (testdoc.parseError.errorCode == 0) {
            if (validating && testdoc.docType == null)
            {
                // MSXML succeeds if there is no DTD, even when validateOnParse=true
                // therefore, check to see if docType is null, if it is then obviously
                // it cannot be a valid document.
                var error = new Object();
                error.reason = "NO DTD!!";
                diagnose (element, table, "?PASS", id, error);
                negpass++;
            }
            else
            {
                failed++;
                diagnose (element, table, "NEGFAIL", id, null);
            }

        // ... and failing is correct, but the reason must be correct
        } else {
            diagnose (element, table, "?PASS", id, testdoc.parseError);
            negpass++;
        }
        return;
    }

    // positive tests that fail are easy too
    if (testdoc.parseError.errorCode != 0) {
        failed++;
        diagnose (element, table, "FAIL", id, testdoc.parseError);
        if (output != null) {
            failed++;
            diagnose (null, table_valid_output, "xxx", id,
                "[ input failed, no output to test ]");
        }
        return;
    }


    //
    // There are two basic issues with output testing here.
    //
    //  (1) Output testing needs to transform parser results into a
    //      form that can be byte-wise compared with reference data.
    //      But JavaScript can't manipulate such bytes.
    //
    //  (2) If you trust that the reference data can safely be read
    //      by the parser under test -- RISKY assumption! -- then data
    //      in DOM can be compared, vs a binary comparison.
    //
    // Of necessity, the latter approach is used here.  Even given its
    // basic flaw (trusting the DOM not to have certain bugs), it can
    // turn up some useful information.
    //
    if (output == null)
        return;

    try {
        // discard irrelevant data that's reported by this DOM
        removeRefs (testdoc);
        purify (testdoc);
        try {
            testdoc.documentElement.normalize ();
        } catch (e) {
            e.diagnostic =
                "Element.normalize internal error (" + e.diagnostic + ")";
            throw e;
        }

        // read the reference data

            // NOTE:  this relies on MSXML-specific
            // methods to load XML text
        var refdoc = new ActiveXObject (parser);
        refdoc.async = false;
            // NOTE:  this is _essential_ to be very correct ... and
            // it's not the default !!!
        refdoc.preserveWhiteSpace = true;
        refdoc.validateOnParse = false;
        if (refdoc.load (output) == false) {
            failed++;
            diagnose (null, table_valid_output, "xxx", id,
                "[ can't read reference data: "
                        + refdoc.parseError.reason + " ]");
            return;
        }
        // refdoc.documentElement.normalize ();

        // compare results ... the only special case is for
        // notations, everything else must be identical
        var docNodes = testdoc.childNodes;
        var refNodes = refdoc.childNodes;

            // this loop is nonportable (simpler than maintaining two
            // different indices) but could be written portably
        for (;;) {
            var actual = docNodes.nextNode ();
            var correct = refNodes.nextNode ();

            if (correct == null) {
                if (actual != null) {
                    failed++;
                    diagnose (null, table_valid_output, "xxx", id,
                        "Extra data: " + actual.xml);
                }
                // successful return!
                return;
            }
            if (actual == null) {
                failed++;
                diagnose (null, table_valid_output, "xxx", id,
                    "Missing data: " + correct.xml);
                return;
            }

            if (correct.nodeType == 10) {       // NODE_DOCUMENT_TYPE
                if (actual.nodeType != 10) {
                    failed++;
                    diagnose (null, table_valid_output, "xxx", id,
                        "[ no doctype from parsing testcase ]");
                    return;
                }

                var refNotations = correct.notations;
                var docNotations = actual.notations;

                    // NOTE:  bug, MSXML doesn't consistently implement
                    // NamedNodeMap.item(); this loop is nonportable
                for (;;) {
                    var notation;
                    var result;

                    notation = refNotations.nextNode ();
                    if (notation == null)
                        break;

                    result = docNotations.getNamedItem (notation.nodeName);
                    if (result == null) {
                        failed++;
                        diagnose (null, table_valid_output, "xxx", id,
                            "missing notation decl: " + notation.nodeName);
                        return;
                    }
                    if (notation.systemId != result.systemId
                            || notation.publicId != result.publicId) {
                        failed++;
                        diagnose (null, table_valid_output, "xxx", id,
                            "incorrect notation decl: " + notation.nodeName);
                        return;
                    }
                }
                continue;
            }

            // skip actual doctype if the notations didn't matter
            if (actual.nodeType == 10)          // NODE_DOCUMENT_TYPE
                actual = docNodes.nextNode ();

            // provide an indication of what went wrong (if anything)
            var result = compareNodes (actual, correct);
            if (result != null) {
                failed++;
                diagnose (null, table_valid_output, "xxx", id, result);
                return;
            }
        }

    } catch (e) {
        failed++;
        diagnose (null, table_valid_output, "xxx", id,
            "[ exception thrown: " + e.diagnostic + " ]");
    }
}


//
// Run every test in the specified test database, leaving results
// in various tables and other state, ready to be printed out.
//
function runSuite (TESTS)
{
    WScript.echo ("Test database: " + TESTS);

        // NOTE:  this relies on MSXML-specific
        // methods to load XML text
    var doc = new ActiveXObject (defParser);
    doc.async = false;
    doc.validateOnParse = true;
    if (doc.load (TESTS) == false) {
            //
            //   see if document will load in non-validating mode
            //       (shouldn't be necessary to create a new one, but
            //          Xerces doesn't like multiple parses)
            var reason = doc.parseError.reason;
            doc = new ActiveXObject(defParser);
            doc.validateOnParse = false;
            doc.async = false;
            if (doc.load(TESTS) == false) {
                WScript.echo (TESTS + " was not well-formed");
                WScript.echo ("reason: " + doc.parseError.reason);
                return;
            }
            WScript.echo (TESTS + " was well-formed but not valid, tests proceeding.");
            WScript.echo ("reason: " + reason);
    }

    // initialize results
    suite = doc.documentElement.getAttribute ("PROFILE");
    total = 0;
    passed = 0;
    failed = 0;
    negpass = 0;
    skipped = 0;

    // on MSXML, doc.getElementsByTagName duplicates EVERY test element
    // (perhaps it looks inside declared entities, via the doctype
    // children that aren't supposed to exist?)

    var cases = doc.documentElement.getElementsByTagName ("TEST");
    var index = 0;
    var node = null;

    WScript.echo ("Testing, validation = " + validating);
    for (var index = 0;
            (node = cases.item (index)) != null;
            index++) {
        runTest (TESTS, node)
    }

    passed = total - failed;
    passed -= skipped;

    var rate = parseInt ((passed * 10000) / total) / 100;

    WScript.echo ("Found " + index + " basic test cases.  ");
    WScript.echo ("Found " + total + " overall testcases.  ");
    WScript.echo ("Skipped: " + skipped);
    WScript.echo ("Passed:  " + passed);
    WScript.echo ("Failed:  " + failed);
    WScript.echo ("Negative passes:  " + negpass + " (need examination).");
    WScript.echo ("");

    if (passed == 0)
        status = "N/A";
    else if (passed == total)
        status = "CONFORMS (provisionally)";
    else
        status = "DOES NOT CONFORM";
}


//
// Use the same report template as the SAX harness, but work it
// differently.
//
function loadTemplate (templateFile)
{
        // NOTE:  this relies on MSXML-specific
        // methods to load XML text
    template = new ActiveXObject (defParser);
    template.async = false;
    template.validateOnParse = false;
    if (template.load (templateFile) == false) {
            WScript.echo ("couldn't load " + templateFile);
            WScript.echo ("reason: " + template.parseError.reason);
            return;
    }
    purify (template);

    table_valid = template.createDocumentFragment ();
    table_valid_output = template.createDocumentFragment ();
    table_invalid = template.createDocumentFragment ();
    table_not_wf = template.createDocumentFragment ();
    table_informative = template.createDocumentFragment ();
}


//
// The template has PIs indicating where various chunks of data
// should go.  They get substituted, conditionally removed, etc ...
//
function handleTemplatePIs (element)
{
    var children = element.childNodes;
    var child;
    var nuke = false;

    for (var index = 0;
            (child = children.item (index)) != null;
            index++) {

        // inside a failed <?if ...?>...<?endif?>
        if (nuke == true && child.nodeType != 7 /* PI */) {
            element.removeChild (child);
            index--;
            continue;
        }
        if (child.hasChildNodes ()) {
            handleTemplatePIs (child);
            continue;
        }
        if (child.nodeType != 7 /* NODE_PROCESSING_INSTRUCTION */)
            continue;

        var data = child.data;
        var newChild = null;

        if (child.target == "run-id") {
            // ... parser descriptions
            if ("name" == data)
                newChild = template.createTextNode (parser);
            else if ("description" == data)
                newChild = template.createTextNode (parserName);
            else if ("general-entities" == data)
                newChild = template.createTextNode ("included");
            else if ("parameter-entities" == data)
                newChild = template.createTextNode ("included");
            else if ("type" == data) {
                if (validating)
                    data = "Validating";
                else
                    data = "Non-Validating";
                newChild = template.createTextNode (data);
            }

            // ... test run description
            else if ("date" == data)
                newChild = template.createTextNode (
                        new Date ().toUTCString ());
            else if ("harness" == data)
                newChild = template.createTextNode (harness);
            else if ("java" == data)
                newChild = template.createTextNode (runtime);
            else if ("os" == data)
                newChild = template.createTextNode (os);
            else if ("testsuite" == data)
                newChild = template.createTextNode (suite);
            else if ("version" == data)
                newChild = template.createTextNode (version);

            // ... test result info
            else if ("failed" == data)
                newChild = template.createTextNode (failed.toString (10));
            else if ("passed" == data)
                newChild = template.createTextNode (passed.toString (10));
            else if ("passed-negative" == data)
                newChild = template.createTextNode (negpass.toString (10));
            else if ("skipped" == data)
                newChild = template.createTextNode (skipped.toString (10));
            else if ("status" == data)
                newChild = template.createTextNode (status);

            // ELSE ILLEGAL
            else
                newChild = template.createComment (
                    "(illegal run-id PI data, " + data + ")");

            element.replaceChild (newChild, child);
            continue;

        } else if (child.target == "table") {
            if (data == "valid")
                element.replaceChild (table_valid, child);
            else if (data == "valid output")
                element.replaceChild (table_valid_output, child);
            else if (data == "invalid negative")
                element.replaceChild (table_invalid, child);
            else if (data == "invalid positive")
                element.replaceChild (table_invalid, child);
            else if (data == "not-wf")
                element.replaceChild (table_not_wf, child);
            else if (data == "error")
                element.replaceChild (table_informative, child);
            else {
                element.replaceChild (
                    template.createComment (
                        "(illegal table PI data, " + data + ")"),
                    child);
            }

        // if/endif don't nest, and always have the same parent
        // we rely on those facts here!
        } else if (child.target == "if") {
            if (data == "validating" && validating)
                nuke = false;
            else if (data == "nonvalidating" && !validating)
                nuke = false;
            else
                nuke = true;
            element.removeChild (child);
            index--;
            continue;

        } else if (child.target == "endif") {
            nuke = false;
            element.removeChild (child);
            index--;
            continue;

        } else {
            element.replaceChild (
                template.createComment (
                    "(illegal PI target, " + child.target + ")"),
                child);
        }
    }
}


//
// Run test in validating or nonvalidating mode, saving output
//
function testParser (valFlag, outfile)
{
    loadTemplate (TEMPLATE);

    if (template != null) {
        validating = valFlag;
        runSuite (SUITE);
        if (suite != null) {
            handleTemplatePIs (template.documentElement)
            // WScript.echo (template.documentElement.xml);
            template.save (outfile);
        }
        suite = null;
    }
}


//
// workaround current Xerces/COM wrapper problem:
// it needs absolute filenames as input.
//
var regex = new RegExp ("\\\\", "g");
function reslash (s)
{
    return s.replace (regex, "/");
}

if (!isAbsoluteURI (SUITE))
    SUITE = reslash (fso.GetAbsolutePathName (SUITE));
if (!isAbsoluteURI (TEMPLATE))
    TEMPLATE = reslash (fso.GetAbsolutePathName (TEMPLATE));

//
// Run all requested tests
//
if (NVREPORT != null)
    testParser (false, NVREPORT);
if (VREPORT != null)
    testParser (true, VREPORT);
