Revision 9c45c738b057be0a30ca67a3609ec04b6825513a authored by Eric Prud'hommeaux on 03 October 2017, 12:12:48 UTC, committed by Eric Prud'hommeaux on 03 October 2017, 12:12:48 UTC
1 parent c213bf3
Raw File
Validation-test.js
//  "use strict";
var VERBOSE = "VERBOSE" in process.env;
var TERSE = VERBOSE;
var TESTS = "TESTS" in process.env ? process.env.TESTS.split(/,/) : null;
var EARL = "EARL" in process.env;

// var ShExUtil = require("../lib/ShExUtil");
var ShExParser = require("../lib/ShExParser");
var ShExLoader = require("../lib/ShExLoader");
var ShExValidator = require("../lib/ShExValidator");
var TestExtension = require("../extensions/shex-test/module");

var N3 = require("n3");
var N3Util = N3.Util;
var fs = require("fs");
var path = require("path");
var chai = require("chai");
var expect = chai.expect;
var assert = chai.assert;
var findPath = require("./findPath.js");

var schemasPath = findPath("schemas");
var validationPath = findPath("validation");
var manifestFile = validationPath + "manifest.jsonld";
var regexModules = [
  require("../lib/regex/nfax-val-1err"),
  require("../lib/regex/threaded-val-nerr")
];
if (EARL)
  regexModules = regexModules.slice(1);

TODO = [
  // delightfully empty (for now)
];

describe("A ShEx validator", function () {
  "use strict";

  var shexParser = ShExParser.construct();
  beforeEach(shexParser._resetBlanks);
  /*
    Note that the tests.forEach will run before any of the it() functions.
    shexParser._setBase() must execute before shexParser.parse().
   */

  var tests = parseJSONFile(manifestFile)["@graph"][0]["entries"];
  var resultMap = parseJSONFile(__dirname + "/val/test-result-map.json");

  if (TESTS) {
    tests = tests.filter(function (t) {
      return TESTS.indexOf(t["@id"]) !== -1 ||
        TESTS.indexOf(t["@id"].substr(1)) !== -1 ||
        TESTS.indexOf(t.action.schema) !== -1 ||
        TESTS.indexOf(t.action.data) !== -1 ||
        TESTS.indexOf(t.result) !== -1;
    });
  }

  tests = tests.filter(test => {
    return test.trait.indexOf("OneOf") === -1 &&
      TODO.indexOf(test.name) === -1;
  });

  regexModules.forEach(regexModule => {
    tests.forEach(function (test) {
      try {
        var schemaFile = path.resolve(schemasPath, test.action.schema);
        var schemaURL = "file://" + schemaFile;
        var semActsFile = "semActs" in test.action ? path.resolve(schemasPath, test.action.semActs) : null;
        var semActsURL = "file://" + semActsFile;
        var shapeExternsFile = "shapeExterns" in test.action ? path.resolve(schemasPath, test.action.shapeExterns) : null;
        var shapeExternsURL = "file://" + shapeExternsFile;
        var dataFile = path.resolve(validationPath, test.action.data);
        var dataURL = "file://" + dataFile;
        var valFile = resultMap[test["@id"]];
        if (valFile) {
          valFile = "val/" + valFile;
        }
        it("should use " + regexModule.name + " to validate data '" + (TERSE ? test.action.data : dataFile) + // test title
           "' against schema '" + (TERSE ? test.action.schema : schemaFile) +
           "' and get '" + (TERSE ? test.result : valFile) + "'" +
           " in test '" + test["@id"] + "'.",
           function (report) {                                             // test action
             var absoluteVal = valFile ? parseJSONFile(__dirname + "/" + valFile, function (k, obj) {
               // resolve relative URLs in results file
               if (["shape", "reference", "valueExprRef", "node", "subject", "predicate", "object"].indexOf(k) !== -1 &&
                   typeof obj[k] !== "object" &&
                   N3Util.isIRI(obj[k])) {
                 obj[k] = resolveRelativeIRI(["shape", "reference", "valueExprRef"].indexOf(k) !== -1 ? schemaURL : dataURL, obj[k]);
               } else if (["values"].indexOf(k) !== -1) {
                 for (var i = 0; i < obj[k].length; ++i) {
                   if (typeof obj[k][i] !== "object" && N3Util.isIRI(obj[k][i])) {
                     obj[k][i] = resolveRelativeIRI(dataURL, obj[k][i]);
                   }
                 };
               }
             }) : null; // !! replace with ShExUtil.absolutizeResults(JSON.parse(fs.readFileSync(valFile, "utf8")))

             doIt(report, absoluteVal, {results: "val"}, true);
           });

        if (test.result) {
          var resultsFile = test.result ? path.resolve(validationPath, test.result) : null;
          it("should use " + regexModule.name + " to validate data '" + (TERSE ? test.action.data : dataFile) + // test title
             "' against schema '" + (TERSE ? test.action.schema : schemaFile) +
             "' and get '" + (TERSE ? test.result : resultsFile) + "'" +
             " in test '" + test["@id"] + "'.",
             function (report) {                                             // test action
               var res = JSON.parse(fs.readFileSync(resultsFile, "utf8"));
               doIt(report, res, {results: "api"}, true);
             });
        }

        function doIt (report, referenceResult, params, required) {
          var semActs, shapeExterns;
          if (semActsFile) {
            shexParser._setBase(semActsURL);
            semActs = shexParser.parse(fs.readFileSync(semActsFile, "utf8")).
              startActs.reduce(function (ret, a) {
                ret[a.name] = a.code;
                return ret;
              }, {});
          }
          if (shapeExternsFile) {
            shexParser._setBase(shapeExternsURL);
            shapeExterns = shexParser.parse(fs.readFileSync(shapeExternsFile, "utf8")).
              shapes;
          }
          shexParser._setBase(schemaURL);
          var validator;
          var schemaOptions = Object.assign({
            regexModule: regexModule,
            diagnose: true,
            or:
            "trait" in test &&
              test.trait.indexOf("OneOf") !== -1 ?
              "oneOf" :
              "someOf",
            partition:
            "trait" in test &&
              test.trait.indexOf("Exhaustive") !== -1 ?
              "exhaustive" :
              "greedy",
            semActs: semActs,
            validateExtern: function (db, point, shapeLabel, depth, seen) {
              return validator._validateShapeExpr(db, point, shapeExterns[shapeLabel],
                                                  shapeLabel, depth, seen);
            }
          }, params);
          function pickShEx (i) {
            return i + ".shex";
          }
          ShExLoader.load([schemaFile], [], [], [], { parser: shexParser, iriTransform: pickShEx }, {}).
            then(function (loaded) {
              var schema = loaded.schema;
              validator = ShExValidator.construct(schema, schemaOptions);
              var testResults = TestExtension.register(validator);

              assert(referenceResult !== null || test["@type"] === "sht:ValidationFailure", "test " + test["@id"] + " has no reference result");
              // var start = schema.start;
              // if (start === undefined && Object.keys(schema.action.shapes).length === 1)
              //   start = Object.keys(schema.action.shapes)[0];

              var store = new N3.Store();
              var turtleParser = new N3.Parser({documentIRI: dataURL, blankNodePrefix: "", format: "text/turtle"});
              turtleParser.parse(
                fs.readFileSync(dataFile, "utf8"),
                function (error, triple, prefixes) {
                  if (error) {
                    report("error parsing " + dataFile + ": " + error);
                  } else if (triple) {
                    store.addTriple(triple);
                  } else {
                    try {
                      function maybeGetTerm (base, s) {
                        return s === undefined ? null :
                          typeof(s) === "object" ? "\""+s["@value"]+"\""+(
                            "@type" in s ? "^^"+s["@type"] :
                              "@language" in s ? "@"+s["@language"] :
                              ""
                          ):
                        s.substr(0, 2) === "_:" ? s :
                          resolveRelativeIRI(base, s);
                      }
                      var map = maybeGetTerm(manifestFile, test.action.map);
                      if (map) {
                        map = JSON.parse(fs.readFileSync(map, "utf8"));
                        // map = Object.keys(map).reduce((r, k) => {
                        //   return r.concat({node: k, shape: map[k]});
                        // }, [])
                      } else {
                        var focus = maybeGetTerm(dataURL, test.action.focus);
                        var shape = maybeGetTerm(schemaURL, test.action.shape);
                        map = [{node: focus, shape: shape}];
                      }
                      var validationResult = validator.validate(store, map);
                      if (VERBOSE) { console.log("result   :" + JSON.stringify(validationResult)); }
                      if (VERBOSE) { console.log("expected :" + JSON.stringify(referenceResult)); }
                      if (params.results !== "api") {
                        if (test["@type"] === "sht:ValidationFailure") {
                          assert(!validationResult || "errors" in validationResult, "test expected to fail");
                          if (referenceResult)
                            expect(restoreUndefined(validationResult)).to.deep.equal(restoreUndefined(referenceResult));
                        } else {
                          assert(validationResult && !("errors" in validationResult), "test expected to succeed; got " + JSON.stringify(validationResult));
                          expect(restoreUndefined(validationResult)).to.deep.equal(restoreUndefined(referenceResult));
                        }
                      }
                      var xr = test.extensionResults.filter(function (x) {
                        return x.extension === TestExtension.url;
                      }).map(function (x) {
                        return x.prints;
                      });
                      if (xr.length) {
                        expect(testResults).to.deep.equal(xr);
                      }
                      report();
                    } catch (e) {
                      report(e);
                    }
                  }
                });
            }).
            catch(function (e) {
              report(e);
            });
        }
      } catch (e) {
        var throwMe = new Error("in " + test["@id"] + " " + e); // why doesn't this change the error message?
        throwMe.stack = "in " + test["@id"] + " " + e.stack;
        throw throwMe;
      }
    });
  });
});

/* Leverage n3.js's relative IRI parsing.
 * !! requires intimate (so intimate it makes me blush) knowledge of n3.
 */
function resolveRelativeIRI (baseIri, relativeIri) {
  var p = N3.Parser({ documentIRI: baseIri });
  p._readSubject({type: "IRI", value: relativeIri});
  return p._subject;
}

// Parses a JSON object, restoring `undefined` values
function parseJSONFile(filename, mapFunction) {
  "use strict";
  try {
    var string = fs.readFileSync(filename, "utf8");
    var object = JSON.parse(string);
    function resolveRelativeURLs (obj) {
      Object.keys(obj).forEach(function (k) {
        if (typeof obj[k] === "object") {
          resolveRelativeURLs(obj[k]);
        }
        if (mapFunction) {
          mapFunction(k, obj);
        }
      });
    }
    resolveRelativeURLs(object);
    return /"\{undefined\}"/.test(string) ? restoreUndefined(object) : object;
  } catch (e) {
    throw new Error("error reading " + filename +
                    ": " + ("stack" in e ? e.stack : e));
  }
}

// Not sure this is needed when everything's working but I have hunch it makes
// error handling a little more graceful.

// Stolen from Ruben Verborgh's SPARQL.js tests:
// Recursively replace values of "{undefined}" by `undefined`
function restoreUndefined(object) {
  "use strict";
  for (var key in object) {
    var item = object[key];
    if (typeof item === "object") {
      object[key] = restoreUndefined(item);
    } else if (item === "{undefined}") {
      object[key] = undefined;
    }
  }
  return object;
}
back to top