lib/App/Test/Generator/TestStrategy.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 86.67%
TER3 (LCSAJ): 100.0% (8/8)
Approximate LCSAJ segments: 31

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::TestStrategy;
    2: 
    3: use strict;
    4: use warnings;
    5: use Readonly;
    6: 
    7: # --------------------------------------------------
    8: # Accessor type strings from the schema
    9: # --------------------------------------------------
   10: Readonly my $ACCESSOR_GETTER   => 'getter';
   11: Readonly my $ACCESSOR_SETTER   => 'setter';
   12: Readonly my $ACCESSOR_GETSET   => 'getset';
   13: 
   14: # --------------------------------------------------
   15: # Output type strings from the schema
   16: # --------------------------------------------------
   17: Readonly my $TYPE_BOOLEAN => 'boolean';
   18: Readonly my $TYPE_OBJECT  => 'object';
   19: Readonly my $TYPE_VOID    => 'void';
   20: 
   21: # --------------------------------------------------
   22: # Default confidence threshold for plan generation
   23: # --------------------------------------------------
   24: Readonly my $DEFAULT_CONFIDENCE => 'medium';
   25: 
   26: # --------------------------------------------------
   27: # Test plan flag keys written to the method plan
   28: # --------------------------------------------------
   29: Readonly my $TEST_CONTEXT         => 'context_tests';
   30: Readonly my $TEST_PREDICATE       => 'predicate_test';
   31: Readonly my $TEST_GETTER          => 'getter_test';
   32: Readonly my $TEST_SETTER          => 'setter_test';
   33: Readonly my $TEST_GETSET          => 'getset_test';
   34: Readonly my $TEST_OBJECT_INJECT   => 'object_injection_test';
   35: Readonly my $TEST_BOOLEAN_SET     => 'boolean_set_test';
   36: Readonly my $TEST_VOID            => 'void_context_test';
   37: Readonly my $TEST_ERROR_HANDLING  => 'error_handling_test';
   38: Readonly my $TEST_BOUNDARY        => 'boundary_tests';
   39: Readonly my $TEST_CHAINING        => 'chaining_test';
   40: Readonly my $TEST_BASIC           => 'basic_test';
   41: 
   42: our $VERSION = '0.46';
   43: 
   44: =head1 VERSION
   45: 
   46: Version 0.46
   47: 
   48: =head1 DESCRIPTION
   49: 
   50: Generates a test strategy plan for all methods in a schema, determining
   51: which test types should be produced for each method based on its
   52: accessor classification, output type, and other metadata. Side-effect
   53: and dependency-driven planning (mocking, isolation) is handled
   54: separately by L<App::Test::Generator::Planner::Mock> and
   55: L<App::Test::Generator::Planner::Isolation>.
   56: 
   57: This way a package's API can be automatically tested.
   58: 
   59: =head2 new
   60: 
   61: Construct a new TestStrategy.
   62: 
   63:     my $strategy = App::Test::Generator::TestStrategy->new(
   64:         schema     => \%schemas,
   65:         thresholds => { confidence => 'high' },
   66:     );
   67: 
   68: =head3 Arguments
   69: 
   70: =over 4
   71: 
   72: =item * C<schema>
   73: 
   74: A hashref of method name to schema hashref
   75: Optional - defaults to
   76: an empty hashref.
   77: 
   78: =item * C<thresholds>
   79: 
   80: A hashref of threshold configuration.
   81: Optional - defaults to
   82: C<< { confidence => 'medium' } >>.
   83: 
   84: =back
   85: 
   86: =head3 Returns
   87: 
   88: A blessed hashref.
   89: 
   90: =head3 API specification
   91: 
   92: =head4 input
   93: 
   94:     {
   95:         schema     => { type => 'hashref', optional => 1 },
   96:         thresholds => { type => 'hashref', optional => 1 },
   97:     }
   98: 
   99: =head4 output
  100: 
  101:     {
  102:         type => OBJECT,
  103:         isa  => 'App::Test::Generator::TestStrategy',
  104:     }
  105: 
  106: =cut
  107: 
  108: sub new {
  109: 	my ($class, %args) = @_;
  110: 	return bless {

Mutants (Total: 2, Killed: 2, Survived: 0)

111: schema => $args{schema} || {}, 112: thresholds => $args{thresholds} || { confidence => $DEFAULT_CONFIDENCE }, 113: plans => {}, 114: }, $class; 115: } 116: 117: =head2 generate_plan 118: 119: Generate a test plan for all methods in the schema and return it as 120: a hashref mapping method names to plan hashrefs. 121: 122: my $strategy = App::Test::Generator::TestStrategy->new( 123: schema => \%schemas, 124: ); 125: my $plan = $strategy->generate_plan; 126: 127: for my $method (keys %{$plan}) { 128: print "$method: ", join(', ', keys %{ $plan->{$method} }), "\n"; 129: } 130: 131: =head3 Arguments 132: 133: None beyond C<$self>. 134: 135: =head3 Returns 136: 137: A hashref mapping method names to test plan hashrefs, each containing 138: boolean flags for the test types that should be generated. 139: 140: =head3 API specification 141: 142: =head4 input 143: 144: { 145: self => { type => OBJECT, isa => 'App::Test::Generator::TestStrategy' }, 146: } 147: 148: =head4 output 149: 150: { 151: type => 'hashref', 152: keys => { 153: '*' => { type => 'hashref' }, 154: }, 155: } 156: 157: =cut 158: 159: sub generate_plan { 160 → 162 → 169 160: my $self = $_[0]; 161: 162: for my $method (keys %{ $self->{schema} }) { 163: my $schema = $self->{schema}{$method}; 164: 165: # Generate and store the plan for this method 166: $self->{plans}{$method} = $self->_plan_for_method($schema); 167: } 168: 169: return $self->{plans};

Mutants (Total: 2, Killed: 2, Survived: 0)

170: } 171: 172: # -------------------------------------------------- 173: # _plan_for_method 174: # 175: # Determine which test types should be 176: # generated for a single method based on 177: # its schema metadata. 178: # 179: # Entry: $schema - the per-method schema hashref 180: # 181: # Exit: Returns a hashref of test type flags. 182: # Always contains at least basic_test => 1. 183: # 184: # Side effects: None. 185: # 186: # Notes: All string comparisons use // '' guards 187: # to avoid uninitialized value warnings 188: # when schema fields are absent. 189: # -------------------------------------------------- 190: sub _plan_for_method { 191 → 199 → 207 191: my ($self, $schema) = @_; 192: 193: my %plan; 194: 195: # -------------------------------------------------- 196: # Context-aware returns need both scalar and list 197: # context tests to verify correct behaviour in each 198: # -------------------------------------------------- 199: if($schema->{output}{_context_aware}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

200: $plan{$TEST_CONTEXT} = 1; 201: } 202: 203: # -------------------------------------------------- 204: # Accessor detection — choose test types based on 205: # whether the method is a getter, setter, or both 206: # -------------------------------------------------- 207 → 207 → 247 207: if($schema->{accessor} && scalar keys %{ $schema->{accessor} }) {

Mutants (Total: 1, Killed: 1, Survived: 0)

208: my $acc_type = $schema->{accessor}{type} // ''; 209: 210: if($acc_type eq $ACCESSOR_GETTER) {

Mutants (Total: 1, Killed: 1, Survived: 0)

211: # Boolean getters are predicates and need 212: # truthy/falsy tests in addition to getter tests 213: if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) {

Mutants (Total: 1, Killed: 0, Survived: 1)
214: $plan{$TEST_PREDICATE} = 1; 215: } 216: $plan{$TEST_GETTER} = 1; 217: 218: } elsif($acc_type eq $ACCESSOR_SETTER) { 219: $plan{$TEST_SETTER} = 1; 220: 221: } elsif($acc_type eq $ACCESSOR_GETSET) { 222: # For getset accessors, check the input parameter 223: # type to determine if object injection or boolean 224: # set tests are more appropriate. Sort by position (keys 225: # %hash has no defined order) so the choice is deterministic 226: # if more than one candidate is present. 227: my $input = $schema->{input} || {}; 228: my ($param) = sort { 229: ($input->{$a}{position} // 9999) <=> ($input->{$b}{position} // 9999) 230: || $a cmp $b 231: } grep { !/^_/ } keys %{ $input }; 232: my $param_type = ($param && $input->{$param}{type}) // ''; 233: 234: if($param_type eq $TYPE_OBJECT) {

Mutants (Total: 1, Killed: 1, Survived: 0)

235: $plan{$TEST_OBJECT_INJECT} = 1; 236: } elsif($param_type eq $TYPE_BOOLEAN) { 237: $plan{$TEST_BOOLEAN_SET} = 1; 238: } 239: $plan{$TEST_GETSET} = 1; 240: } 241: } 242: 243: # -------------------------------------------------- 244: # Void return type — verify the method returns nothing 245: # and does not accidentally return a useful value 246: # -------------------------------------------------- 247 → 247 → 255 247: if(($schema->{output}{type} // '') eq $TYPE_VOID) {

Mutants (Total: 1, Killed: 1, Survived: 0)

248: $plan{$TEST_VOID} = 1; 249: } 250: 251: # -------------------------------------------------- 252: # Error handling — verify error return conventions 253: # are tested explicitly 254: # -------------------------------------------------- 255 → 255 → 264 255: if($schema->{output}{_error_return}

Mutants (Total: 1, Killed: 1, Survived: 0)

256: || $schema->{output}{success_failure_pattern}) { 257: $plan{$TEST_ERROR_HANDLING} = 1; 258: } 259: 260: # -------------------------------------------------- 261: # Boundary hints from YAML test configuration — 262: # generate boundary/equivalence class tests 263: # -------------------------------------------------- 264 → 264 → 272 264: if($schema->{_yamltest_hints} && keys %{ $schema->{_yamltest_hints} }) {

Mutants (Total: 1, Killed: 1, Survived: 0)

265: $plan{$TEST_BOUNDARY} = 1; 266: } 267: 268: # -------------------------------------------------- 269: # Method chaining — verify that $self is returned 270: # and that calls can be chained 271: # -------------------------------------------------- 272 → 272 → 280 272: if($schema->{output}{_returns_self}) {

Mutants (Total: 1, Killed: 1, Survived: 0)

273: $plan{$TEST_CHAINING} = 1; 274: } 275: 276: # -------------------------------------------------- 277: # Boolean output — needs predicate tests regardless 278: # of whether an accessor was detected 279: # -------------------------------------------------- 280 → 280 → 288 280: if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) {

Mutants (Total: 1, Killed: 1, Survived: 0)

281: $plan{$TEST_PREDICATE} = 1; 282: } 283: 284: # -------------------------------------------------- 285: # Always generate at least a basic call test even 286: # if no other test types were identified 287: # -------------------------------------------------- 288: $plan{$TEST_BASIC} = 1 unless %plan; 289: 290: return \%plan;

Mutants (Total: 2, Killed: 2, Survived: 0)

291: } 292: 293: =head1 SUPPORT 294: 295: This module is provided as-is without any warranty. 296: 297: =head1 LICENCE AND COPYRIGHT 298: 299: Copyright 2025-2026 Nigel Horne. 300: 301: Usage is subject to the terms of GPL2. 302: If you use it, 303: please let me know. 304: 305: =cut 306: 307: 1;