TER1 (Statement): 100.00%
TER2 (Branch): 86.67%
TER3 (LCSAJ): 100.0% (8/8)
Approximate LCSAJ segments: 31
● 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::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) {
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: 0, Survived: 1)
- COND_INV_213_4: Invert condition if to unless
MEDIUM: Add tests asserting both true and false outcomesMutants (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;