TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (4/4)
Approximate LCSAJ segments: 45
● Covered — this LCSAJ path was executed during testing.
● Not covered — this LCSAJ path was never executed. These are the paths to focus on.
Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:
start → end → jump
Uncovered paths show [NOT COVERED] in the tooltip.
1: package App::Test::Generator::Emitter::Perl; 2: 3: use strict; 4: use warnings; 5: use Carp qw(croak); 6: use Readonly; 7: 8: # -------------------------------------------------- 9: # Plan key names â must match those emitted by 10: # App::Test::Generator::TestStrategy and 11: # App::Test::Generator::Planner 12: # -------------------------------------------------- 13: Readonly my $TEST_BASIC => 'basic_test'; 14: Readonly my $TEST_GETTER => 'getter_test'; 15: Readonly my $TEST_SETTER => 'setter_test'; 16: Readonly my $TEST_GETSET => 'getset_test'; 17: Readonly my $TEST_CHAINING => 'chaining_test'; 18: Readonly my $TEST_ERROR_HANDLING => 'error_handling_test'; 19: Readonly my $TEST_CONTEXT => 'context_tests'; 20: Readonly my $TEST_OBJECT_INJECT => 'object_injection_test'; 21: Readonly my $TEST_PREDICATE => 'predicate_test'; 22: Readonly my $TEST_BOOLEAN => 'boolean_test'; 23: Readonly my $TEST_VOID => 'void_context_test'; 24: Readonly my $TEST_BOUNDARY => 'boundary_tests'; 25: 26: # -------------------------------------------------- 27: # Input/output type strings from the schema 28: # -------------------------------------------------- 29: Readonly my $TYPE_OBJECT => 'object'; 30: Readonly my $TYPE_BOOLEAN => 'boolean'; 31: 32: our $VERSION = '0.41'; 33: 34: =head1 VERSION 35: 36: Version 0.41 37: 38: =head1 DESCRIPTION 39: 40: Emits Perl test code for a set of method schemas and their associated 41: test plans. Each method plan is translated into one or more test blocks 42: using L<Test::Most>. The emitted code is returned as a string ready to 43: be written to a C<.t> file. 44: 45: =head2 new 46: 47: Construct a new Perl emitter. 48: 49: my $emitter = App::Test::Generator::Emitter::Perl->new( 50: schema => \%schemas, 51: plans => \%plans, 52: package => 'My::Module', 53: ); 54: 55: =head3 Arguments 56: 57: =over 4 58: 59: =item * C<schema> 60: 61: A hashref of method name to schema hashref. Required. 62: 63: =item * C<plans> 64: 65: A hashref of method name to test plan hashref, as produced by 66: L<App::Test::Generator::TestStrategy> or 67: L<App::Test::Generator::Planner>. Required. 68: 69: =item * C<package> 70: 71: The Perl package name of the module under test. Required. 72: 73: =back 74: 75: =head3 Returns 76: 77: A blessed hashref. Croaks if any required argument is missing. 78: 79: =head3 API specification 80: 81: =head4 input 82: 83: { 84: schema => { type => HASHREF }, 85: plans => { type => HASHREF }, 86: package => { type => SCALAR }, 87: } 88: 89: =head4 output 90: 91: { 92: type => OBJECT, 93: isa => 'App::Test::Generator::Emitter::Perl', 94: } 95: 96: =cut 97: 98: sub new { 99: my ($class, %args) = @_; 100: 101: # All three arguments are required for meaningful emission 102: croak 'schema required' unless defined $args{schema}; 103: croak 'plans required' unless defined $args{plans}; 104: croak 'package required' unless defined $args{package}; 105: 106: # $args{package} is spliced unescaped into use_ok()/new_ok() calls 107: # in _emit_header() â reject anything that isn't a valid Perl 108: # package name now, rather than generating broken or injected code. 109: croak "package '$args{package}' is not a valid Perl package name" 110: unless $args{package} =~ /^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/; 111: 112: return bless { 113: schema => $args{schema}, 114: plans => $args{plans}, 115: package => $args{package}, 116: }, $class; 117: } 118: 119: =head2 emit 120: 121: Generate and return the complete Perl test file source as a string, 122: including the file header, one test block per method, and the 123: C<done_testing()> footer. 124: 125: my $emitter = App::Test::Generator::Emitter::Perl->new( 126: schema => \%schemas, 127: plans => \%plans, 128: package => 'My::Module', 129: ); 130: my $test_code = $emitter->emit; 131: write_file('t/generated.t', $test_code); 132: 133: =head3 Arguments 134: 135: None beyond C<$self>. 136: 137: =head3 Returns 138: 139: A string containing the complete Perl test file source. 140: 141: =head3 Notes 142: 143: A method whose plan has the C<boundary_tests> flag set (because its 144: schema carries non-empty C<_yamltest_hints>) gets one smoke-test block 145: per hint value in C<boundary_values> and C<invalid_inputs>, calling 146: the method with that value and asserting only that the call does not 147: crash the test process. 148: 149: =head3 API specification 150: 151: =head4 input 152: 153: { 154: self => { type => OBJECT, isa => 'App::Test::Generator::Emitter::Perl' }, 155: } 156: 157: =head4 output 158: 159: { type => SCALAR } 160: 161: =cut 162: 163: sub emit {Mutants (Total: 2, Killed: 2, Survived: 0)
●164 → 170 → 175 164: my $self = $_[0]; 165: 166: # Start with the file header then append per-method test blocks 167: my $code = $self->_emit_header(); 168: 169: # Sort methods for deterministic output order 170: for my $method (sort keys %{ $self->{plans} }) { 171: $code .= $self->_emit_method_tests($method); 172: } 173: 174: # TAP footer required by Test::More / Test::Most 175: $code .= "\ndone_testing();\n"; 176: 177: return $code; 178: } 179: 180: # -------------------------------------------------- 181: # _emit_header 182: #
Mutants (Total: 2, Killed: 2, Survived: 0)
183: # Purpose: Generate the standard test file header 184: # including strict/warnings, use_ok and 185: # a default object construction. 186: # 187: # Entry: None beyond $self. 188: # Exit: Returns a string of Perl code. 189: # Side effects: None. 190: # Notes: The generated $obj is used by all 191: # subsequent test blocks. 192: # -------------------------------------------------- 193: sub _emit_header { 194: my $self = $_[0]; 195: 196: return <<"END_HEADER"; 197: use strict; 198: use warnings; 199: use Test::Most; 200: 201: use_ok('$self->{package}'); 202: 203: my \$obj = new_ok('$self->{package}'); 204: 205: END_HEADER 206: } 207: 208: # -------------------------------------------------- 209: # _emit_method_tests 210: # 211: # Purpose: Dispatch to the appropriate emit method 212: # for each test type flagged in the plan 213: # for a given method. 214: # 215: # Entry: $method - the method name string. 216: # Plan and schema are read from $self. 217: # Exit: Returns a string of Perl test code. 218: # Side effects: None. 219: # Notes: Test types are emitted in a fixed order 220: # for deterministic output. Methods with 221: # no recognised plan flags produce no 222: # output beyond the section comment. 223: # -------------------------------------------------- 224: sub _emit_method_tests { 225: my ($self, $method) = @_; 226: 227: # $method is spliced unescaped as a bareword method name 228: # (->$method(...)) by every _emit_*_test sub below â reject 229: # anything that isn't a valid Perl identifier before any of them run. 230: croak "method '$method' is not a valid Perl identifier" 231: unless $method =~ /^[A-Za-z_]\w*\z/; 232: 233: my $plan = $self->{plans}{$method}; 234: my $code = "\n# --- Tests for $method ---\n"; 235: 236: # Emit each test type in a consistent fixed order 237: $code .= $self->_emit_basic_test($method) if $plan->{$TEST_BASIC};
Mutants (Total: 2, Killed: 2, Survived: 0)
238: 239: $code .= $self->_emit_getter_test($method) if $plan->{$TEST_GETTER}; 240: 241: $code .= $self->_emit_setter_test($method) if $plan->{$TEST_SETTER}; 242: 243: $code .= $self->_emit_getset_test($method) if $plan->{$TEST_GETSET}; 244: 245: $code .= $self->_emit_chaining_test($method) if $plan->{$TEST_CHAINING}; 246: 247: $code .= $self->_emit_error_test($method) if $plan->{$TEST_ERROR_HANDLING}; 248: 249: $code .= $self->_emit_context_test($method) if $plan->{$TEST_CONTEXT}; 250: 251: $code .= $self->_emit_object_injection_test($method) if $plan->{$TEST_OBJECT_INJECT}; 252: 253: $code .= $self->_emit_boolean_test($method) if $plan->{$TEST_PREDICATE} || $plan->{$TEST_BOOLEAN};
Mutants (Total: 2, Killed: 2, Survived: 0)
254: 255: $code .= $self->_emit_void_test($method) if $plan->{$TEST_VOID}; 256: 257: $code .= $self->_emit_boundary_test($method) if $plan->{$TEST_BOUNDARY}; 258: 259: return $code; 260: } 261: 262: # -------------------------------------------------- 263: # _emit_basic_test 264: # 265: # Purpose: Emit a minimal test that calls the 266: # method and verifies it does not die. 267: # 268: # Entry: $method - method name string. 269: # Exit: Returns a string of Perl test code. 270: # Side effects: None. 271: # -------------------------------------------------- 272: sub _emit_basic_test { 273: my ($self, $method) = @_; 274:
Mutants (Total: 2, Killed: 2, Survived: 0)
275: return <<"END_TEST"; 276: { 277: my \$result = eval { \$obj->$method() }; 278: ok(!\$@, '$method does not die'); 279: } 280: END_TEST 281: } 282: 283: # -------------------------------------------------- 284: # _emit_getter_test 285: # 286: # Purpose: Emit a test that calls the getter and 287: # verifies it returns a defined value. 288: # 289: # Entry: $method - method name string. 290: # Exit: Returns a string of Perl test code. 291: # Side effects: None. 292: # -------------------------------------------------- 293: sub _emit_getter_test { 294: my ($self, $method) = @_; 295: 296: return <<"END_TEST";
Mutants (Total: 2, Killed: 2, Survived: 0)
297: { 298: my \$value = \$obj->$method(); 299: ok(defined \$value, '$method returns a value'); 300: } 301: END_TEST 302: } 303: 304: # -------------------------------------------------- 305: # _emit_setter_test 306: # 307: # Purpose: Emit a test that calls the setter with 308: # a string argument and verifies it 309: # accepts the input without dying. 310: # 311: # Entry: $method - method name string. 312: # Exit: Returns a string of Perl test code. 313: # Side effects: None. 314: # -------------------------------------------------- 315: sub _emit_setter_test { 316: my ($self, $method) = @_; 317: 318: return <<"END_TEST"; 319: { 320: ok(\$obj->$method('test'), '$method accepts input'); 321: } 322: END_TEST 323: } 324: 325: # -------------------------------------------------- 326: # _emit_getset_test 327: # 328: # Purpose: Emit a round-trip get/set test. The
Mutants (Total: 1, Killed: 1, Survived: 0)
329: # test type (object, boolean, or string)
Mutants (Total: 2, Killed: 2, Survived: 0)
330: # is determined from the schema input 331: # parameter type. 332: # 333: # Entry: $method - method name string. 334: # Schema is read from $self. 335: # Exit: Returns a string of Perl test code. 336: # Side effects: None. 337: # Notes: Falls back to string round-trip if the 338: # parameter type is unrecognised. 339: # --------------------------------------------------
Mutants (Total: 1, Killed: 1, Survived: 0)
340: sub _emit_getset_test {
Mutants (Total: 2, Killed: 2, Survived: 0)
●341 → 350 → 361 341: my ($self, $method) = @_; 342: 343: my $schema = $self->{schema}{$method}; 344: 345: # Find the first non-internal input parameter 346: my ($param) = grep { !/^_/ } keys %{ $schema->{input} || {} }; 347: my $type = ($param && $schema->{input}{$param}{type}) // ''; 348: 349: # Object injection round-trip 350: if($type eq $TYPE_OBJECT) { 351: return <<"END_TEST";
Mutants (Total: 2, Killed: 2, Survived: 0)
352: { 353: my \$mock = bless {}, 'Test::MockObject'; 354: \$obj->$method(\$mock); 355: isa_ok(\$obj->$method(), ref(\$mock), '$method get/set works'); 356: } 357: END_TEST 358: } 359: 360: # Boolean round-trip ●361 → 361 → 373 361: if($type eq $TYPE_BOOLEAN) { 362: return <<"END_TEST"; 363: { 364: \$obj->$method(1); 365: ok(\$obj->$method(), '$method get/set boolean true works'); 366: \$obj->$method(0); 367: ok(!\$obj->$method(), '$method get/set boolean false works'); 368: } 369: END_TEST 370: } 371: 372: # Default string round-trip
Mutants (Total: 2, Killed: 2, Survived: 0)
373: return <<"END_TEST"; 374: { 375: \$obj->$method('value'); 376: is(\$obj->$method(), 'value', '$method get/set works'); 377: } 378: END_TEST 379: } 380: 381: # -------------------------------------------------- 382: # _emit_chaining_test 383: # 384: # Purpose: Emit a test that verifies the method 385: # returns $self for method chaining. 386: # 387: # Entry: $method - method name string. 388: # Exit: Returns a string of Perl test code. 389: # Side effects: None. 390: # -------------------------------------------------- 391: sub _emit_chaining_test { 392: my ($self, $method) = @_; 393: 394: return <<"END_TEST";
Mutants (Total: 2, Killed: 2, Survived: 0)
395: { 396: my \$ret = \$obj->$method(); 397: isa_ok(\$ret, ref(\$obj), '$method returns self for chaining'); 398: } 399: END_TEST 400: } 401: 402: # -------------------------------------------------- 403: # _emit_error_test 404: # 405: # Purpose: Emit a test that calls the method with 406: # undef input and verifies it handles the 407: # error gracefully. 408: # 409: # Entry: $method - method name string. 410: # Exit: Returns a string of Perl test code. 411: # Side effects: None. 412: # -------------------------------------------------- 413: sub _emit_error_test { 414: my ($self, $method) = @_; 415: 416: return <<"END_TEST"; 417: { 418: my \$result = eval { \$obj->$method(undef) }; 419: ok(!\$result || \$@, '$method handles invalid input');
Mutants (Total: 2, Killed: 2, Survived: 0)
420: } 421: END_TEST 422: } 423: 424: # -------------------------------------------------- 425: # _emit_context_test 426: # 427: # Purpose: Emit tests that call the method in 428: # both scalar and list context to verify 429: # context-aware return behaviour. 430: # 431: # Entry: $method - method name string. 432: # Exit: Returns a string of Perl test code. 433: # Side effects: None. 434: # Notes: Uses eval to verify the calls survive 435: # rather than checking return values, 436: # since context-aware return values vary. 437: # -------------------------------------------------- 438: sub _emit_context_test { 439: my ($self, $method) = @_; 440: 441: return <<"END_TEST"; 442: { 443: my \$scalar = eval { \$obj->$method() }; 444: ok(!\$@, '$method survives in scalar context');
Mutants (Total: 2, Killed: 2, Survived: 0)
445: 446: my \@list = eval { \$obj->$method() }; 447: ok(!\$@, '$method survives in list context'); 448: } 449: END_TEST 450: } 451: 452: # -------------------------------------------------- 453: # _emit_object_injection_test 454: # 455: # Purpose: Emit a test that injects a mock object 456: # and verifies the same object is returned 457: # by the getter. 458: # 459: # Entry: $method - method name string. 460: # Exit: Returns a string of Perl test code. 461: # Side effects: None. 462: # -------------------------------------------------- 463: sub _emit_object_injection_test { 464: my ($self, $method) = @_; 465: 466: return <<"END_TEST"; 467: { 468: my \$mock = bless {}, 'Mock::Object'; 469: \$obj->$method(\$mock); 470: isa_ok(\$obj->$method(), 'Mock::Object', 471: '$method stores injected object instance');
Mutants (Total: 2, Killed: 2, Survived: 0)
472: } 473: END_TEST 474: } 475: 476: # -------------------------------------------------- 477: # _emit_boolean_test 478: # 479: # Purpose: Emit a test that verifies the method 480: # returns a defined scalar boolean value. 481: # 482: # Entry: $method - method name string. 483: # Exit: Returns a string of Perl test code. 484: # Side effects: None. 485: # Notes: Checks that the return value is defined, 486: # is not a reference, and is boolean-like 487: # without using numeric comparison which 488: # would warn on string returns. 489: # -------------------------------------------------- 490: sub _emit_boolean_test { 491: my ($self, $method) = @_; 492: 493: return <<"END_TEST"; 494: { 495: my \$result = \$obj->$method();
Mutants (Total: 2, Killed: 2, Survived: 0)
496: ok(defined \$result, '$method returns a defined value'); 497: ok(!ref \$result, '$method returns a scalar'); 498: ok(\$result ? 1 : 0, '$method returns a boolean-like value'); 499: } 500: END_TEST 501: } 502: 503: # -------------------------------------------------- 504: # _emit_void_test 505: # 506: # Emit a test that verifies the method 507: # does not return a meaningful value, 508: # consistent with a void return type. 509: # 510: # Entry: $method - method name string. 511: # Exit: Returns a string of Perl test code. 512: # Side effects: None. 513: # -------------------------------------------------- 514: sub _emit_void_test { 515: my ($self, $method) = @_; 516: 517: return <<"END_TEST"; 518: { 519: my \$result = eval { \$obj->$method() }; 520: ok(!\$@, '$method does not die'); 521: ok(!defined \$result, '$method returns nothing (void)'); 522: } 523: END_TEST 524: } 525: 526: # -------------------------------------------------- 527: # _emit_boundary_test 528: # 529: # Purpose: Emit one smoke-test block per boundary 530: # or invalid-input value detected by 531: # SchemaExtractor's _yamltest_hints, calling 532: # the method with each and confirming the 533: # call does not crash the test process. 534: # 535: # Entry: $method - method name string. 536: # Schema is read from $self. 537: # Exit: Returns a string of Perl test code, or 538: # the empty string if no hint values exist. 539: # Side effects: None. 540: # Notes: boundary_values and invalid_inputs are 541: # deliberately tested the same way: these 542: # are smoke tests proving resilience, not 543: # assertions on whether the call should 544: # succeed or die, since invalid_inputs are 545: # expected to be rejected while boundary_values 546: # are expected to be accepted. 547: # -------------------------------------------------- 548: sub _emit_boundary_test { ●549 → 562 → 572 549: my ($self, $method) = @_; 550: 551: require App::Test::Generator; 552: 553: my $hints = $self->{schema}{$method}{_yamltest_hints} || {}; 554: my @values = ( 555: @{ $hints->{boundary_values} || [] }, 556: @{ $hints->{invalid_inputs} || [] }, 557: ); 558: 559: return '' unless @values; 560: 561: my $code = ''; 562: for my $value (@values) { 563: my $literal = App::Test::Generator::perl_quote($value); 564: $code .= <<"END_TEST"; 565: { 566: eval { \$obj->$method($literal) }; 567: pass('$method survives boundary input $literal'); 568: } 569: END_TEST 570: } 571: 572: return $code; 573: } 574: 575: 1;