| File: | bin/extract-schemas2 |
| Coverage: | 83.6% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | #!/usr/bin/env perl | |||||
| 2 | ||||||
| 3 | 1 1 1 | 1793 1 42 | use strict; | |||
| 4 | 1 1 1 | 4 1 22 | use warnings; | |||
| 5 | ||||||
| 6 | 1 1 1 | 217 3248 26 | use Data::Dumper; | |||
| 7 | 1 1 1 | 293 4523 2 | use Getopt::Long; | |||
| 8 | 1 1 1 | 68 1 25 | use File::Path qw(make_path); | |||
| 9 | 1 1 1 | 195 25381 47 | use Pod::Usage; | |||
| 10 | 1 1 1 | 156 442 26 | use FindBin; | |||
| 11 | 1 1 1 | 161 289 2 | use lib "$FindBin::Bin/../lib"; | |||
| 12 | ||||||
| 13 | 1 1 1 | 688 2 16 | use App::Test::Generator::SchemaExtractor; | |||
| 14 | 1 1 1 | 132 27 16 | use App::Test::Generator::Planner; | |||
| 15 | 1 1 1 | 155 1 1213 | use App::Test::Generator::Emitter::Perl; | |||
| 16 | ||||||
| 17 - 39 | =head1 NAME extract-schemas2 - Extract schemas and optionally emit Perl tests =head1 SYNOPSIS extract-schemas2 [options] <module.pm> Options: --output-dir DIR Directory for schema files (default: schemas/) --emit-tests Generate Perl tests from schemas --test-dir DIR Output directory for generated tests (default: t/generated/) --strict-pod=off|warn|fatal --verbose --help --man =head1 DESCRIPTION This tool extracts method schemas from a Perl module and can optionally generate Perl test files using App::Test::Generator::Emitter::Perl. =cut | |||||
| 40 | ||||||
| 41 | # ---------------------------------------------------------------------- | |||||
| 42 | # CLI Options | |||||
| 43 | # ---------------------------------------------------------------------- | |||||
| 44 | ||||||
| 45 | 1 | 77835 | my %cli_opts = ( | |||
| 46 | help => 0, | |||||
| 47 | man => 0, | |||||
| 48 | ); | |||||
| 49 | ||||||
| 50 | 1 | 2 | my %opts = ( | |||
| 51 | output_dir => 'schemas', | |||||
| 52 | test_dir => 't/generated', | |||||
| 53 | strict_pod => 'warn', | |||||
| 54 | verbose => 0, | |||||
| 55 | emit_tests => 0, | |||||
| 56 | ); | |||||
| 57 | ||||||
| 58 | GetOptions( | |||||
| 59 | 'output-dir|o=s' => \$opts{output_dir}, | |||||
| 60 | 'emit-tests|e' => \$opts{emit_tests}, | |||||
| 61 | 'test-dir|t=s' => \$opts{test_dir}, | |||||
| 62 | 'strict-pod|s=s' => \$opts{strict_pod}, | |||||
| 63 | 'verbose|v' => \$opts{verbose}, | |||||
| 64 | 'help|h' => \$cli_opts{help}, | |||||
| 65 | 'man|m' => \$cli_opts{man}, | |||||
| 66 | 1 | 4 | ) or pod2usage(2); | |||
| 67 | ||||||
| 68 | 1 | 542 | pod2usage(-exitval => 0, -verbose => 1) if $cli_opts{help}; | |||
| 69 | 1 | 1 | pod2usage(-exitval => 0, -verbose => 2) if $cli_opts{man}; | |||
| 70 | ||||||
| 71 | 1 | 2 | if ($opts{strict_pod} !~ /^(off|warn|fatal)$/) { | |||
| 72 | 0 | 0 | die "Invalid --strict-pod value '$opts{strict_pod}'. Expected off, warn, or fatal\n"; | |||
| 73 | } | |||||
| 74 | ||||||
| 75 | 1 | 2 | my $input_file = shift @ARGV | |||
| 76 | or pod2usage('Error: No input file specified'); | |||||
| 77 | ||||||
| 78 | 1 | 7 | die "Error: File not found: $input_file\n" unless -f $input_file; | |||
| 79 | ||||||
| 80 | # ---------------------------------------------------------------------- | |||||
| 81 | # Extraction Phase | |||||
| 82 | # ---------------------------------------------------------------------- | |||||
| 83 | ||||||
| 84 | 1 | 4 | print "Extracting schemas from: $input_file\n"; | |||
| 85 | 1 | 1 | print "Schema output directory: $opts{output_dir}\n"; | |||
| 86 | ||||||
| 87 | 1 | 139 | make_path($opts{output_dir}) unless -d $opts{output_dir}; | |||
| 88 | ||||||
| 89 | 1 | 5 | my $extractor = App::Test::Generator::SchemaExtractor->new( | |||
| 90 | input_file => $input_file, | |||||
| 91 | %opts, | |||||
| 92 | ); | |||||
| 93 | ||||||
| 94 | 1 | 2 | my $schemas = $extractor->extract_all(); | |||
| 95 | ||||||
| 96 | # ---------------------------------------------------------------------- | |||||
| 97 | # Summary | |||||
| 98 | # ---------------------------------------------------------------------- | |||||
| 99 | ||||||
| 100 | 1 | 2 | print "\n", '=' x 70, "\n"; | |||
| 101 | 1 | 0 | print "EXTRACTION SUMMARY\n"; | |||
| 102 | 1 | 1 | print '=' x 70, "\n\n"; | |||
| 103 | ||||||
| 104 | 1 | 4 | my %confidence = ( | |||
| 105 | input => { high => 0, medium => 0, low => 0 }, | |||||
| 106 | output => { high => 0, medium => 0, low => 0 }, | |||||
| 107 | ); | |||||
| 108 | ||||||
| 109 | 1 | 2 | foreach my $method (sort keys %$schemas) { | |||
| 110 | 1 | 1 | my $schema = $schemas->{$method}; | |||
| 111 | ||||||
| 112 | 1 | 2 | my $iconf = $schema->{_confidence}{input}{level} // 'low'; | |||
| 113 | 1 | 1 | my $oconf = $schema->{_confidence}{output}{level} // 'low'; | |||
| 114 | ||||||
| 115 | 1 | 1 | $confidence{input}{$iconf}++; | |||
| 116 | 1 | 1 | $confidence{output}{$oconf}++; | |||
| 117 | ||||||
| 118 | my $param_count = | |||||
| 119 | 1 2 1 | 1 2 2 | scalar grep { $_ !~ /^_/ } keys %{ $schema->{input} }; | |||
| 120 | ||||||
| 121 | 1 | 3 | printf "%-30s %2d params [%s input] [%s output]\n", | |||
| 122 | $method, | |||||
| 123 | $param_count, | |||||
| 124 | uc($iconf), | |||||
| 125 | uc($oconf); | |||||
| 126 | } | |||||
| 127 | ||||||
| 128 | 1 | 5 | print "\nTotal methods: ", scalar(keys %$schemas), "\n"; | |||
| 129 | ||||||
| 130 | 1 | 1 | foreach my $phase (qw(input output)) { | |||
| 131 | 2 | 1 | print ucfirst($phase), " confidence:\n"; | |||
| 132 | 2 | 2 | foreach my $level (qw(high medium low)) { | |||
| 133 | 6 | 6 | printf " %-6s %d\n", ucfirst($level), $confidence{$phase}{$level}; | |||
| 134 | } | |||||
| 135 | } | |||||
| 136 | ||||||
| 137 | # ---------------------------------------------------------------------- | |||||
| 138 | # Test Emission Phase | |||||
| 139 | # ---------------------------------------------------------------------- | |||||
| 140 | ||||||
| 141 | 1 | 2 | if ($opts{emit_tests}) { | |||
| 142 | ||||||
| 143 | 1 | 1 | print "\nGenerating Perl tests...\n"; | |||
| 144 | 1 | 1 | print "Test output directory: $opts{test_dir}\n"; | |||
| 145 | ||||||
| 146 | 1 | 83 | make_path($opts{test_dir}) unless -d $opts{test_dir}; | |||
| 147 | ||||||
| 148 | # ------------------------------------------------------------ | |||||
| 149 | # Derive package name from input file | |||||
| 150 | # ------------------------------------------------------------ | |||||
| 151 | ||||||
| 152 | 1 | 14 | open my $fh, '<', $input_file | |||
| 153 | or die "Cannot open $input_file: $!"; | |||||
| 154 | ||||||
| 155 | 1 | 0 | my $package_name; | |||
| 156 | 1 | 6 | while (<$fh>) { | |||
| 157 | 1 | 3 | if (/^\s*package\s+([\w:]+)\s*;/) { | |||
| 158 | 1 | 1 | $package_name = $1; | |||
| 159 | 1 | 1 | last; | |||
| 160 | } | |||||
| 161 | } | |||||
| 162 | 1 | 4 | close $fh; | |||
| 163 | ||||||
| 164 | 1 | 1 | die "Could not determine package name from $input_file\n" | |||
| 165 | unless $package_name; | |||||
| 166 | ||||||
| 167 | # ------------------------------------------------------------ | |||||
| 168 | # Generate very basic plans from schema | |||||
| 169 | # ------------------------------------------------------------ | |||||
| 170 | ||||||
| 171 | 1 | 1 | my %plans; | |||
| 172 | ||||||
| 173 | 1 | 2 | foreach my $method (keys %$schemas) { | |||
| 174 | 1 | 1 | my $schema = $schemas->{$method}; | |||
| 175 | ||||||
| 176 | 1 | 3 | my $accessor = $schema->{accessor}{type} // ''; | |||
| 177 | ||||||
| 178 | 1 | 5 | $plans{$method} = { | |||
| 179 | basic_test => 1, | |||||
| 180 | getter_test => $accessor eq 'getter' ? 1 : 0, | |||||
| 181 | setter_test => $accessor eq 'setter' ? 1 : 0, | |||||
| 182 | getset_test => $accessor eq 'getset' ? 1 : 0, | |||||
| 183 | chaining_test => $accessor eq 'setter' ? 1 : 0, | |||||
| 184 | error_handling_test => 0, | |||||
| 185 | context_tests => 0, | |||||
| 186 | }; | |||||
| 187 | } | |||||
| 188 | ||||||
| 189 | # ---------------------------------------------------------- | |||||
| 190 | # Planning Phase | |||||
| 191 | # ---------------------------------------------------------- | |||||
| 192 | ||||||
| 193 | 1 | 1 | print "Planning tests...\n" if $opts{verbose}; | |||
| 194 | ||||||
| 195 | 1 | 3 | my $planner = App::Test::Generator::Planner->new(schemas => $schemas, package => $package_name); | |||
| 196 | ||||||
| 197 | 1 | 1 | my $plans = $planner->plan_all(); | |||
| 198 | ||||||
| 199 | # ---------------------------------------------------------- | |||||
| 200 | # Emission Phase | |||||
| 201 | # ---------------------------------------------------------- | |||||
| 202 | ||||||
| 203 | 1 | 4 | my $emitter = App::Test::Generator::Emitter::Perl->new( | |||
| 204 | package => $package_name, | |||||
| 205 | schema => $schemas, | |||||
| 206 | plans => $plans, | |||||
| 207 | ); | |||||
| 208 | ||||||
| 209 | 1 | 1 | my $code = $emitter->emit(); | |||
| 210 | ||||||
| 211 | # ------------------------------------------------------------ | |||||
| 212 | # Write output file | |||||
| 213 | # ------------------------------------------------------------ | |||||
| 214 | ||||||
| 215 | 1 | 1 | my $test_file = "$opts{test_dir}/01-basic.t"; | |||
| 216 | ||||||
| 217 | 1 | 32 | open my $out, '>', $test_file | |||
| 218 | or die "Cannot write $test_file: $!"; | |||||
| 219 | ||||||
| 220 | 1 | 3 | print $out $code; | |||
| 221 | 1 | 13 | close $out; | |||
| 222 | ||||||
| 223 | 1 | 8 | print "Wrote test file: $test_file\n"; | |||
| 224 | } | |||||
| 225 | ||||||
| 226 | ||||||
| 227 | 1 | 2 | if ($opts{verbose}) { | |||
| 228 | 0 | 0 | print "\nSchemas structure:\n"; | |||
| 229 | 0 | 0 | print Dumper($schemas); | |||
| 230 | } | |||||
| 231 | ||||||
| 232 | 1 | 12 | print "\nDone.\n"; | |||
| 233 | ||||||