Skip to content

Commit c22e921

Browse files
committed
Fill in the missing coverage, and ensure it runs in CI.
Duplicates some tests from the official suite in order to get to 100%, which I still haven't spent time figuring how to represent in the referencing-suite, so they're just here to exercise the code fully internally without relying on jsonschema's test suite doing so via the official suite.
1 parent d28493a commit c22e921

File tree

7 files changed

+218
-21
lines changed

7 files changed

+218
-21
lines changed

.github/workflows/ci.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ jobs:
4747
fail-fast: false
4848
matrix:
4949
noxenv: ${{ fromJson(needs.list.outputs.noxenvs) }}
50+
include:
51+
- os: ubuntu-latest
52+
noxenv: "tests-3.11"
53+
posargs: ghcoverage
5054

5155
steps:
5256
- uses: actions/checkout@v3
@@ -65,7 +69,7 @@ jobs:
6569
- name: Set up nox
6670
uses: wntrblm/[email protected]
6771
- name: Run nox
68-
run: nox -s "${{ matrix.noxenv }}"
72+
run: nox -s "${{ matrix.noxenv }}" -- ${{ matrix.posargs }}
6973

7074
packaging:
7175
needs: ci

pyproject.toml

+10
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,23 @@ Issues = "https://github.com/python-jsonschema/referencing/issues/"
4444
Funding = "https://github.com/sponsors/Julian"
4545
Source = "https://github.com/python-jsonschema/referencing"
4646

47+
[tool.coverage.html]
48+
show_contexts = true
49+
skip_covered = false
50+
4751
[tool.coverage.run]
4852
branch = true
4953
source = ["referencing"]
5054
dynamic_context = "test_function"
5155

5256
[tool.coverage.report]
57+
exclude_also = [
58+
"if TYPE_CHECKING:",
59+
"\\s*\\.\\.\\.\\s*",
60+
]
5361
fail_under = 100
62+
show_missing = true
63+
skip_covered = true
5464

5565
[tool.doc8]
5666
ignore = [

referencing/jsonschema.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,7 @@ def _subresources_of_with_crazy_aP_items_dependencies(
233233
Specifically handle even older drafts where there are some funky keywords.
234234
"""
235235

236-
def subresources_of(contents: Schema) -> Iterable[ObjectSchema]:
237-
if isinstance(contents, bool):
238-
return
236+
def subresources_of(contents: ObjectSchema) -> Iterable[ObjectSchema]:
239237
for each in in_value:
240238
if each in contents:
241239
yield contents[each]

referencing/tests/test_core.py

+29-12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
)
2525

2626

27+
def blow_up(uri): # pragma: no cover
28+
"""
29+
A retriever suitable for use in tests which expect it never to be used.
30+
"""
31+
raise RuntimeError("This retrieve function expects to never be called!")
32+
33+
2734
class TestRegistry:
2835
def test_with_resource(self):
2936
"""
@@ -317,7 +324,7 @@ def test_combine_with_single_retrieve(self):
317324
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
318325
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
319326

320-
def retrieve(uri):
327+
def retrieve(uri): # pragma: no cover
321328
pass
322329

323330
first = Registry().with_resource("http://example.com/1", one)
@@ -350,7 +357,7 @@ def test_combine_with_common_retrieve(self):
350357
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
351358
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
352359

353-
def retrieve(uri):
360+
def retrieve(uri): # pragma: no cover
354361
pass
355362

356363
first = Registry(retrieve=retrieve).with_resource(
@@ -389,10 +396,10 @@ def test_combine_conflicting_retrieve(self):
389396
two = ID_AND_CHILDREN.create_resource({"foo": "bar"})
390397
three = ID_AND_CHILDREN.create_resource({"baz": "quux"})
391398

392-
def foo_retrieve(uri):
399+
def foo_retrieve(uri): # pragma: no cover
393400
pass
394401

395-
def bar_retrieve(uri):
402+
def bar_retrieve(uri): # pragma: no cover
396403
pass
397404

398405
first = Registry(retrieve=foo_retrieve).with_resource(
@@ -490,21 +497,15 @@ def retrieve(uri):
490497
registry.get_or_retrieve("urn:uhoh")
491498

492499
def test_retrieve_already_available_resource(self):
493-
def retrieve(uri):
494-
raise Exception("Oh no!")
495-
496500
foo = Resource.opaque({"foo": "bar"})
497-
registry = Registry({"urn:example": foo})
501+
registry = Registry({"urn:example": foo}, retrieve=blow_up)
498502
assert registry["urn:example"] == foo
499503
assert registry.get_or_retrieve("urn:example").value == foo
500504

501505
def test_retrieve_first_checks_crawlable_resource(self):
502-
def retrieve(uri):
503-
raise Exception("Oh no!")
504-
505506
child = ID_AND_CHILDREN.create_resource({"ID": "urn:child", "foo": 12})
506507
root = ID_AND_CHILDREN.create_resource({"children": [child.contents]})
507-
registry = Registry(retrieve=retrieve).with_resource("urn:root", root)
508+
registry = Registry(retrieve=blow_up).with_resource("urn:root", root)
508509
assert registry.crawl()["urn:child"] == child
509510

510511
def test_resolver(self):
@@ -765,6 +766,7 @@ def test_lookup_non_existent_anchor(self):
765766
ref = "urn:example#noSuchAnchor"
766767
with pytest.raises(exceptions.Unresolvable) as e:
767768
resolver.lookup(ref)
769+
assert "'noSuchAnchor' does not exist" in str(e.value)
768770
assert e.value == exceptions.NoSuchAnchor(
769771
ref="urn:example",
770772
resource=root,
@@ -794,6 +796,21 @@ def test_lookup_retrieved_resource(self):
794796
resolved = resolver.lookup("http://example.com/")
795797
assert resolved.contents == resource.contents
796798

799+
def test_lookup_failed_retrieved_resource(self):
800+
"""
801+
Unretrievable exceptions are also wrapped in Unresolvable.
802+
"""
803+
804+
uri = "http://example.com/"
805+
806+
registry = Registry(retrieve=blow_up)
807+
with pytest.raises(exceptions.Unretrievable):
808+
registry.get_or_retrieve(uri)
809+
810+
resolver = registry.resolver()
811+
with pytest.raises(exceptions.Unresolvable):
812+
resolver.lookup(uri)
813+
797814
def test_repeated_lookup_from_retrieved_resource(self):
798815
"""
799816
A (custom-)retrieved resource is added to the registry returned by

referencing/tests/test_exceptions.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import itertools
2+
3+
import pytest
4+
5+
from referencing import Resource, exceptions
6+
7+
8+
def pairs(*choices):
9+
return itertools.combinations(choices, 2)
10+
11+
12+
@pytest.mark.parametrize(
13+
"one, two",
14+
pairs(
15+
exceptions.NoSuchResource("urn:example:foo"),
16+
exceptions.NoInternalID(Resource.opaque({})),
17+
exceptions.Unresolvable("urn:example:foo"),
18+
),
19+
)
20+
def test_eq_incompatible_types(one, two):
21+
assert one != two

referencing/tests/test_jsonschema.py

+148-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,109 @@ def test_specification_with_default():
181181
assert specification is Specification.OPAQUE
182182

183183

184-
# FIXME: These two also ideally should live in the referencing suite.
184+
# FIXME: The tests below should move to the referencing suite but I haven't yet
185+
# figured out how to represent dynamic (& recursive) ref lookups in it.
186+
def test_lookup_trivial_dynamic_ref():
187+
one = referencing.jsonschema.DRAFT202012.create_resource(
188+
{"$dynamicAnchor": "foo"},
189+
)
190+
resolver = Registry().with_resource("http://example.com", one).resolver()
191+
resolved = resolver.lookup("http://example.com#foo")
192+
assert resolved.contents == one.contents
193+
194+
195+
def test_multiple_lookup_trivial_dynamic_ref():
196+
TRUE = referencing.jsonschema.DRAFT202012.create_resource(True)
197+
root = referencing.jsonschema.DRAFT202012.create_resource(
198+
{
199+
"$id": "http://example.com",
200+
"$dynamicAnchor": "fooAnchor",
201+
"$defs": {
202+
"foo": {
203+
"$id": "foo",
204+
"$dynamicAnchor": "fooAnchor",
205+
"$defs": {
206+
"bar": True,
207+
"baz": {
208+
"$dynamicAnchor": "fooAnchor",
209+
},
210+
},
211+
},
212+
},
213+
},
214+
)
215+
resolver = (
216+
Registry()
217+
.with_resources(
218+
[
219+
("http://example.com", root),
220+
("http://example.com/foo/", TRUE),
221+
("http://example.com/foo/bar", root),
222+
],
223+
)
224+
.resolver()
225+
)
226+
227+
first = resolver.lookup("http://example.com")
228+
second = first.resolver.lookup("foo/")
229+
resolver = second.resolver.lookup("bar").resolver
230+
fourth = resolver.lookup("#fooAnchor")
231+
assert fourth.contents == root.contents
232+
233+
234+
def test_multiple_lookup_dynamic_ref_to_nondynamic_ref():
235+
one = referencing.jsonschema.DRAFT202012.create_resource(
236+
{"$anchor": "fooAnchor"},
237+
)
238+
two = referencing.jsonschema.DRAFT202012.create_resource(
239+
{
240+
"$id": "http://example.com",
241+
"$dynamicAnchor": "fooAnchor",
242+
"$defs": {
243+
"foo": {
244+
"$id": "foo",
245+
"$dynamicAnchor": "fooAnchor",
246+
"$defs": {
247+
"bar": True,
248+
"baz": {
249+
"$dynamicAnchor": "fooAnchor",
250+
},
251+
},
252+
},
253+
},
254+
},
255+
)
256+
resolver = (
257+
Registry()
258+
.with_resources(
259+
[
260+
("http://example.com", two),
261+
("http://example.com/foo/", one),
262+
("http://example.com/foo/bar", two),
263+
],
264+
)
265+
.resolver()
266+
)
267+
268+
first = resolver.lookup("http://example.com")
269+
second = first.resolver.lookup("foo/")
270+
resolver = second.resolver.lookup("bar").resolver
271+
fourth = resolver.lookup("#fooAnchor")
272+
assert fourth.contents == two.contents
273+
274+
275+
def test_lookup_trivial_recursive_ref():
276+
one = referencing.jsonschema.DRAFT201909.create_resource(
277+
{"$recursiveAnchor": True},
278+
)
279+
resolver = Registry().with_resource("http://example.com", one).resolver()
280+
first = resolver.lookup("http://example.com")
281+
resolved = referencing.jsonschema.lookup_recursive_ref(
282+
resolver=first.resolver,
283+
)
284+
assert resolved.contents == one.contents
285+
286+
185287
def test_lookup_recursive_ref_to_bool():
186288
TRUE = referencing.jsonschema.DRAFT201909.create_resource(True)
187289
registry = Registry({"http://example.com": TRUE})
@@ -229,3 +331,48 @@ def test_multiple_lookup_recursive_ref_to_bool():
229331
resolver = second.resolver.lookup("bar").resolver
230332
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
231333
assert fourth.contents == root.contents
334+
335+
336+
def test_multiple_lookup_recursive_ref_with_nonrecursive_ref():
337+
one = referencing.jsonschema.DRAFT201909.create_resource(
338+
{"$recursiveAnchor": True},
339+
)
340+
two = referencing.jsonschema.DRAFT201909.create_resource(
341+
{
342+
"$id": "http://example.com",
343+
"$recursiveAnchor": True,
344+
"$defs": {
345+
"foo": {
346+
"$id": "foo",
347+
"$recursiveAnchor": True,
348+
"$defs": {
349+
"bar": True,
350+
"baz": {
351+
"$recursiveAnchor": True,
352+
"$anchor": "fooAnchor",
353+
},
354+
},
355+
},
356+
},
357+
},
358+
)
359+
three = referencing.jsonschema.DRAFT201909.create_resource(
360+
{"$recursiveAnchor": False},
361+
)
362+
resolver = (
363+
Registry()
364+
.with_resources(
365+
[
366+
("http://example.com", three),
367+
("http://example.com/foo/", two),
368+
("http://example.com/foo/bar", one),
369+
],
370+
)
371+
.resolver()
372+
)
373+
374+
first = resolver.lookup("http://example.com")
375+
second = first.resolver.lookup("foo/")
376+
resolver = second.resolver.lookup("bar").resolver
377+
fourth = referencing.jsonschema.lookup_recursive_ref(resolver=resolver)
378+
assert fourth.contents == two.contents

referencing/tests/test_referencing_suite.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class SuiteNotFound(Exception):
13-
def __str__(self):
13+
def __str__(self): # pragma: no cover
1414
return (
1515
"Cannot find the referencing suite. "
1616
"Set the REFERENCING_SUITE environment variable to the path to "
@@ -19,11 +19,11 @@ def __str__(self):
1919
)
2020

2121

22-
if "REFERENCING_SUITE" in os.environ:
22+
if "REFERENCING_SUITE" in os.environ: # pragma: no cover
2323
SUITE = Path(os.environ["REFERENCING_SUITE"]) / "tests"
2424
else:
2525
SUITE = Path(__file__).parent.parent.parent / "suite/tests"
26-
if not SUITE.is_dir():
26+
if not SUITE.is_dir(): # pragma: no cover
2727
raise SuiteNotFound()
2828
DIALECT_IDS = json.loads(SUITE.joinpath("specifications.json").read_text())
2929

@@ -56,7 +56,7 @@ def test_referencing_suite(test_path, subtests):
5656
assert resolved.contents == test["target"]
5757

5858
then = test.get("then")
59-
while then:
59+
while then: # pragma: no cover
6060
with subtests.test(test=test, then=then):
6161
resolved = resolved.resolver.lookup(then["ref"])
6262
assert resolved.contents == then["target"]

0 commit comments

Comments
 (0)