11#include < sourcemeta/blaze/output.h>
22#include < sourcemeta/blaze/test.h>
33
4+ #include < sourcemeta/core/json.h>
5+
6+ #include < chrono> // std::chrono
47#include < cstdlib> // EXIT_FAILURE
58#include < iostream> // std::cerr, std::cout
9+ #include < sstream> // std::ostringstream
610#include < string> // std::string
11+ #include < thread> // std::this_thread
12+ #include < vector> // std::vector
713
814#include " command.h"
915#include " configuration.h"
16+ #include " configure.h"
1017#include " error.h"
1118#include " input.h"
1219#include " logger.h"
1320#include " resolver.h"
1421#include " utils.h"
1522
16- auto sourcemeta::jsonschema::test (const sourcemeta::core::Options &options)
17- -> void {
18- bool result{true };
19-
20- const auto verbose{options.contains (" verbose" )};
23+ namespace {
2124
22- for (const auto &entry : for_each_json (options)) {
23- const auto configuration_path{find_configuration (entry.first )};
24- const auto &configuration{read_configuration (options, configuration_path)};
25- const auto dialect{default_dialect (options, configuration)};
26-
27- const auto &schema_resolver{
28- resolver (options, options.contains (" http" ), dialect, configuration)};
29-
30- std::optional<sourcemeta::blaze::TestSuite> test_suite;
31- try {
32- test_suite.emplace (sourcemeta::blaze::TestSuite::parse (
33- entry.second , entry.positions , entry.first .parent_path (),
34- schema_resolver, sourcemeta::core::schema_walker,
35- sourcemeta::blaze::default_schema_compiler, dialect));
36- } catch (const sourcemeta::blaze::TestParseError &error) {
25+ auto parse_test_suite (const sourcemeta::jsonschema::InputJSON &entry,
26+ const sourcemeta::core::SchemaResolver &schema_resolver,
27+ const std::optional<std::string> &dialect,
28+ const bool json_output) -> sourcemeta::blaze::TestSuite {
29+ try {
30+ return sourcemeta::blaze::TestSuite::parse (
31+ entry.second , entry.positions , entry.first .parent_path (),
32+ schema_resolver, sourcemeta::core::schema_walker,
33+ sourcemeta::blaze::default_schema_compiler, dialect);
34+ } catch (const sourcemeta::blaze::TestParseError &error) {
35+ if (!json_output) {
3736 std::cout << entry.first .string () << " :\n " ;
38- throw FileError<sourcemeta::blaze::TestParseError>{
39- entry.first , error.what (), error.location (), error.line (),
40- error.column ()};
41- } catch (const sourcemeta::core::SchemaRelativeMetaschemaResolutionError
42- &error) {
37+ }
38+ throw sourcemeta::jsonschema::FileError<sourcemeta::blaze::TestParseError>{
39+ entry.first , error.what (), error.location (), error.line (),
40+ error.column ()};
41+ } catch (
42+ const sourcemeta::core::SchemaRelativeMetaschemaResolutionError &error) {
43+ if (!json_output) {
4344 std::cout << entry.first .string () << " :\n " ;
44- throw FileError<
45- sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{
46- entry.first , error};
47- } catch (const sourcemeta::core::SchemaResolutionError &error) {
45+ }
46+ throw sourcemeta::jsonschema::FileError<
47+ sourcemeta::core::SchemaRelativeMetaschemaResolutionError>{entry.first ,
48+ error};
49+ } catch (const sourcemeta::core::SchemaResolutionError &error) {
50+ if (!json_output) {
4851 std::cout << entry.first .string () << " :\n " ;
49- throw FileError<sourcemeta::core::SchemaResolutionError>{entry.first ,
50- error};
51- } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) {
52+ }
53+ throw sourcemeta::jsonschema::FileError<
54+ sourcemeta::core::SchemaResolutionError>{entry.first , error};
55+ } catch (const sourcemeta::core::SchemaUnknownBaseDialectError &) {
56+ if (!json_output) {
5257 std::cout << entry.first .string () << " :\n " ;
53- throw FileError<sourcemeta::core::SchemaUnknownBaseDialectError>{
54- entry.first };
55- } catch (...) {
58+ }
59+ throw sourcemeta::jsonschema::FileError<
60+ sourcemeta::core::SchemaUnknownBaseDialectError>{entry.first };
61+ } catch (...) {
62+ if (!json_output) {
5663 std::cout << entry.first .string () << " :\n " ;
57- throw ;
5864 }
65+ throw ;
66+ }
67+ }
68+
69+ auto report_as_text (const sourcemeta::core::Options &options) -> void {
70+ bool result{true };
71+ const auto verbose{options.contains (" verbose" )};
72+
73+ for (const auto &entry : sourcemeta::jsonschema::for_each_json (options)) {
74+ const auto configuration_path{
75+ sourcemeta::jsonschema::find_configuration (entry.first )};
76+ const auto &configuration{sourcemeta::jsonschema::read_configuration (
77+ options, configuration_path)};
78+ const auto dialect{
79+ sourcemeta::jsonschema::default_dialect (options, configuration)};
80+ const auto &schema_resolver{sourcemeta::jsonschema::resolver (
81+ options, options.contains (" http" ), dialect, configuration)};
82+
83+ auto test_suite{parse_test_suite (entry, schema_resolver, dialect, false )};
5984
6085 std::cout << entry.first .string () << " :" ;
6186
62- const auto suite_result{test_suite-> run (
87+ const auto suite_result{test_suite. run (
6388 [&](const sourcemeta::core::JSON::String &, std::size_t index,
6489 std::size_t total, const sourcemeta::blaze::TestCase &test_case,
6590 bool actual, sourcemeta::blaze::TestTimestamp,
@@ -90,20 +115,19 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options)
90115 std::cout << " \n " ;
91116 }
92117 } else {
93- // Re-run with exhaustive mode to get detailed error output
94118 const std::string ref{" $ref" };
95119 sourcemeta::blaze::SimpleOutput output{test_case.data ,
96120 {std::cref (ref)}};
97- test_suite-> evaluator .validate (test_suite-> schema_exhaustive ,
98- test_case.data , std::ref (output));
121+ test_suite. evaluator .validate (test_suite. schema_exhaustive ,
122+ test_case.data , std::ref (output));
99123
100124 if (!verbose) {
101125 std::cout << " \n " ;
102126 }
103127
104128 std::cout << " " << index << " /" << total << " FAIL "
105129 << description << " \n\n " ;
106- print (output, test_case.tracker , std::cout);
130+ sourcemeta::jsonschema:: print (output, test_case.tracker , std::cout);
107131
108132 if (index != total && verbose) {
109133 std::cout << " \n " ;
@@ -124,8 +148,174 @@ auto sourcemeta::jsonschema::test(const sourcemeta::core::Options &options)
124148 }
125149
126150 if (!result) {
127- // Report a different exit code for test failures, to
128- // distinguish them from other errors
129- throw Fail{2 };
151+ throw sourcemeta::jsonschema::Fail{2 };
152+ }
153+ }
154+
155+ auto timestamp_to_unix_ms (
156+ const sourcemeta::blaze::TestTimestamp ×tamp,
157+ const std::chrono::system_clock::time_point &system_ref,
158+ const sourcemeta::blaze::TestTimestamp &steady_ref) -> std::int64_t {
159+ const auto offset{timestamp - steady_ref};
160+ const auto unix_time{system_ref + offset};
161+ return std::chrono::duration_cast<std::chrono::milliseconds>(
162+ unix_time.time_since_epoch ())
163+ .count ();
164+ }
165+
166+ auto duration_ms (const sourcemeta::blaze::TestTimestamp &start,
167+ const sourcemeta::blaze::TestTimestamp &end) -> std::int64_t {
168+ return std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
169+ .count ();
170+ }
171+
172+ auto report_as_ctrf (const sourcemeta::core::Options &options) -> void {
173+ bool result{true };
174+
175+ const auto system_ref{std::chrono::system_clock::now ()};
176+ const auto steady_ref{std::chrono::steady_clock::now ()};
177+
178+ auto ctrf_tests{sourcemeta::core::JSON::make_array ()};
179+ std::size_t total_passed{0 };
180+ std::size_t total_failed{0 };
181+ sourcemeta::blaze::TestTimestamp global_start{};
182+ sourcemeta::blaze::TestTimestamp global_end{};
183+ bool first_suite{true };
184+
185+ for (const auto &entry : sourcemeta::jsonschema::for_each_json (options)) {
186+ const auto configuration_path{
187+ sourcemeta::jsonschema::find_configuration (entry.first )};
188+ const auto &configuration{sourcemeta::jsonschema::read_configuration (
189+ options, configuration_path)};
190+ const auto dialect{
191+ sourcemeta::jsonschema::default_dialect (options, configuration)};
192+ const auto &schema_resolver{sourcemeta::jsonschema::resolver (
193+ options, options.contains (" http" ), dialect, configuration)};
194+
195+ auto test_suite{parse_test_suite (entry, schema_resolver, dialect, true )};
196+
197+ const auto file_path{
198+ sourcemeta::core::weakly_canonical (entry.first ).string ()};
199+
200+ const auto suite_result{test_suite.run (
201+ [&](const sourcemeta::core::JSON::String &target, std::size_t ,
202+ std::size_t , const sourcemeta::blaze::TestCase &test_case,
203+ bool actual, sourcemeta::blaze::TestTimestamp start,
204+ sourcemeta::blaze::TestTimestamp end) {
205+ auto test_object{sourcemeta::core::JSON::make_object ()};
206+
207+ const auto &name{test_case.description .empty ()
208+ ? " <no description>"
209+ : test_case.description };
210+ test_object.assign (" name" , sourcemeta::core::JSON{name});
211+
212+ const bool passed{test_case.valid == actual};
213+ test_object.assign (
214+ " status" , sourcemeta::core::JSON{passed ? " passed" : " failed" });
215+
216+ test_object.assign (" duration" ,
217+ sourcemeta::core::JSON{duration_ms (start, end)});
218+ auto suite{sourcemeta::core::JSON::make_array ()};
219+ suite.push_back (sourcemeta::core::JSON{target});
220+ test_object.assign (" suite" , std::move (suite));
221+ test_object.assign (" type" , sourcemeta::core::JSON{" unit" });
222+ test_object.assign (" filePath" , sourcemeta::core::JSON{file_path});
223+
224+ test_object.assign (" line" ,
225+ sourcemeta::core::JSON{static_cast <std::int64_t >(
226+ std::get<0 >(test_case.position ))});
227+ test_object.assign (
228+ " retries" , sourcemeta::core::JSON{static_cast <std::int64_t >(0 )});
229+ test_object.assign (" flaky" , sourcemeta::core::JSON{false });
230+ std::ostringstream thread_id_stream;
231+ thread_id_stream << std::this_thread::get_id ();
232+ test_object.assign (" threadId" ,
233+ sourcemeta::core::JSON{thread_id_stream.str ()});
234+
235+ if (!passed) {
236+ if (!test_case.valid && actual) {
237+ test_object.assign (" message" ,
238+ sourcemeta::core::JSON{" Passed but was "
239+ " expected to fail" });
240+ } else {
241+ std::ostringstream trace_stream;
242+ const std::string ref{" $ref" };
243+ sourcemeta::blaze::SimpleOutput output{test_case.data ,
244+ {std::cref (ref)}};
245+ test_suite.evaluator .validate (test_suite.schema_exhaustive ,
246+ test_case.data , std::ref (output));
247+ sourcemeta::jsonschema::print (output, test_case.tracker ,
248+ trace_stream);
249+ test_object.assign (" trace" ,
250+ sourcemeta::core::JSON{trace_stream.str ()});
251+ }
252+ }
253+
254+ ctrf_tests.push_back (test_object);
255+ })};
256+
257+ if (first_suite) {
258+ global_start = suite_result.start ;
259+ first_suite = false ;
260+ }
261+ global_end = suite_result.end ;
262+
263+ total_passed += suite_result.passed ;
264+ total_failed += suite_result.total - suite_result.passed ;
265+
266+ if (suite_result.passed != suite_result.total ) {
267+ result = false ;
268+ }
269+ }
270+
271+ // Build CTRF output
272+ auto summary{sourcemeta::core::JSON::make_object ()};
273+ summary.assign (" tests" , sourcemeta::core::JSON{static_cast <std::int64_t >(
274+ total_passed + total_failed)});
275+ summary.assign (" passed" , sourcemeta::core::JSON{
276+ static_cast <std::int64_t >(total_passed)});
277+ summary.assign (" failed" , sourcemeta::core::JSON{
278+ static_cast <std::int64_t >(total_failed)});
279+ summary.assign (" pending" ,
280+ sourcemeta::core::JSON{static_cast <std::int64_t >(0 )});
281+ summary.assign (" skipped" ,
282+ sourcemeta::core::JSON{static_cast <std::int64_t >(0 )});
283+ summary.assign (" other" , sourcemeta::core::JSON{static_cast <std::int64_t >(0 )});
284+ summary.assign (" start" , sourcemeta::core::JSON{timestamp_to_unix_ms (
285+ global_start, system_ref, steady_ref)});
286+ summary.assign (" stop" , sourcemeta::core::JSON{timestamp_to_unix_ms (
287+ global_end, system_ref, steady_ref)});
288+
289+ auto tool{sourcemeta::core::JSON::make_object ()};
290+ tool.assign (" name" , sourcemeta::core::JSON{" jsonschema" });
291+ tool.assign (" version" , sourcemeta::core::JSON{std::string{
292+ sourcemeta::jsonschema::PROJECT_VERSION}});
293+
294+ auto results{sourcemeta::core::JSON::make_object ()};
295+ results.assign (" tool" , std::move (tool));
296+ results.assign (" summary" , std::move (summary));
297+ results.assign (" tests" , std::move (ctrf_tests));
298+
299+ auto ctrf{sourcemeta::core::JSON::make_object ()};
300+ ctrf.assign (" reportFormat" , sourcemeta::core::JSON{" CTRF" });
301+ ctrf.assign (" specVersion" , sourcemeta::core::JSON{" 0.0.0" });
302+ ctrf.assign (" results" , std::move (results));
303+
304+ sourcemeta::core::prettify (ctrf, std::cout);
305+ std::cout << " \n " ;
306+
307+ if (!result) {
308+ throw sourcemeta::jsonschema::Fail{2 };
309+ }
310+ }
311+
312+ } // namespace
313+
314+ auto sourcemeta::jsonschema::test (const sourcemeta::core::Options &options)
315+ -> void {
316+ if (options.contains (" json" )) {
317+ report_as_ctrf (options);
318+ } else {
319+ report_as_text (options);
130320 }
131321}
0 commit comments