1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
#!/usr/bin/env python3
# Copyright 2022-2025 David Robillard <d@drobilla.net>
# SPDX-License-Identifier: ISC
"""Utilities for data-driven tests."""
# pylint: disable=consider-using-f-string
# pylint: disable=invalid-name
import argparse
import datetime
import difflib
import os
import re
import shlex
import subprocess
import sys
import urllib.parse
class Results:
"""Counts of test executions and failures."""
def __init__(self):
self.n_tests = 0
self.n_failures = 0
def test_passed(self):
"""Record a successful test."""
self.n_tests += 1
def test_failed(self):
"""Record a failed test."""
self.n_tests += 1
self.n_failures += 1
def check(self, condition, message=None):
"""Check a test condition and update counts accordingly."""
if not condition:
self.test_failed()
if message is not None:
error(message)
else:
self.test_passed()
def error(message):
"""Log an error message to stderr."""
sys.stderr.write("error: ")
sys.stderr.write(message)
sys.stderr.write("\n")
def wrapper_args(description, with_input=False):
"""Return the command line arguments for a wrapped test."""
parser = argparse.ArgumentParser(description)
parser.add_argument("--serdi", default="./serdi", help="serdi executable")
parser.add_argument("--wrapper", default="", help="executable wrapper")
if with_input:
parser.add_argument("input", help="input file")
return parser.parse_args(sys.argv[1:])
def command_output(wrapper, command, stdin=None):
"""Run a command and check that stdout matches the expected output."""
proc = subprocess.run(
shlex.split(wrapper) + command,
capture_output=True,
check=True,
encoding="utf-8",
input=stdin,
)
assert wrapper or not proc.stderr
return proc.stdout
def print_result_summary(results):
"""Print test result summary to stdout or stderr as appropriate."""
if results.n_tests <= 0:
error("No tests found")
return -1
failed, total = (results.n_failures, results.n_tests)
if failed == 0:
sys.stdout.write("All {} tests passed\n".format(total))
else:
error("{}/{} tests failed".format(failed, total))
return failed
def uri_path(uri):
"""Return the path component of a URI."""
path = urllib.parse.urlparse(uri).path
drive = os.path.splitdrive(path[1:])[0]
return path if not drive else path[1:]
def file_path(suite_dir, uri):
"""Return a relative path to a file in a test suite."""
return os.path.relpath(os.path.join(suite_dir, os.path.basename(uri)))
def syntax_from_path(path):
"""Return the serd syntax name corresponding to a file path."""
extensions = {
".ttl": "turtle",
".nt": "ntriples",
".trig": "trig",
".nq": "nquads",
}
return extensions[os.path.splitext(path)[1]]
def earl_assertion(test, passed, asserter):
"""Return a Turtle description of an assertion for the test report."""
asserter_str = ""
if asserter is not None:
asserter_str = "\n\tearl:assertedBy <%s> ;" % asserter
return """
[]
\ta earl:Assertion ;%s
\tearl:subject <http://drobilla.net/sw/serd> ;
\tearl:test <%s> ;
\tearl:result [
\t\ta earl:TestResult ;
\t\tearl:outcome %s ;
\t\tdc:date "%s"^^xsd:dateTime
\t] .
""" % (
asserter_str,
test,
"earl:passed" if passed else "earl:failed",
datetime.datetime.now().replace(microsecond=0).isoformat(),
)
def load_rdf(filename, base_uri, command_prefix):
"""Load an RDF file as dictionaries via serdi (only supports URIs)."""
rdf_type = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
model = {}
instances = {}
cmd = command_prefix + [filename, base_uri]
proc = subprocess.run(
cmd,
encoding="utf-8",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
for line in proc.stdout.splitlines():
matches = re.match(r"<([^ ]*)> <([^ ]*)> <([^ ]*)> \.", line)
if matches:
s, p, o = (matches.group(1), matches.group(2), matches.group(3))
if s not in model:
model[s] = {p: [o]}
elif p not in model[s]:
model[s][p] = [o]
else:
model[s][p].append(o)
if p == rdf_type:
if o not in instances:
instances[o] = set([s])
else:
instances[o].update([s])
return model, instances
def lines_equal(from_lines, to_lines, from_filename, to_filename):
"""Return true if from_lines equals to_lines, or print a diff."""
same = True
for line in difflib.unified_diff(
from_lines,
to_lines,
fromfile=os.path.abspath(from_filename),
tofile=os.path.abspath(to_filename),
):
sys.stderr.write(line)
same = False
return same
|