Newer
Older
sparql-test-runner / src / main / scala / uk / org / floop / sparqlTestRunner / Run.scala
package uk.org.floop.sparqlTestRunner

import java.io._
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Path}

import org.apache.jena.query.{DatasetFactory, QueryExecutionFactory, QueryFactory, ResultSetFormatter}
import org.apache.jena.rdf.model.{Model, ModelFactory}
import org.apache.jena.riot.RDFDataMgr

import scala.io.Source
import scala.xml.{NodeSeq, PrettyPrinter}

case class Config(dir: File = new File("tests/sparql"),
                  report: File = new File("reports/TESTS-sparql-test-runner.xml"),
                  ignoreFail: Boolean = false,
                  data: Seq[File] = Seq())

object Run extends App {
  val packageVersion: String = getClass.getPackage.getImplementationVersion()
  val parser = new scopt.OptionParser[Config]("sparql-testrunner") {
    head("sparql-testrunner", packageVersion)
    opt[File]('t', "testdir") optional() valueName "<dir>" action { (x, c) =>
      c.copy(dir = x)
    } text "location of SPARQL queries to run, defaults to tests/sparql"
    opt[File]('r', "report") optional() valueName "<report>" action { (x, c) =>
      c.copy(report = x)
    } text "file to output XML test report, defaults to reports/TESTS-sparql-test-runner.xml"
    opt[Unit]('i', "ignorefail") optional() action { (_, c) =>
      c.copy(ignoreFail = true)
    } text "exit with success even if there are reported failures"
    arg[File]("<file>...") unbounded() required() action { (x, c) =>
      c.copy(data = c.data :+ x) } text "data to run the queries against"
  }
  parser.parse(args, Config()) match {
    case Some(config) =>
      val dataset = DatasetFactory.create
      for (d <- config.data) {
        RDFDataMgr.read(dataset, d.toString)
      }
      val union = ModelFactory.createDefaultModel
      union.add(dataset.getDefaultModel)
      union.add(dataset.getUnionModel)
      dataset.close()
      val (error, results) = runTestsUnder(config.dir, union, config.dir.toPath)
      for (dir <- Option(config.report.getParentFile)) {
        dir.mkdirs
      }
      val pp = new PrettyPrinter(80, 2)
      val pw = new PrintWriter(config.report)
      pw.write(pp.format(<testsuites>{results}</testsuites>))
      pw.close
      System.exit((error && !config.ignoreFail) match {
        case true => 1
        case false => 0
      })
    case None =>
  }

  def runTestsUnder(dir: File, model: Model, root: Path): (Boolean, NodeSeq) = {
    var testSuites = NodeSeq.Empty
    var testCases = NodeSeq.Empty
    var overallError = false
    var errors = 0
    var skipped = 0
    var tests = 0
    val timeSuiteStart = System.currentTimeMillis()
    var subSuiteTimes = 0L
    for (f <- dir.listFiles) yield {
      if (f.isDirectory) {
        val subSuiteStart = System.currentTimeMillis()
        val (error, subSuites) = runTestsUnder(f, model, root)
        testSuites ++= subSuites
        overallError |= error
        subSuiteTimes += (System.currentTimeMillis() - subSuiteStart)
      } else if (f.isFile && f.getName.endsWith(".sparql")) {
        val timeTestStart = System.currentTimeMillis()
        val relativePath = root.relativize(f.toPath).toString
        val className = relativePath.substring(0, relativePath.lastIndexOf('.')).replace(File.pathSeparatorChar, '.')
        val comment = {
          val queryLines = Source.fromFile(f).getLines()
          if (queryLines.hasNext) {
            val line = queryLines.next()
            if (line.startsWith("# "))
              line.substring(2)
            else
              line
          } else
            className
        }
        tests += 1
        val query = QueryFactory.create(new String(Files.readAllBytes(f.toPath), StandardCharsets.UTF_8))
        val exec = QueryExecutionFactory.create(query, model)
        if (query.isSelectType) {
          var results = exec.execSelect()
          var nonEmptyResults = results.hasNext()
          val timeTaken = (System.currentTimeMillis() - timeTestStart).toFloat / 1000
          testCases = testCases ++ <testcase name={comment} class={className} time={timeTaken.toString}>
            {
              val out = new ByteArrayOutputStream
              ResultSetFormatter.outputAsCSV(out, results)
              val actualResults = out.toString("utf-8")
              val expect = new File(f.getPath.substring(0, f.getPath.lastIndexOf('.')) + ".expected")
              if (expect.exists && expect.isFile) {
                val expectedResults = new String(Files.readAllBytes(expect.toPath), StandardCharsets.UTF_8)
                if (actualResults != expectedResults) {
                  errors += 1
                  System.err.println(s"Testcase $comment\nExpected:\n${expectedResults}\nActual:\n${actualResults}")
                    <error message={"Expected: \n" + expectedResults + "\nGot: \n" + actualResults}/>
                }
              } else {
                // assume there should be no results
                if (nonEmptyResults) {
                  errors += 1
                  System.err.println(s"Testcase $comment\nExpected empty result set, got:\n${actualResults}")
                    <failure message={s"Expected empty result set, got:\n${actualResults}"}/>
                }
              }
            }
          </testcase>
        } else if (query.isAskType) {
          val result = exec.execAsk()
          val timeTaken = (System.currentTimeMillis() - timeTestStart).toFloat / 1000
          testCases = testCases ++ <testcase name={comment} class={className} time={timeTaken.toString}>{
            if (result) {
              errors += 1
              System.err.println(s"Testcase $comment\nExpected ASK query to return FALSE")
              <failure message={"Constraint violated"}/>
            }}</testcase>
        } else {
          skipped += 1
          System.out.println(s"Skipped testcase $comment")
          testCases = testCases ++ <testcase name={comment} class={className}>
            <skipped/>
          </testcase>
        }
      }
    }
    if (errors > 0) {
      overallError = true
    }
    val testSuiteTime = (System.currentTimeMillis() - timeSuiteStart - subSuiteTimes).toFloat / 1000
    val suiteName = {
      val relativeName = root.getParent.relativize(dir.toPath).toString
      if (relativeName.length == 0) {
        "root"
      } else {
        relativeName
      }
    }
    (overallError, testSuites ++ <testsuite errors={errors.toString} tests={tests.toString} time={testSuiteTime.toString}
                             name={suiteName} skipped={skipped.toString}>
      {testCases}
    </testsuite>)
  }
}