1package TestBridge; 2 3use strict; 4use warnings; 5 6use Test::More 0.99; 7use TestUtils; 8use TestML::Tiny; 9 10BEGIN { 11 $| = 1; 12 binmode(Test::More->builder->$_, ":utf8") 13 for qw/output failure_output todo_output/; 14} 15 16use CPAN::Meta::YAML; 17 18use Exporter (); 19our @ISA = qw{ Exporter }; 20our @EXPORT = qw{ 21 run_all_testml_files 22 run_testml_file 23 test_yaml_roundtrip 24 test_perl_to_yaml 25 test_dump_error 26 test_load_error 27 test_yaml_json 28 test_code_point 29 error_like 30 cmp_deeply 31 _testml_has_points 32}; 33 34# regular expressions for checking error messages; incomplete, but more 35# can be added as more error messages get test coverage 36my %ERROR = ( 37 E_CIRCULAR => qr{\QCPAN::Meta::YAML does not support circular references}, 38 E_FEATURE => qr{\QCPAN::Meta::YAML does not support a feature}, 39 E_PLAIN => qr{\QCPAN::Meta::YAML found illegal characters in plain scalar}, 40 E_CLASSIFY => qr{\QCPAN::Meta::YAML failed to classify the line}, 41); 42 43# use XXX -with => 'YAML::XS'; 44 45#--------------------------------------------------------------------------# 46# run_all_testml_files 47# 48# Iterate over all .tml files in a directory using a particular test bridge 49# code # reference. Each file is wrapped in a subtest with a test plan 50# equal to the number of blocks. 51#--------------------------------------------------------------------------# 52 53sub run_all_testml_files { 54 my ($label, $dir, $bridge, @args) = @_; 55 56 my $code = sub { 57 my ($file, $blocks) = @_; 58 subtest "$label: $file" => sub { 59 plan tests => scalar @$blocks; 60 $bridge->($_, @args) for @$blocks; 61 }; 62 }; 63 64 my @files = find_tml_files($dir); 65 66 run_testml_file($_, $code) for sort @files; 67} 68 69sub run_testml_file { 70 my ($file, $code) = @_; 71 72 my $blocks = TestML::Tiny->new( 73 testml => $file, 74 version => '0.1.0', 75 )->{function}{data}; 76 77 $code->($file, $blocks); 78} 79 80sub _testml_has_points { 81 my ($block, @points) = @_; 82 my @values; 83 for my $point (@points) { 84 defined $block->{$point} or return; 85 push @values, $block->{$point}; 86 } 87 push @values, $block->{Label}; 88 return @values; 89} 90 91#--------------------------------------------------------------------------# 92# test_yaml_roundtrip 93# 94# two blocks: perl, yaml 95# 96# Tests that a YAML string loads to the expected perl data. Also, tests 97# roundtripping from perl->YAML->perl. 98# 99# We can't compare the YAML for roundtripping because CPAN::Meta::YAML doesn't 100# preserve order and comments. Therefore, all we can test is that given input 101# YAML we can produce output YAML that produces the same Perl data as the 102# input. 103# 104# The perl must be an array reference of data to serialize: 105# 106# [ $thing1, $thing2, ... ] 107# 108# However, if a test point called 'serializes' exists, the output YAML is 109# expected to match the input YAML and will be checked for equality. 110#--------------------------------------------------------------------------# 111 112sub test_yaml_roundtrip { 113 my ($block) = @_; 114 115 my ($yaml, $perl, $label) = 116 _testml_has_points($block, qw(yaml perl)) or return; 117 118 my %options = (); 119 for (qw(serializes)) { 120 if (defined($block->{$_})) { 121 $options{$_} = 1; 122 } 123 } 124 125 my $expected = eval $perl; die $@ if $@; 126 bless $expected, 'CPAN::Meta::YAML'; 127 128 subtest $label, sub { 129 # Does the string parse to the structure 130 my $yaml_copy = $yaml; 131 my $got = eval { CPAN::Meta::YAML->read_string( $yaml_copy ); }; 132 is( $@, '', "CPAN::Meta::YAML parses without error" ); 133 is( $yaml_copy, $yaml, "CPAN::Meta::YAML does not modify the input string" ); 134 SKIP: { 135 skip( "Shortcutting after failure", 2 ) if $@; 136 isa_ok( $got, 'CPAN::Meta::YAML' ); 137 cmp_deeply( $got, $expected, "CPAN::Meta::YAML parses correctly" ) 138 or diag "ERROR: $CPAN::Meta::YAML::errstr\n\nYAML:$yaml"; 139 } 140 141 # Does the structure serialize to the string. 142 # We can't test this by direct comparison, because any 143 # whitespace or comments would be lost. 144 # So instead we parse back in. 145 my $output = eval { $expected->write_string }; 146 is( $@, '', "CPAN::Meta::YAML serializes without error" ); 147 SKIP: { 148 skip( "Shortcutting after failure", 5 ) if $@; 149 ok( 150 !!(defined $output and ! ref $output), 151 "CPAN::Meta::YAML serializes to scalar", 152 ); 153 my $roundtrip = eval { CPAN::Meta::YAML->read_string( $output ) }; 154 is( $@, '', "CPAN::Meta::YAML round-trips without error" ); 155 skip( "Shortcutting after failure", 2 ) if $@; 156 isa_ok( $roundtrip, 'CPAN::Meta::YAML' ); 157 cmp_deeply( $roundtrip, $expected, "CPAN::Meta::YAML round-trips correctly" ); 158 159 # Testing the serialization 160 skip( "Shortcutting perfect serialization tests", 1 ) unless $options{serializes}; 161 is( $output, $yaml, 'Serializes ok' ); 162 } 163 164 }; 165} 166 167#--------------------------------------------------------------------------# 168# test_perl_to_yaml 169# 170# two blocks: perl, yaml 171# 172# Tests that perl references serialize correctly to a specific YAML output 173# 174# The perl must be an array reference of data to serialize: 175# 176# [ $thing1, $thing2, ... ] 177#--------------------------------------------------------------------------# 178 179sub test_perl_to_yaml { 180 my ($block) = @_; 181 182 my ($perl, $yaml, $label) = 183 _testml_has_points($block, qw(perl yaml)) or return; 184 185 my $input = eval "no strict; $perl"; die $@ if $@; 186 187 subtest $label, sub { 188 my $result = eval { CPAN::Meta::YAML->new( @$input )->write_string }; 189 is( $@, '', "write_string lives" ); 190 is( $result, $yaml, "dumped YAML correct" ); 191 }; 192} 193 194#--------------------------------------------------------------------------# 195# test_dump_error 196# 197# two blocks: perl, error 198# 199# Tests that perl references result in an error when dumped 200# 201# The perl must be an array reference of data to serialize: 202# 203# [ $thing1, $thing2, ... ] 204# 205# The error must be a key in the %ERROR hash in this file 206#--------------------------------------------------------------------------# 207 208sub test_dump_error { 209 my ($block) = @_; 210 211 my ($perl, $error, $label) = 212 _testml_has_points($block, qw(perl error)) or return; 213 214 my $input = eval "no strict; $perl"; die $@ if $@; 215 chomp $error; 216 my $expected = $ERROR{$error}; 217 218 subtest $label, sub { 219 my $result = eval { CPAN::Meta::YAML->new( @$input )->write_string }; 220 ok( !$result, "returned false" ); 221 error_like( $expected, "Got expected error" ); 222 }; 223} 224 225#--------------------------------------------------------------------------# 226# test_load_error 227# 228# two blocks: yaml, error 229# 230# Tests that a YAML string results in an error when loaded 231# 232# The error must be a key in the %ERROR hash in this file 233#--------------------------------------------------------------------------# 234 235sub test_load_error { 236 my ($block) = @_; 237 238 my ($yaml, $error, $label) = 239 _testml_has_points($block, qw(yaml error)) or return; 240 241 chomp $error; 242 my $expected = $ERROR{$error}; 243 244 subtest $label, sub { 245 my $result = eval { CPAN::Meta::YAML->read_string( $yaml ) }; 246 is( $result, undef, 'read_string returns undef' ); 247 error_like( $expected, "Got expected error" ) 248 or diag "YAML:\n$yaml"; 249 }; 250} 251 252#--------------------------------------------------------------------------# 253# test_yaml_json 254# 255# two blocks: yaml, json 256# 257# Tests that a YAML string can be loaded to Perl and dumped to JSON and 258# match an expected JSON output. The expected JSON is loaded and dumped 259# to ensure similar JSON dump options. 260#--------------------------------------------------------------------------# 261 262sub test_yaml_json { 263 my ($block, $json_lib) = @_; 264 $json_lib ||= do { require JSON::PP; 'JSON::PP' }; 265 266 my ($yaml, $json, $label) = 267 _testml_has_points($block, qw(yaml json)) or return; 268 269 subtest "$label", sub { 270 # test YAML Load 271 my $object = eval { 272 CPAN::Meta::YAML::Load($yaml); 273 }; 274 my $err = $@; 275 ok !$err, "YAML loads"; 276 return if $err; 277 278 # test YAML->Perl->JSON 279 # N.B. round-trip JSON to decode any \uNNNN escapes and get to 280 # characters 281 my $want = $json_lib->new->encode( 282 $json_lib->new->decode($json) 283 ); 284 my $got = $json_lib->new->encode($object); 285 is $got, $want, "Load is accurate"; 286 }; 287} 288 289#--------------------------------------------------------------------------# 290# test_code_point 291# 292# two blocks: code, yaml 293# 294# Tests that a Unicode codepoint is correctly dumped to YAML as both 295# key and value. 296# 297# The code test point must be a non-negative integer 298# 299# The yaml code point is the expected output of { $key => $value } where 300# both key and value are the character represented by the codepoint. 301#--------------------------------------------------------------------------# 302 303sub test_code_point { 304 my ($block) = @_; 305 306 my ($code, $yaml, $label) = 307 _testml_has_points($block, qw(code yaml)) or return; 308 309 subtest "$label - Unicode map key/value test" => sub { 310 my $data = { chr($code) => chr($code) }; 311 my $dump = CPAN::Meta::YAML::Dump($data); 312 $dump =~ s/^---\n//; 313 is $dump, $yaml, "Dump key and value of code point char $code"; 314 315 my $yny = CPAN::Meta::YAML::Dump(CPAN::Meta::YAML::Load($yaml)); 316 $yny =~ s/^---\n//; 317 is $yny, $yaml, "YAML for code point $code YNY roundtrips"; 318 319 my $nyn = CPAN::Meta::YAML::Load(CPAN::Meta::YAML::Dump($data)); 320 cmp_deeply( $nyn, $data, "YAML for code point $code NYN roundtrips" ); 321 } 322} 323 324#--------------------------------------------------------------------------# 325# error_like 326# 327# Test CPAN::Meta::YAML->errstr against a regular expression and clear the 328# errstr afterwards 329#--------------------------------------------------------------------------# 330 331sub error_like { 332 my ($regex, $label) = @_; 333 $label = "Got expected error" unless defined $label; 334 local $Test::Builder::Level = $Test::Builder::Level + 1; 335 my $ok = like( $@, $regex, $label ); 336 return $ok; 337} 338 339#--------------------------------------------------------------------------# 340# cmp_deeply 341# 342# is_deeply with some better diagnostics 343#--------------------------------------------------------------------------# 344sub cmp_deeply { 345 my ($got, $want, $label) = @_; 346 local $Test::Builder::Level = $Test::Builder::Level + 1; 347 is_deeply( $got, $want, $label ) 348 or diag "GOT:\n", explain($got), "\nWANTED:\n", explain($want); 349} 350 3511; 352