Skip to content

Commit f1bc66b

Browse files
authored
Supporting running a test suite against more than one schema (#768)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent adcafd9 commit f1bc66b

5 files changed

Lines changed: 1241 additions & 47 deletions

File tree

src/test/include/sourcemeta/blaze/test.h

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,17 +90,17 @@ struct SOURCEMETA_BLAZE_TEST_EXPORT TestSuite {
9090
#if defined(_MSC_VER)
9191
#pragma warning(disable : 4251)
9292
#endif
93-
/// The target schema URI or file path
94-
sourcemeta::core::JSON::String target;
93+
/// The target schema URIs or file paths
94+
std::vector<sourcemeta::core::JSON::String> targets;
9595
/// The list of test cases in the suite
9696
std::vector<TestCase> tests;
97+
/// The compiled schema templates for fast validation
98+
std::vector<Template> schemas_fast;
99+
/// The compiled schema templates for exhaustive validation
100+
std::vector<Template> schemas_exhaustive;
97101
#if defined(_MSC_VER)
98102
#pragma warning(default : 4251)
99103
#endif
100-
/// The compiled schema template for fast validation
101-
Template schema_fast;
102-
/// The compiled schema template for exhaustive validation
103-
Template schema_exhaustive;
104104
/// The evaluator instance used for validation
105105
Evaluator evaluator;
106106

@@ -193,7 +193,9 @@ struct SOURCEMETA_BLAZE_TEST_EXPORT TestSuite {
193193
/// sourcemeta::core::schema_walker,
194194
/// sourcemeta::blaze::default_schema_compiler)};
195195
///
196-
/// assert(suite.target == "https://json-schema.org/draft/2020-12/schema");
196+
/// assert(suite.targets.size() == 1);
197+
/// assert(suite.targets.front() ==
198+
/// "https://json-schema.org/draft/2020-12/schema");
197199
/// assert(suite.tests.size() == 2);
198200
/// ```
199201
static auto

src/test/test_parser.cc

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ auto TestSuite::parse(const sourcemeta::core::JSON &document,
101101
TEST_ERROR_IF(!document.defines("target"), tracker,
102102
sourcemeta::core::empty_pointer,
103103
"The test document must contain a `target` property");
104-
TEST_ERROR_IF(!document.at("target").is_string(), tracker,
105-
sourcemeta::core::Pointer{"target"},
106-
"The test document `target` property must be a URI");
104+
TEST_ERROR_IF(!document.at("target").is_string() &&
105+
!document.at("target").is_array(),
106+
tracker, sourcemeta::core::Pointer{"target"},
107+
"The test document `target` property must be a URI or an "
108+
"array of URIs");
107109
TEST_ERROR_IF(!document.defines("tests"), tracker,
108110
sourcemeta::core::empty_pointer,
109111
"The test document must contain a `tests` property");
@@ -113,12 +115,33 @@ auto TestSuite::parse(const sourcemeta::core::JSON &document,
113115

114116
const auto base_path_uri{
115117
sourcemeta::core::URI::from_path(base_path / "test.json")};
116-
sourcemeta::core::URI schema_uri{document.at("target").to_string()};
117-
schema_uri.resolve_from(base_path_uri);
118-
schema_uri.canonicalize();
119118

120119
TestSuite test_suite;
121-
test_suite.target = schema_uri.recompose();
120+
121+
if (document.at("target").is_string()) {
122+
sourcemeta::core::URI schema_uri{document.at("target").to_string()};
123+
schema_uri.resolve_from(base_path_uri);
124+
schema_uri.canonicalize();
125+
test_suite.targets.push_back(schema_uri.recompose());
126+
} else {
127+
TEST_ERROR_IF(document.at("target").empty(), tracker,
128+
sourcemeta::core::Pointer{"target"},
129+
"The test document `target` array must contain at least "
130+
"one URI");
131+
// TODO(C++23): Use std::views::enumerate when available in libc++
132+
std::size_t target_index{0};
133+
for (const auto &target_entry : document.at("target").as_array()) {
134+
const sourcemeta::core::Pointer target_location{"target", target_index};
135+
TEST_ERROR_IF(!target_entry.is_string(), tracker, target_location,
136+
"Each entry in the test document `target` array must be "
137+
"a URI");
138+
sourcemeta::core::URI schema_uri{target_entry.to_string()};
139+
schema_uri.resolve_from(base_path_uri);
140+
schema_uri.canonicalize();
141+
test_suite.targets.push_back(schema_uri.recompose());
142+
target_index += 1;
143+
}
144+
}
122145

123146
// TODO(C++23): Use std::views::enumerate when available in libc++
124147
std::size_t index{0};
@@ -131,23 +154,28 @@ auto TestSuite::parse(const sourcemeta::core::JSON &document,
131154
index += 1;
132155
}
133156

134-
const auto target_schema{sourcemeta::core::wrap(test_suite.target)};
135-
136-
try {
137-
test_suite.schema_fast =
138-
compile(target_schema, walker, schema_resolver, compiler,
139-
Mode::FastValidation, default_dialect, default_id, "", tweaks);
140-
test_suite.schema_exhaustive =
141-
compile(target_schema, walker, schema_resolver, compiler,
142-
Mode::Exhaustive, default_dialect, default_id, "", tweaks);
143-
} catch (const sourcemeta::core::SchemaReferenceError &error) {
144-
if (error.location() == sourcemeta::core::Pointer{"$ref"} &&
145-
error.identifier() == test_suite.target) {
146-
throw sourcemeta::core::SchemaResolutionError{
147-
test_suite.target, "Could not resolve schema under test"};
157+
test_suite.schemas_fast.reserve(test_suite.targets.size());
158+
test_suite.schemas_exhaustive.reserve(test_suite.targets.size());
159+
160+
for (const auto &target : test_suite.targets) {
161+
const auto target_schema{sourcemeta::core::wrap(target)};
162+
163+
try {
164+
test_suite.schemas_fast.push_back(compile(
165+
target_schema, walker, schema_resolver, compiler,
166+
Mode::FastValidation, default_dialect, default_id, "", tweaks));
167+
test_suite.schemas_exhaustive.push_back(
168+
compile(target_schema, walker, schema_resolver, compiler,
169+
Mode::Exhaustive, default_dialect, default_id, "", tweaks));
170+
} catch (const sourcemeta::core::SchemaReferenceError &error) {
171+
if (error.location() == sourcemeta::core::Pointer{"$ref"} &&
172+
error.identifier() == target) {
173+
throw sourcemeta::core::SchemaResolutionError{
174+
target, "Could not resolve schema under test"};
175+
}
176+
177+
throw;
148178
}
149-
150-
throw;
151179
}
152180

153181
return test_suite;

src/test/test_runner.cc

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@
55
namespace sourcemeta::blaze {
66

77
auto TestSuite::run(const Callback &callback) -> Result {
8-
Result result{.total = this->tests.size(),
8+
const auto total{this->targets.size() * this->tests.size()};
9+
Result result{.total = total,
910
.passed = 0,
1011
.start = std::chrono::steady_clock::now(),
1112
.end = {}};
12-
// TODO(C++23): Use std::views::enumerate when available in libc++
13-
for (std::size_t index = 0; index < this->tests.size(); ++index) {
14-
const auto &test_case = this->tests[index];
15-
const auto start{std::chrono::steady_clock::now()};
16-
const auto actual{
17-
this->evaluator.validate(this->schema_fast, test_case.data)};
18-
const auto end{std::chrono::steady_clock::now()};
19-
callback(this->target, index + 1, this->tests.size(), test_case, actual,
20-
start, end);
21-
if (test_case.valid == actual) {
22-
result.passed += 1;
13+
14+
std::size_t step{0};
15+
for (std::size_t target_index = 0; target_index < this->targets.size();
16+
++target_index) {
17+
const auto &target = this->targets[target_index];
18+
const auto &schema_fast = this->schemas_fast[target_index];
19+
for (const auto &test_case : this->tests) {
20+
const auto start{std::chrono::steady_clock::now()};
21+
const auto actual{this->evaluator.validate(schema_fast, test_case.data)};
22+
const auto end{std::chrono::steady_clock::now()};
23+
step += 1;
24+
callback(target, step, total, test_case, actual, start, end);
25+
if (test_case.valid == actual) {
26+
result.passed += 1;
27+
}
2328
}
2429
}
2530

0 commit comments

Comments
 (0)