Skip to content

Commit 640826d

Browse files
committed
Migrate test runner to pytest
1 parent bd1ce40 commit 640826d

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import os
2+
import unittest
3+
import pytest
4+
5+
# Import the existing test runner module so we can reuse Manifest/Test
6+
# implementations with minimal changes.
7+
from . import runtests
8+
9+
10+
def pytest_addoption(parser):
11+
# Do only long options for pytest integration; pytest reserves
12+
# lowercase single-letter short options for its own CLI flags.
13+
parser.addoption('--tests', nargs='*', default=[], help='A manifest or directory to test')
14+
parser.addoption('--earl', dest='earl', help='The filename to write an EARL report to')
15+
parser.addoption('--loader', dest='loader', default='requests', help='The remote URL document loader: requests, aiohttp')
16+
parser.addoption('--number', dest='number', help='Limit tests to those containing the specified test identifier')
17+
18+
19+
def pytest_configure(config):
20+
# Apply loader choice and selected test number globally so that the
21+
# existing `runtests` helpers behave the same as the CLI runner.
22+
loader = config.getoption('loader')
23+
if loader == 'requests':
24+
runtests.jsonld._default_document_loader = runtests.jsonld.requests_document_loader()
25+
elif loader == 'aiohttp':
26+
runtests.jsonld._default_document_loader = runtests.jsonld.aiohttp_document_loader()
27+
28+
number = config.getoption('number')
29+
if number:
30+
runtests.ONLY_IDENTIFIER = number
31+
# If an EARL output file was requested, create a session-level
32+
# EarlReport instance we will populate per-test.
33+
earl_fn = config.getoption('earl')
34+
if earl_fn:
35+
config._earl_report = runtests.EarlReport()
36+
else:
37+
config._earl_report = None
38+
39+
40+
def _flatten_suite(suite):
41+
"""Yield TestCase instances from a unittest TestSuite (recursively)."""
42+
if isinstance(suite, unittest.TestSuite):
43+
for s in suite:
44+
yield from _flatten_suite(s)
45+
elif isinstance(suite, unittest.TestCase):
46+
yield suite
47+
48+
49+
def pytest_generate_tests(metafunc):
50+
# Parametrize tests using the existing manifest loader if the test
51+
# function needs a `manifest_test` argument.
52+
if 'manifest_test' not in metafunc.fixturenames:
53+
return
54+
55+
config = metafunc.config
56+
tests_arg = config.getoption('tests') or []
57+
58+
if len(tests_arg):
59+
test_targets = tests_arg
60+
else:
61+
# Default sibling directories used by the original runner
62+
sibling_dirs = [
63+
'../specifications/json-ld-api/tests/',
64+
'../specifications/json-ld-framing/tests/',
65+
'../specifications/normalization/tests/',
66+
]
67+
test_targets = []
68+
for d in sibling_dirs:
69+
if os.path.exists(d):
70+
test_targets.append(d)
71+
72+
if len(test_targets) == 0:
73+
pytest.skip('No test manifest or directory specified (use --tests)')
74+
75+
# Build a root manifest structure like the original runner did.
76+
root_manifest = {
77+
'@context': 'https://w3c.github.io/tests/context.jsonld',
78+
'@id': '',
79+
'@type': 'mf:Manifest',
80+
'description': 'Top level PyLD test manifest',
81+
'name': 'PyLD',
82+
'sequence': [],
83+
'filename': '/'
84+
}
85+
86+
for test in test_targets:
87+
if os.path.isfile(test):
88+
root, ext = os.path.splitext(test)
89+
if ext in ['.json', '.jsonld']:
90+
root_manifest['sequence'].append(os.path.abspath(test))
91+
else:
92+
raise Exception('Unknown test file ext', root, ext)
93+
elif os.path.isdir(test):
94+
filename = os.path.join(test, 'manifest.jsonld')
95+
if os.path.exists(filename):
96+
root_manifest['sequence'].append(os.path.abspath(filename))
97+
98+
# Use the existing Manifest loader to create a TestSuite and flatten it
99+
suite = runtests.Manifest(root_manifest, root_manifest['filename']).load()
100+
tests = list(_flatten_suite(suite))
101+
102+
# Parametrize the test function with Test instances and use their
103+
# string representation as test ids for readability in pytest output.
104+
metafunc.parametrize('manifest_test', tests, ids=[str(t) for t in tests])
105+
106+
107+
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
108+
def pytest_runtest_makereport(item, call):
109+
# Hookwrapper gives us the final test report via `outcome.get_result()`.
110+
outcome = yield
111+
rep = outcome.get_result()
112+
# We handle failures/errors that occur during setup as well as the
113+
# main call phase so EARL reflects tests that fail before the test
114+
# body runs. We intentionally do not record skipped tests to match
115+
# the behaviour of the original runner which only reported passes
116+
# and failures/errors.
117+
if rep.when not in ('call'):
118+
return
119+
120+
# The parametrized pytest test attaches the original runtests.Test
121+
# instance as the `manifest_test` fixture; retrieve it here.
122+
manifest_test = item.funcargs.get('manifest_test')
123+
if manifest_test is None:
124+
return
125+
126+
# If an EARL report was requested at configure time, add an assertion
127+
# for this test based on the pytest outcome.
128+
earl_report = getattr(item.config, '_earl_report', None)
129+
if earl_report is None:
130+
return
131+
132+
# Map pytest outcomes to whether the test should be recorded as
133+
# succeeded or failed. We skip 'skipped' outcomes to avoid polluting
134+
# the EARL report with non-asserted tests.
135+
if rep.outcome == 'skipped':
136+
return
137+
138+
success = (rep.outcome == 'passed')
139+
try:
140+
earl_report.add_assertion(manifest_test, success)
141+
except Exception:
142+
# Don't let EARL bookkeeping break test execution; be quiet on error.
143+
pass
144+
145+
146+
def pytest_sessionfinish(session, exitstatus):
147+
# If the user requested an EARL report, write it using the existing
148+
# `EarlReport` helper. We can't collect per-test assertions here
149+
# The per-test assertions (if any) were appended to config._earl_report
150+
# during test execution; write the report now if present.
151+
earl = session.config.getoption('earl')
152+
earl_report = getattr(session.config, '_earl_report', None)
153+
if earl and earl_report is not None:
154+
earl_report.write(os.path.abspath(earl))

tests/test_manifests.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
import unittest
3+
4+
# Reuse the existing Test wrapper from `runtests.py`. The pytest test
5+
# simply calls `setUp()` and `runTest()` on the original Test instance
6+
# so that all existing behavior and comparison logic remains unchanged.
7+
from . import runtests
8+
9+
10+
def _call_test(testcase):
11+
"""Call setUp and runTest on a `runtests.Test` instance.
12+
13+
Convert unittest.SkipTest to pytest.skip so pytest reports skipped
14+
tests correctly.
15+
"""
16+
try:
17+
testcase.setUp()
18+
except unittest.SkipTest as e:
19+
pytest.skip(str(e))
20+
21+
try:
22+
testcase.runTest()
23+
except unittest.SkipTest as e:
24+
pytest.skip(str(e))
25+
26+
27+
def test_manifest_case(manifest_test):
28+
# manifest_test is a `runtests.Test` instance provided by the
29+
# parametrization implemented in `conftest.py`.
30+
_call_test(manifest_test)

0 commit comments

Comments
 (0)