File Coverage

blib/lib/Test/JSON/Schema/Acceptance.pm
Criterion Covered Total %
statement 269 286 94.0
branch 110 134 82.0
condition 77 91 84.6
subroutine 43 47 91.4
pod 2 4 50.0
total 501 562 89.1


line stmt bran cond sub pod time code
1 17     17   8175476 use strict;
  17         44  
  17         791  
2 17     17   100 use warnings;
  17         38  
  17         1895  
3             package Test::JSON::Schema::Acceptance; # git description: v1.036-3-ga8fe3f6
4             # vim: set ts=8 sts=2 sw=2 tw=100 et :
5             # ABSTRACT: Acceptance testing for JSON-Schema based validators
6              
7             our $VERSION = '1.037';
8              
9 17     17   361 use 5.020;
  17         69  
10 17     17   12350 use Moo;
  17         169689  
  17         103  
11 17     17   46657 use strictures 2;
  17         215  
  17         833  
12 17     17   9221 use stable 0.031 'postderef';
  17         390  
  17         139  
13 17     17   4090 use experimental 'signatures';
  17         62  
  17         77  
14 17     17   1440 no autovivification warn => qw(fetch store exists delete);
  17         54  
  17         154  
15 17     17   1541 use if "$]" >= 5.022, experimental => 're_strict';
  17         71  
  17         492  
16 17     17   1676 no if "$]" >= 5.031009, feature => 'indirect';
  17         47  
  17         1483  
17 17     17   123 no if "$]" >= 5.033001, feature => 'multidimensional';
  17         45  
  17         1263  
18 17     17   99 no if "$]" >= 5.033006, feature => 'bareword_filehandles';
  17         40  
  17         1228  
19 17     17   131 no if "$]" >= 5.041009, feature => 'smartmatch';
  17         31  
  17         1000  
20 17     17   130 no feature 'switch';
  17         117  
  17         654  
21 17     17   156 use Test2::API ();
  17         70  
  17         503  
22 17     17   9892 use Test2::Todo;
  17         18348  
  17         584  
23 17     17   138 use Test2::Tools::Compare ();
  17         64  
  17         306  
24 17     17   35832 use Mojo::JSON (); # for JSON_XS, MOJO_NO_JSON_XS environment variables
  17         4845714  
  17         879  
25 17     17   5922 use File::ShareDir 'dist_dir';
  17         354716  
  17         1557  
26 17     17   10403 use Feature::Compat::Try;
  17         7236  
  17         78  
27 17     17   10488 use MooX::TypeTiny 0.002002;
  17         7664  
  17         115  
28 17     17   167639 use Types::Standard 1.016003 qw(Str InstanceOf ArrayRef HashRef Dict Any HasMethods Bool Optional Slurpy Enum);
  17         2647737  
  17         330  
29 17     17   96694 use Types::Common::Numeric 'PositiveOrZeroInt';
  17         621809  
  17         299  
30 17     17   29088 use Path::Tiny 0.069;
  17         174353  
  17         2026  
31 17     17   183 use List::Util 1.33 qw(any max sum0);
  17         482  
  17         1684  
32 17     17   14151 use Ref::Util qw(is_plain_arrayref is_plain_hashref is_ref);
  17         13194  
  17         1806  
33 17     17   11121 use Git::Wrapper;
  17         520344  
  17         970  
34 17     17   10948 use namespace::clean;
  17         348517  
  17         137  
35              
36             # specification version => metaschema URI
37 17         95940 use constant METASCHEMA => {
38             'v1' => 'https://json-schema.org/v1',
39             'draft2020-12' => 'https://json-schema.org/draft/2020-12/schema',
40             'draft2019-09' => 'https://json-schema.org/draft/2019-09/schema',
41             'draft7' => 'http://json-schema.org/draft-07/schema#',
42             'draft6' => 'http://json-schema.org/draft-06/schema#',
43             'draft4' => 'http://json-schema.org/draft-04/schema#',
44             'draft3' => 'http://json-schema.org/draft-03/schema#',
45 17     17   11146 };
  17         89  
46              
47             my $spec_type = Enum[sort keys METASCHEMA->%*];
48             has specification => (
49             is => 'ro',
50             isa => $spec_type,
51             lazy => 1,
52             default => 'draft2020-12',
53             predicate => '_has_specification',
54             );
55              
56             has supported_specifications => (
57             is => 'ro',
58             isa => ArrayRef[$spec_type],
59             lazy => 1,
60             default => sub { [ shift->specification ] },
61             );
62              
63             # this comes from the tests/ directories in the JSON-Schema-Test-Suite repository
64             has test_dir => (
65             is => 'ro',
66             isa => InstanceOf['Path::Tiny'],
67             coerce => sub { path($_[0])->absolute('.') },
68             lazy => 1,
69             builder => '_build_test_dir',
70             predicate => '_has_test_dir',
71             );
72 53     53   819 sub _build_test_dir { path(dist_dir('Test-JSON-Schema-Acceptance'), 'tests', $_[0]->specification) };
73              
74             # this comes from the remotes/ directory in the JSON-Schema-Test-Suite repository
75             has additional_resources => (
76             is => 'ro',
77             isa => InstanceOf['Path::Tiny'],
78             coerce => sub { path($_[0])->absolute('.') },
79             lazy => 1,
80             default => sub { $_[0]->test_dir->parent->parent->child('remotes') },
81             );
82              
83             has verbose => (
84             is => 'ro',
85             isa => Bool,
86             default => 0,
87             );
88              
89             has include_optional => (
90             is => 'ro',
91             isa => Bool,
92             default => 0,
93             );
94              
95             has skip_dir => (
96             is => 'ro',
97             isa => ArrayRef[Str],
98             coerce => sub { ref($_[0]) ? $_[0] : [ $_[0] ] },
99             lazy => 1,
100             default => sub { [] },
101             );
102              
103             has test_schemas => (
104             is => 'ro',
105             isa => Bool,
106             );
107              
108             has results => (
109             is => 'rwp',
110             init_arg => undef,
111             isa => ArrayRef[Dict[
112             file => InstanceOf['Path::Tiny'],
113             map +($_ => PositiveOrZeroInt), qw(pass todo_fail fail),
114             ]],
115             );
116              
117             has results_text => (
118             is => 'ro',
119             init_arg => undef,
120             isa => Str,
121             lazy => 1,
122             builder => '_build_results_text',
123             );
124              
125             around BUILDARGS => sub ($orig, $class, @args) {
126             my %args = @args % 2 ? ( specification => 'draft'.$args[0] ) : @args;
127             $args{specification} = 'draft2020-12' if ($args{specification} // '') eq 'latest';
128             $class->$orig(\%args);
129             };
130              
131 52     52 0 10759 sub BUILD ($self, @) {
  52         122  
  52         87  
132 52 100       1035 -d $self->test_dir or die 'test_dir does not exist: '.$self->test_dir;
133             }
134              
135             sub acceptance {
136 56     56 1 342144 my $self = shift;
137 56 100       397 my $options = +{ ref $_[0] eq 'CODE' ? (validate_json_string => @_) : @_ };
138              
139             die 'require one or the other of "validate_data", "validate_json_string"'
140 56 50 66     323 if not $options->{validate_data} and not $options->{validate_json_string};
141              
142             die 'cannot provide both "validate_data" and "validate_json_string"'
143 56 50 66     485 if $options->{validate_data} and $options->{validate_json_string};
144              
145 56 100       305 warn "'skip_tests' option is deprecated" if $options->{skip_tests};
146              
147 56         214 my $ctx = Test2::API::context;
148              
149 56 100 66     14450 if ($options->{add_resource} and -d $self->additional_resources) {
150             # this is essentially what `bin/jsonschema_suite remote` does: resolves the filename against the
151             # base uri to determine the absolute schema location of each resource.
152 1         95 my $base = 'http://localhost:1234';
153 1         34 $ctx->note('adding resources from '.$self->additional_resources.' with the base URI "'.$base.'"...');
154 9         19 $self->additional_resources->visit(
155 9     9   2160 sub ($path, @) {
  9         17  
156 9 100 66     32 return if not $path->is_file or $path !~ /\.json$/;
157              
158             # version-specific resources are stored in a subdirectory by version:
159             # skip resource files that are marked as being for an unsupported draft
160 5         363 my $relative_path = $path->relative($self->additional_resources);
161 5         1396 my ($topdir) = split qr{/}, $relative_path, 2;
162 5 100 100     218 return if $topdir =~ /^(?:draft(?:[3467]|2019-09|2020-12)|v1)\z/ and not grep $topdir eq $_, $self->supported_specifications->@*;
163              
164 4         40 my $data = $self->json_deserialize($path->slurp_raw);
165 4         1042 my $uri = $base.'/'.$relative_path;
166 4 100       50 $options->{add_resource}->($uri => $data,
167             # ensure the evaluator parses this resource using its specified version
168             $topdir =~ /^draft/ ? (specification_version => $topdir) : ());
169             },
170 1         655 { recurse => 1 },
171             );
172             }
173              
174 56 100       2324 $ctx->note('running tests in '.$self->test_dir.' against '
175             .($self->_has_specification ? $self->specification : 'unknown version').'...');
176 56         21090 my $tests = $self->_test_data;
177              
178             # [ { file => .., pass => .., fail => .. }, ... ]
179 56         39726 my @results;
180              
181 56         241 foreach my $one_file (@$tests) {
182 215         566 my %results;
183             next if $options->{tests} and $options->{tests}{file}
184             and not grep $_ eq $one_file->{file},
185             (ref $options->{tests}{file} eq 'ARRAY'
186 215 100 100     1506 ? $options->{tests}{file}->@* : $options->{tests}{file});
    100 100        
187              
188 204         1034 $ctx->note('');
189              
190 204         52622 foreach my $test_group ($one_file->{json}->@*) {
191             next if $options->{tests} and $options->{tests}{group_description}
192             and not grep $_ eq $test_group->{description},
193             (ref $options->{tests}{group_description} eq 'ARRAY'
194 960 100 100     6807 ? $options->{tests}{group_description}->@* : $options->{tests}{group_description});
    100 100        
195              
196 936         1723 my $todo;
197             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
198             if $options->{todo_tests}
199             and any {
200 74     74   180 my $o = $_;
201             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
202             and
203             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
204             and not $o->{test_description}
205 74 100 100     916 }
      100        
      100        
206 936 100 100     4264 $options->{todo_tests}->@*;
207              
208 936         3967 my $schema_fails;
209 936 50       4236 if ($self->test_schemas) {
210 0 0       0 die 'specification_version unknown: cannot evaluate schema against metaschema'
211             if not $self->_has_specification;
212              
213             my $metaschema_uri = is_plain_hashref($test_group->{schema}) && $test_group->{schema}{'$schema'}
214             ? $test_group->{schema}{'$schema'}
215 0 0 0     0 : METASCHEMA->{$self->specification};
216 0         0 my $metaschema_schema = { '$ref' => $metaschema_uri };
217             my $result = $options->{validate_data}
218             ? $options->{validate_data}->($metaschema_schema, $test_group->{schema})
219 0 0       0 : $options->{validate_json_string}->($metaschema_schema, $self->json_serialize($test_group->{schema}));
220 0 0       0 if (not $result) {
221 0         0 $ctx->fail('schema for '.$one_file->{file}.': "'.$test_group->{description}.'" fails to validate against '.$metaschema_uri.':');
222 0         0 $ctx->note($self->json_prettyprint($result));
223 0         0 $schema_fails = 1;
224             }
225             }
226              
227 936         3597 foreach my $test ($test_group->{tests}->@*) {
228             next if $options->{tests} and $options->{tests}{test_description}
229             and not grep $_ eq $test->{description},
230             (ref $options->{tests}{test_description} eq 'ARRAY'
231 3206 100 100     16827 ? $options->{tests}{test_description}->@* : $options->{tests}{test_description});
    100 100        
232              
233 3175         4918 my $todo;
234             $todo = Test2::Todo->new(reason => 'Test marked TODO via deprecated "skip_tests"')
235             if ref $options->{skip_tests} eq 'ARRAY'
236             and grep +(($test_group->{description}.' - '.$test->{description}) =~ /$_/),
237 3175 100 100     12486 $options->{skip_tests}->@*;
238              
239             $todo = Test2::Todo->new(reason => 'Test marked TODO via "todo_tests"')
240             if $options->{todo_tests}
241             and any {
242 222     222   485 my $o = $_;
243             (not $o->{file} or grep $_ eq $one_file->{file}, (ref $o->{file} eq 'ARRAY' ? $o->{file}->@* : $o->{file}))
244             and
245             (not $o->{group_description} or grep $_ eq $test_group->{description}, (ref $o->{group_description} eq 'ARRAY' ? $o->{group_description}->@* : $o->{group_description}))
246             and
247 222 100 100     2490 (not $o->{test_description} or grep $_ eq $test->{description}, (ref $o->{test_description} eq 'ARRAY' ? $o->{test_description}->@* : $o->{test_description}))
    100 100        
      100        
      100        
248             }
249 3175 100 100     13395 $options->{todo_tests}->@*;
250              
251 3175         15745 my $result = $self->_run_test($one_file, $test_group, $test, $options);
252 3175 50       10765 $result = 0 if $schema_fails;
253              
254 3175 100       14995 ++$results{ $result ? 'pass' : $todo ? 'todo_fail' : 'fail' };
    100          
255             }
256             }
257              
258 204         2874 push @results, { file => $one_file->{file}, pass => 0, 'todo_fail' => 0, fail => 0, %results };
259             }
260              
261 56         2257 $self->_set_results(\@results);
262              
263 56 50       6204 my $diag = $self->verbose ? 'diag' : 'note';
264 56         1536 $ctx->$diag("\n".$self->results_text."\n");
265              
266 56 100 100     17340 if ($self->test_dir !~ m{\boptional\b}
      66        
267             and grep +($_->{file} !~ m{^optional/} && $_->{todo_fail} + $_->{fail}), @results) {
268             # non-optional test failures will always be visible, even when not in verbose mode.
269 43         2277 $ctx->diag('WARNING: some non-optional tests are failing! This implementation is not fully compliant with the specification!');
270 43         10322 $ctx->diag('');
271             }
272             else {
273 13         512 $ctx->$diag('Congratulations, all non-optional tests are passing!');
274 13         3749 $ctx->$diag('');
275             }
276              
277 56         12730 $ctx->release;
278             }
279              
280 3175     3175   5577 sub _run_test ($self, $one_file, $test_group, $test, $options) {
  3175         5664  
  3175         5005  
  3175         5322  
  3175         4910  
  3175         4934  
  3175         5185  
281 3175         23701 my $test_name = $one_file->{file}.': "'.$test_group->{description}.'" - "'.$test->{description}.'"';
282              
283 3175         30732 my $pass; # ignores TODO status
284              
285             Test2::API::run_subtest($test_name,
286             sub {
287 3175     3175   1215649 my ($result_bool, $result, $schema_before, $data_before, $schema_after, $data_after);
288 3175         7313 try {
289             ($schema_before, $data_before) = map $self->json_serialize($_),
290 3175         128634 $test_group->{schema}, $test->{data};
291              
292             ($result_bool, $result) = $options->{validate_data}
293             ? $options->{validate_data}->($test_group->{schema}, $test->{data})
294 3175 100       355229 : $options->{validate_json_string}->($test_group->{schema}, $self->json_serialize($test->{data}));
295              
296             ($schema_after, $data_after) = map $self->json_serialize($_),
297 3173         247497 $test_group->{schema}, $test->{data};
298              
299 3173         279780 my $ctx = Test2::API::context;
300              
301             # skip the ugly matrix comparison
302 3173 100       252177 my $expected = $test->{valid} ? 'true' : 'false';
303 3173 100 100     33203 if ($result_bool xor $test->{valid}) {
304 1225 100       17336 $ctx->fail('evaluation result is incorrect', 'expected '.$expected.'; got '.($result_bool ? 'true' : 'false'));
305 1225 50       410405 $ctx->${ $self->verbose ? \'diag' : \'note' }('schema: '.$self->json_prettyprint($test_group->{schema}));
  1225         59020  
306 1225 50       58861 $ctx->${ $self->verbose ? \'diag' : \'note' }('data: '.$self->json_prettyprint($test->{data})
307 1225 100       365825 .(ref $test->{data} ? 'reference to '.ref $test->{data} : ''));
308              
309             # for backwards compatibility, if only one value is returned, it might be possible to
310             # jsonify it to access the full results
311 1225 50 66     341108 $ctx->${ $self->verbose ? \'diag' : \'note' }('result: '.$self->json_prettyprint($result // $result_bool));
  1225         50025  
312 1225         314468 $pass = 0;
313             }
314             else {
315 1948         27065 $ctx->ok(1, 'test passes: data is valid: '.$expected);
316 1948         383875 $pass = 1;
317             }
318              
319 3173         15680 my @mutated_data_paths = $self->_mutation_check($test->{data});
320 3173         8877 my @mutated_schema_paths = $self->_mutation_check($test_group->{schema});
321              
322             # string check path check behaviour
323             # 0 0 ::is(), and note. $pass = 0
324             # 0 1 ::is(). $pass = 0
325             # 1 0 ->fail and note. $pass = 0
326             # 1 1 no test. $pass does not change.
327              
328 3173 100       10647 if ($data_before ne $data_after) {
    100          
329 9         58 Test2::Tools::Compare::is($data_after, $data_before, 'evaluator did not mutate data');
330 9         42567 $pass = 0;
331             }
332             elsif (@mutated_data_paths) {
333 1         7 $ctx->fail('evaluator did not mutate data');
334 1         250 $pass = 0
335             }
336              
337 3173 50       6693 $ctx->note('mutated data at location'.(@mutated_data_paths > 1 ? 's' : '').': '.join(', ', @mutated_data_paths)) if @mutated_data_paths;
    100          
338              
339 3173 100       8891 if ($schema_before ne $schema_after) {
    100          
340 9         73 Test2::Tools::Compare::is($schema_after, $schema_before, 'evaluator did not mutate schema');
341 9         38986 $pass = 0;
342             }
343             elsif (@mutated_schema_paths) {
344 1         6 $ctx->fail('evaluator did not mutate schema');
345 1         245 $pass = 0;
346             }
347              
348 3173 50       6063 $ctx->note('mutated schema at location'.(@mutated_schema_paths > 1 ? 's' : '').': '.join(', ', @mutated_schema_paths)) if @mutated_schema_paths;
    100          
349              
350 3173         12526 $ctx->release;
351             }
352             catch ($e) {
353 2         44 chomp(my $exception = $e);
354 2         7 my $ctx = Test2::API::context;
355 2         141 $ctx->fail('died: '.$exception);
356 2         517 $ctx->release;
357             };
358             },
359 3175         37668 { buffered => 1, inherit_trace => 1 },
360             );
361              
362 3175         3606252 return $pass;
363             }
364              
365 6346     6346   10925 sub _mutation_check ($self, $data) {
  6346         10692  
  6346         11232  
  6346         8914  
366 6346         9880 my @error_paths;
367              
368             # [ path => data ]
369 6346         16448 my @nodes = ([ '', $data ]);
370 6346         17407 while (my $node = shift @nodes) {
371 19875 100       43633 if (not defined $node->[1]) {
372 297         973 next;
373             }
374 19578 100       54884 if (is_plain_arrayref($node->[1])) {
    100          
    100          
375 2259         15079 push @nodes, map [ $node->[0].'/'.$_, $node->[1][$_] ], 0 .. $node->[1]->$#*;
376 2259 50       9206 push @error_paths, $node->[0] if tied($node->[1]->@*);
377             }
378             elsif (is_plain_hashref($node->[1])) {
379 8104         50227 push @nodes, map [ $node->[0].'/'.(s!~!~0!gr =~ s!/!~1!gr), $node->[1]{$_} ], keys $node->[1]->%*;
380 8104 100       30918 push @error_paths, $node->[0] if tied($node->[1]->%*);
381             }
382             elsif (is_ref($node->[1])) {
383 1977         6840 next; # boolean or bignum
384             }
385             else {
386 7238         31245 my $flags = B::svref_2object(\$node->[1])->FLAGS;
387 7238 100 75     43082 push @error_paths, $node->[0]
388             if not ($flags & B::SVf_POK xor $flags & (B::SVf_IOK | B::SVf_NOK));
389             }
390             }
391              
392 6346         15807 return @error_paths;
393             }
394              
395 17     17   259 use constant _JSON_BACKEND => Mojo::JSON::JSON_XS ? 'Cpanel::JSON::XS' : 'JSON::PP';
  17         83  
  17         38651  
396              
397             # used for internal serialization/deserialization; does not prettify the string.
398             has _json_serializer => (
399             is => 'ro',
400             isa => HasMethods[qw(encode decode)],
401             handles => {
402             json_serialize => 'encode',
403             json_deserialize => 'decode',
404             },
405             lazy => 1,
406             default => sub { _JSON_BACKEND->new->allow_nonref(1)->utf8(1)->allow_blessed(1)->allow_bignum(1)->canonical(1) },
407              
408             );
409              
410             # used for displaying diagnostics only
411             has _json_prettyprinter => (
412             is => 'ro',
413             isa => HasMethods['encode'],
414             lazy => 1,
415             handles => {
416             json_prettyprint => 'encode',
417             },
418             default => sub {
419             my $encoder = _JSON_BACKEND->new->allow_nonref(1)->utf8(0)->allow_blessed(1)->allow_bignum(1)->canonical(1)->convert_blessed(1)->pretty(1)->space_before(0);
420             $encoder->indent_length(2) if $encoder->can('indent_length');
421             $encoder;
422             },
423             );
424              
425             # backcompat shims
426 0     0   0 sub _json_decoder { shift->_json_serializer(@_) }
427 0     0 0 0 sub json_decoder { shift->_json_serializer(@_) }
428 0     0   0 sub _json_encoder { shift->_json_prettyprinter(@_) }
429 0     0 1 0 sub json_encoder { shift->_json_prettyprinter(@_) }
430              
431             # see JSON::MaybeXS::is_bool
432             my $json_bool = InstanceOf[qw(JSON::XS::Boolean Cpanel::JSON::XS::Boolean JSON::PP::Boolean)];
433              
434             has _test_data => (
435             is => 'lazy',
436             isa => ArrayRef[Dict[
437             file => InstanceOf['Path::Tiny'],
438             json => ArrayRef[Dict[
439             # id => Optional[Str],
440             # specification => Optional[Str],
441             description => Str,
442             comment => Optional[Str],
443             schema => $json_bool|HashRef,
444             tests => ArrayRef[Dict[
445             # id => Optional[Str],
446             data => Any,
447             description => Str,
448             comment => Optional[Str],
449             valid => $json_bool,
450             Slurpy[Any],
451             ]],
452             Slurpy[Any],
453             ]],
454             ]],
455             );
456              
457 40     40   2269 sub _build__test_data ($self) {
  40         105  
  40         65  
458 40         76 my @test_groups;
459              
460             $self->test_dir->visit(
461             sub {
462 645     645   82058 my ($path) = @_;
463 645 100       22199 return if any { $self->test_dir->child($_)->subsumes($path) } $self->skip_dir->@*;
  10         342  
464 643 100       12164 return if not $path->is_file;
465 617 100       16899 return if $path !~ /\.json$/;
466 615         6394 my $data = $self->json_deserialize($path->slurp_raw);
467 615 100       1495013 return if not @$data; # placeholder files for renamed tests
468 587         16899 my $file = $path->relative($self->test_dir);
469 587         146880 push @test_groups, [
470             scalar(split('/', $file)),
471             {
472             file => $file,
473             json => $data,
474             },
475             ];
476             },
477 40         803 { recurse => $self->include_optional },
478             );
479              
480             return [
481             map $_->[1],
482 40 50       4084 sort { $a->[0] <=> $b->[0] || $a->[1]{file} cmp $b->[1]{file} }
  2316         17280  
483             @test_groups
484             ];
485             }
486              
487 32     32   401 sub _build_results_text ($self) {
  32         71  
  32         60  
488 32         82 my @lines;
489 132     132   519 sub _pad ($s, $rest) { sprintf('%-29s', $s) . $rest }
  132         225  
  132         197  
  132         178  
  132         630  
490 32         604 push @lines, _pad('generated with:', ref($self).' '.$self->VERSION);
491              
492 32         927 my $test_dir = $self->test_dir;
493 32         377 my $orig_dir = $self->_build_test_dir;
494              
495 32         7091 my $submodule_status = path(dist_dir('Test-JSON-Schema-Acceptance'), 'submodule_status');
496 32 100 66     3687 if ($submodule_status->exists and $submodule_status->parent->subsumes($self->test_dir)) {
    50 33        
497 3         1074 chomp(my ($commit, $url) = $submodule_status->lines);
498 3         883 push @lines, _pad('with commit:', $commit);
499 3         16 push @lines, _pad('from repository:', $url);
500             }
501             elsif ($test_dir eq $orig_dir and not -d '.git') {
502 0         0 die 'submodule_status file is missing - packaging error? cannot continue';
503             }
504              
505 32   50     8020 push @lines, _pad('specification version:', $self->specification//'unknown');
506              
507 32 100       125 if ($test_dir ne $orig_dir) {
508 29         229 my $local;
509 29 50       91 if ($orig_dir->subsumes($test_dir)) {
    50          
510 0         0 $test_dir = '/'.substr($test_dir, length($orig_dir)+1);
511             }
512             elsif (Path::Tiny->cwd->subsumes($test_dir)) {
513 29         6694 $test_dir = $test_dir->relative;
514 29         8350 $local = 1;
515             }
516 29         126 push @lines, _pad('using custom test directory:', $test_dir);
517              
518 29 50       240 eval {
519 0         0 my $git = Git::Wrapper->new($test_dir);
520 0         0 my @ref = $git->describe({ all => 1, long => 1, always => 1 });
521 0         0 push @lines, _pad('at ref:', $ref[0]);
522             } if not $local;
523             }
524 32 100       284 push @lines, _pad('optional tests included:', $self->include_optional ? 'yes' : 'no');
525 32         927 push @lines, map _pad('skipping directory:', $_), $self->skip_dir->@*;
526              
527 32         326 push @lines, '';
528 32         238 my $length = max(40, map length $_->{file}, $self->results->@*);
529              
530 32         839 push @lines, sprintf('%-'.$length.'s pass todo-fail fail', 'filename');
531 32         122 push @lines, '-'x($length + 23);
532 32         244 push @lines, map sprintf('%-'.$length.'s % 5d % 4d % 4d', $_->@{qw(file pass todo_fail fail)}),
533             $self->results->@*;
534              
535 32         748 my $total = +{ map { my $type = $_; $type => sum0(map $_->{$type}, $self->results->@*) } qw(pass todo_fail fail) };
  96         192  
  96         760  
536 32         149 push @lines, '-'x($length + 23);
537 32         228 push @lines, sprintf('%-'.$length.'s % 5d % 5d % 5d', 'TOTAL', $total->@{qw(pass todo_fail fail)});
538              
539 32         1066 return join("\n", @lines, '');
540             }
541              
542             1;
543              
544             __END__