Skip to content

Commit 551e2ce

Browse files
committed
test: Use in-memory resolvers for JSON Schema test suite
Signed-off-by: Dmitry Dygalo <[email protected]>
1 parent 32c20ae commit 551e2ce

File tree

8 files changed

+160
-117
lines changed

8 files changed

+160
-117
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ jobs:
110110
111111

112112
- name: Run tests
113+
env:
114+
JSONSCHEMA_SUITE_DRAFT_FILTER: draft2020-12
113115
run: |
114116
if [ -n "${{ matrix.features }}" ]; then
115117
cargo test --target ${{ matrix.target }} --no-default-features --features "${{ matrix.features }}" -p jsonschema

crates/jsonschema-py/tests-py/test_suite.py

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,47 @@
11
import json
22
import os
3-
import random
4-
import socket
5-
import subprocess
6-
import sys
73
from pathlib import Path
8-
from time import sleep
9-
from urllib.parse import urlparse
104

115
import pytest
126

137
import jsonschema_rs
148

159
TEST_SUITE_PATH = Path(__file__).parent.parent.parent / "jsonschema/tests/suite"
16-
EXPONENTIAL_BASE = 2
17-
JITTER = (0.0, 0.5)
18-
INITIAL_RETRY_DELAY = 0.05
19-
MAX_WAITING_RETRIES = 10
10+
REMOTE_PREFIXES = ("http://localhost:1234", "https://localhost:1234")
2011

2112

22-
def is_available(url: str) -> bool:
23-
"""Whether the `url` is available for connection or not."""
24-
parsed = urlparse(url)
25-
try:
26-
with socket.create_connection((parsed.hostname, parsed.port or 80)):
27-
return True
28-
except ConnectionError:
29-
return False
30-
31-
32-
def wait_until_responsive(url: str, retries: int = MAX_WAITING_RETRIES, delay: float = INITIAL_RETRY_DELAY) -> None:
33-
while retries > 0:
34-
if is_available(url):
35-
return
36-
retries -= 1
37-
delay *= EXPONENTIAL_BASE
38-
delay += random.uniform(*JITTER)
39-
sleep(delay)
40-
raise RuntimeError(f"{url} is not available")
41-
42-
43-
@pytest.fixture(scope="session", autouse=True)
44-
def mock_server():
45-
process = subprocess.Popen(args=[sys.executable, TEST_SUITE_PATH / "bin/jsonschema_suite", "serve"])
46-
wait_until_responsive("http://127.0.0.1:1234")
47-
try:
48-
yield
49-
finally:
50-
process.terminate()
13+
def load_remote_documents():
14+
remotes = TEST_SUITE_PATH / "remotes"
15+
documents = {}
16+
for root, _, files in os.walk(remotes):
17+
for filename in files:
18+
path = Path(root) / filename
19+
relative = path.relative_to(remotes).as_posix()
20+
contents = path.read_text(encoding="utf-8")
21+
for prefix in REMOTE_PREFIXES:
22+
documents[f"{prefix}/{relative}"] = contents
23+
return documents
24+
25+
26+
REMOTE_DOCUMENTS = load_remote_documents()
27+
28+
29+
def make_testsuite_retriever():
30+
localhost_alias = "http://127.0.0.1:1234/"
31+
32+
def _retriever(uri: str):
33+
normalized = uri
34+
if normalized.startswith(localhost_alias):
35+
normalized = f"http://localhost:1234/{normalized[len(localhost_alias):]}"
36+
try:
37+
return json.loads(REMOTE_DOCUMENTS[normalized])
38+
except KeyError as exc:
39+
raise ValueError(f"Unknown remote schema: {uri}") from exc
40+
41+
return _retriever
42+
43+
44+
TESTSUITE_RETRIEVER = make_testsuite_retriever()
5145

5246

5347
SUPPORTED_DRAFTS = ("4", "6", "7", "2019-09", "2020-12")
@@ -62,8 +56,7 @@ def mock_server():
6256

6357
def load_file(path):
6458
with open(path, mode="r", encoding="utf-8") as fd:
65-
raw = fd.read().replace("https://localhost:1234", "http://127.0.0.1:1234")
66-
for block in json.loads(raw):
59+
for block in json.load(fd):
6760
yield block
6861

6962

@@ -98,7 +91,7 @@ def test_draft(filename, draft, schema, instance, expected, description, is_opti
9891
"2019-09": jsonschema_rs.Draft201909Validator,
9992
"2020-12": jsonschema_rs.Draft202012Validator,
10093
}[draft]
101-
kwargs = {}
94+
kwargs = {"retriever": TESTSUITE_RETRIEVER}
10295
if is_optional:
10396
kwargs["validate_formats"] = True
10497
result = cls(schema, **kwargs).is_valid(instance)

crates/jsonschema-testsuite-codegen/src/generator.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ fn generate_nested_structure(
7474

7575
quote! {
7676
#ignore_attr
77-
#[test]
77+
#[cfg_attr(not(all(target_arch = "wasm32", target_os = "unknown")), test)]
78+
#[cfg_attr(all(target_arch = "wasm32", target_os = "unknown"), wasm_bindgen_test::wasm_bindgen_test)]
7879
fn #test_ident() {
7980
let test = testsuite::Test {
8081
draft: #draft,

crates/jsonschema-testsuite-codegen/src/lib.rs

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use std::collections::HashSet;
22

33
use proc_macro::TokenStream;
4-
54
use quote::{format_ident, quote};
65
use syn::{parse_macro_input, ItemFn};
76
mod generator;
87
mod idents;
98
mod loader;
10-
mod mocks;
9+
mod remotes;
1110

1211
/// A procedural macro that generates tests from
1312
/// [JSON-Schema-Test-Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite).
@@ -17,8 +16,8 @@ pub fn suite(args: TokenStream, input: TokenStream) -> TokenStream {
1716
let test_func = parse_macro_input!(input as ItemFn);
1817
let test_func_ident = &test_func.sig.ident;
1918

20-
let mocks = match mocks::generate(&config.path) {
21-
Ok(mocks) => mocks,
19+
let remotes = match remotes::generate(&config.path) {
20+
Ok(remotes) => remotes,
2221
Err(e) => {
2322
let err = e.to_string();
2423
return TokenStream::from(quote! {
@@ -30,15 +29,42 @@ pub fn suite(args: TokenStream, input: TokenStream) -> TokenStream {
3029
let mut output = quote! {
3130
#test_func
3231

33-
static MOCK: std::sync::LazyLock<mockito::Server> = std::sync::LazyLock::new(|| {
34-
#mocks
35-
});
32+
#remotes
33+
34+
struct TestsuiteRetriever;
35+
36+
impl jsonschema::Retrieve for TestsuiteRetriever {
37+
fn retrieve(
38+
&self,
39+
uri: &jsonschema::Uri<String>,
40+
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
41+
static REMOTE_MAP: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> =
42+
std::sync::LazyLock::new(|| {
43+
let mut map = std::collections::HashMap::with_capacity(REMOTE_DOCUMENTS.len());
44+
for (uri, contents) in REMOTE_DOCUMENTS {
45+
map.insert(*uri, *contents);
46+
}
47+
map
48+
});
49+
match REMOTE_MAP.get(uri.as_str()) {
50+
Some(contents) => Ok(serde_json::from_str(contents)
51+
.expect("Failed to parse remote schema")),
52+
None => Err(format!("Unknown remote: {}", uri).into()),
53+
}
54+
}
55+
}
56+
57+
fn testsuite_retriever() -> TestsuiteRetriever {
58+
TestsuiteRetriever
59+
}
60+
61+
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
62+
use wasm_bindgen_test::wasm_bindgen_test;
3663
};
3764
// There are a lot of tests in the test suite
3865
let mut functions = HashSet::with_capacity(7200);
39-
40-
for draft in config.drafts {
41-
let suite_tree = match loader::load_suite(&config.path, &draft) {
66+
for draft in &config.drafts {
67+
let suite_tree = match loader::load_suite(&config.path, draft) {
4268
Ok(tree) => tree,
4369
Err(e) => {
4470
let err = e.to_string();
@@ -48,18 +74,17 @@ pub fn suite(args: TokenStream, input: TokenStream) -> TokenStream {
4874
}
4975
};
5076
let modules =
51-
generator::generate_modules(&suite_tree, &mut functions, &config.xfail, &draft);
52-
let draft = format_ident!("{}", &draft.replace('-', "_"));
77+
generator::generate_modules(&suite_tree, &mut functions, &config.xfail, draft);
78+
let module_ident = format_ident!("{}", &draft.replace('-', "_"));
5379
output = quote! {
5480
#output
5581

56-
mod #draft {
82+
mod #module_ident {
5783
use testsuite::Test;
58-
use super::{#test_func_ident, MOCK};
84+
use super::#test_func_ident;
5985

6086
#[inline]
6187
fn inner_test(test: Test) {
62-
let _ = &*MOCK;
6388
#test_func_ident(test);
6489
}
6590
#modules

crates/jsonschema-testsuite-codegen/src/mocks.rs

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
use proc_macro2::TokenStream as TokenStream2;
2+
use quote::quote;
3+
use std::{
4+
fs::read_to_string,
5+
path::{Path, MAIN_SEPARATOR},
6+
};
7+
8+
pub(crate) fn generate(suite_path: &str) -> Result<TokenStream2, Box<dyn std::error::Error>> {
9+
let remotes = Path::new(suite_path).join("remotes");
10+
if !remotes.exists() || !remotes.is_dir() {
11+
return Err(format!(
12+
"Path does not exist or is not a directory: {}. Run `git submodule init && git submodule update`",
13+
remotes.display()
14+
)
15+
.into());
16+
}
17+
18+
let mut resources = Vec::new();
19+
for entry in walkdir::WalkDir::new(&remotes)
20+
.into_iter()
21+
.filter_map(Result::ok)
22+
.filter(|entry| entry.file_type().is_file())
23+
{
24+
let path = entry.path().to_path_buf();
25+
let relative_path = path.strip_prefix(&remotes).expect("Invalid path");
26+
let url_path = relative_path
27+
.to_str()
28+
.expect("Invalid filename")
29+
.replace(MAIN_SEPARATOR, "/");
30+
let uri = format!("http://localhost:1234/{url_path}");
31+
let contents = read_to_string(path).expect("Failed to read a file");
32+
resources.push((uri, contents));
33+
}
34+
35+
resources.sort_by(|(left_uri, _), (right_uri, _)| left_uri.cmp(right_uri));
36+
37+
let entries = resources.iter().map(|(uri, contents)| {
38+
let uri_literal = proc_macro2::Literal::string(uri);
39+
let contents_literal = proc_macro2::Literal::string(contents);
40+
quote! { (#uri_literal, #contents_literal) }
41+
});
42+
43+
Ok(quote! {
44+
static REMOTE_DOCUMENTS: &[(&str, &str)] = &[
45+
#(#entries),*
46+
];
47+
})
48+
}

crates/jsonschema/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ uuid-simd = { version = "0.8", default-features = false, features = ["std", "det
6060
[dev-dependencies]
6161
test-case = "3"
6262
futures = { version = "0.3", default-features = false, features = ["std"] }
63+
testsuite = { package = "jsonschema-testsuite", path = "../jsonschema-testsuite" }
6364

6465
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies]
6566
wasm-bindgen-test = "0.3.55"
@@ -68,10 +69,8 @@ wasm-bindgen-test = "0.3.55"
6869
benchmark = { path = "../benchmark/" }
6970
codspeed-criterion-compat = { version = "4.1", default-features = false }
7071
criterion = { version = "0.7", default-features = false }
71-
mockito = "1.5"
7272
tempfile = "3.13.0"
7373
test-case = "3"
74-
testsuite = { package = "jsonschema-testsuite", path = "../jsonschema-testsuite" }
7574
tokio = { version = "1", features = ["macros", "rt"] }
7675

7776
[lints.clippy]

0 commit comments

Comments
 (0)