lib/App/Test/Generator/Emitter/Perl.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 100.00%
TER3 (LCSAJ): 100.0% (4/4)
Approximate LCSAJ segments: 45

LCSAJ Legend

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.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    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;