File Coverage

blib/lib/Test/Mimic/Recorder.pm
Criterion Covered Total %
statement 175 191 91.6
branch 35 52 67.3
condition 16 23 69.5
subroutine 20 20 100.0
pod 1 1 100.0
total 247 287 86.0


line stmt bran cond sub pod time code
1             package Test::Mimic::Recorder;
2              
3 1     1   91794 use 5.006001; # For open( my $fh, ...
  1         4  
  1         41  
4 1     1   6 use strict;
  1         1  
  1         36  
5 1     1   5 use warnings;
  1         5  
  1         27  
6              
7 1     1   7206 use Devel::EvalError ();
  1         2666  
  1         24  
8 1     1   7 use Cwd qw;
  1         3  
  1         60  
9 1     1   6 use Scalar::Util qw;
  1         2  
  1         62  
10              
11 1         503 use Test::Mimic::Library qw(
12             encode
13             descend
14             stringify
15             gen_arg_key
16             monitor_args
17             init_records
18             load_preferences
19             write_records
20             RETURN
21             EXCEPTION
22             CODE_E
23             SCALAR_CONTEXT
24             VOID_CONTEXT
25             LIST_CONTEXT
26             ARBITRARY
27 1     1   5 );
  1         2  
28              
29             our $VERSION = 0.012_005;
30             our $SuspendRecording = 0; # Turn off recording.
31             my $done_writing = 0;
32              
33             # Data to be stored.
34             my %typeglobs; # Contains recorded data for scalars, arrays, hashes and subroutines in a structure analogous
35             # to the symbol table. The key is the package name.
36             my %extra; # Currently contains only a flattened class hierarchy for each recorded class at
37             # $extra{$class}{'ISA'} as a hash ref. $extra{$recorded_class}{'ISA'}{$other_class} will
38             # exist iff $recorded_class isa $other_class.
39             my @operation_sequence; # An ordered list of recorded operations. The first operation happened first, the
40             # second operation happened second and so forth. This currently only includes
41             # subroutine calls in recorded packages. Orderings of various 'scopes' can later be
42             # extracted from this.
43              
44             # Transient data
45             my $save_to;
46              
47             sub import {
48 1     1   11 my ( $class, $preferences ) = @_;
49              
50 1   50     10 $save_to = $preferences->{'save'} || '.test_mimic_recorder_data';
51              
52             # If we are not being run from Test::Mimic...
53 1 50       4 if ( ! defined( $preferences->{'test_mimic'} ) ) { # Perhaps use caller instead?
54 1         5 init_records();
55 1         10 load_preferences($preferences);
56             }
57              
58             # Call _record_package on each package passing along the package and a list of scalars to record.
59 1         15 for my $package ( keys %{ $preferences->{'packages'} } ) {
  1         6  
60 1   50     6 _record_package( $package, $preferences->{'packages'}->{$package}->{'scalars'} ||= [] );
61             }
62             }
63              
64             # Writes recording to disk. Typically called automatically.
65             sub finish {
66 1     1 1 2064 $done_writing = 1; # Prevents the END block from overwriting what we just wrote.
67              
68             # Move the current directory to the specified directory, creating if needed.
69 1         17 my $top_level = abs_path();
70 1         5 descend($save_to);
71              
72 1 50       199 open( my $fh, '>', 'additional_info.rec' ) or die "Unable to open file: $!";
73 1 50       8 print $fh stringify( [ \%typeglobs, \%extra, \@operation_sequence ] )
74             or die "Unable to write: $!";
75 1 50       8137 close($fh) or die "Unable to close file: $!";
76 1         8 write_records( 'history_from_recorder.rec' );
77              
78             # Undo the change to the current working directory above.
79 1 50       3545 chdir($top_level) or die "Unable to change the current working directory: $!";
80             }
81              
82             # Accepts a package name and a list of scalars in the package to be recorded. Test::Mimic::Recorder will
83             # begin monitoring this package including the passed scalars.
84             sub _record_package {
85 1     1   2 my ( $package, $user_selected_scalars ) = @_;
86            
87 1 50       429 eval("require $package; 1")
88             or die "Failed to load package $package. $@";
89            
90             # Consider every symbol in the package, tie arrays and tie hashes.
91 1         4 my $symbol_table;
92             {
93 1     1   5 no strict 'refs';
  1         2  
  1         427  
  1         2  
94 1         2 $symbol_table = \%{ $package . '::' };
  1         4  
95             }
96              
97 1   50     10 my $fake_package = ( $typeglobs{$package} ||= {} );
98 1         2 for my $symbol ( keys %{$symbol_table} ) {
  1         6  
99              
100 14         328 my $typeglob = \$symbol_table->{$symbol}; # According to Tye it is better to handle glob refs than
101             # globs themselves.
102 14   50     68 my $fake_typeglob = ( $fake_package->{$symbol} ||= {} );
103              
104 14         18 my $symbol_type = reftype( ${$typeglob} );
  14         31  
105 14 100 66     37 if ( ! defined($symbol_type) ) {
    50          
106 12         42 my $pointer_type = reftype($typeglob);
107 12 50       22 if ( $pointer_type eq 'GLOB' ) {
    0          
108             # Tie arrays and hashes.
109 12         21 my $reference = *{$typeglob}{'ARRAY'};
  12         23  
110 12 100       27 if ( defined($reference) ) {
111 2         8 $fake_typeglob->{'ARRAY'} = encode( $reference, 0 );
112             }
113              
114 12         165 $reference = *{$typeglob}{'HASH'};
  12         18  
115 12 100 66     48 if ( defined($reference) && ! ($symbol =~ m/^\w+::$/) ) { #Avoid tying symbol tables!
116 1         4 $fake_typeglob->{'HASH'} = encode( $reference, 0 );
117             }
118             }
119             # Perl apparently sometimes stores subroutine stub declarations as simple
120             # scalars. We would like to leave these alone. (See the Perl 5.10
121             # delta for one reference.)
122             elsif ( $pointer_type ne 'SCALAR' ) {
123 0         0 warn "The symbol <$symbol> in package <$package> with type <$pointer_type> is neither a glob,"
124             . ' constant optimization or subroutine stub declaration. Ignoring and proceeding.';
125             }
126             }
127             # Perl 5.10 optimizes constants by storing them as plain references, not globs initially, so
128             # we handle that here by watching the 'constant' value. This is needed because although the
129             # value itself is constant, the contents of the value may not be. If it is an array reference,
130             # for example, we can modify the backing array.
131             elsif ( $symbol_type eq 'REF' || $symbol_type eq 'SCALAR' ) {
132             # If we are dealing with a simple scalar then it won't be tied anyways. Otherwise, an $at_level
133             # of 1 will start the monitoring/tying on the elements of the aggregate/dereferenced value rather
134             # than the aggregate/reference itself.
135 2         4 $fake_typeglob->{'CONSTANT'} = encode( ${${$typeglob}}, 1 );
  2         2  
  2         8  
136             }
137             else {
138 0         0 warn "The symbol <$symbol> in package <$package> with reftype <$symbol_type> is neither a glob,"
139             . ' constant optimization or subroutine stub declaration. Ignoring and proceeding.';
140             }
141             }
142            
143             # Combine the user selected scalars with the the exported scalars.
144 1         4 my %all_scalars;
145 1 50       14 if ( $package->isa('Exporter') ) {
146 1     1   5 no strict 'refs';
  1         1  
  1         1136  
147 0         0 for my $symbol ( @{ $package . '::EXPORT' }, @{ $package . '::EXPORT_OK' } ) {
  0         0  
  0         0  
148 0 0       0 if ( substr( $symbol, 0, 1 ) eq '$' ) {
149 0         0 $all_scalars{ substr( $symbol, 1 ) } = ARBITRARY;
150             }
151             }
152             }
153 1         9 for my $scalar ( @{$user_selected_scalars} ) {
  1         3  
154 1         4 $all_scalars{$scalar} = ARBITRARY;
155             }
156            
157             # Tie all scalars.
158 1         3 for my $scalar ( keys %all_scalars ) {
159 1         3 my $typeglob = \$symbol_table->{$scalar};
160            
161 1 50       5 if ( reftype($typeglob) eq 'GLOB' ) { # Ignore constant optimizations, handled above in array/hash code.
162 1         1 $fake_package->{$scalar}->{'SCALAR'} = encode( *{$typeglob}{'SCALAR'}, 0 );
  1         6  
163             }
164             }
165            
166             #Handle inheritance issues regarding both isa and can.
167 1         58 my ( $full_ISA, $all_subs ) = _get_hierarchy_info($package);
168 1         3 $extra{$package}{'ISA'} = $full_ISA;
169            
170             # Wrap all subroutines. (Or rather, redefine each subroutine to record the operation of the original.)
171 1         2 for my $sub ( keys %{$all_subs} ) {
  1         4  
172 11         61 my $original_sub = $package->can($sub);
173 11   50     69 my $record_to = ( $fake_package->{$sub}->{'CODE'} ||= {} );
174            
175             # Define the new subroutine
176             my $wrapper_sub = sub {
177            
178             # Discard calls while recording is suspended, i.e. make the call, but don't record it.
179 25 50   25   65 if ($Test::Mimic::Recorder::SuspendRecording) {
180 0         0 goto &{$original_sub};
  0         0  
181             }
182            
183             # Set up the recording storage for this call.
184 25         77 my $arg_key = gen_arg_key($package, $sub, \@_);
185 25   100     393 my $context_to_result = ( $record_to->{$arg_key} ||= [] );
186            
187             # TODO: Query user settings regarding the volatility of the arguments.
188 25         79 my $arg_signature = monitor_args( $package, $sub, \@_ );
189            
190             # Make actual call, trap exceptions or store return.
191 25         989 local $Test::Mimic::Recorder::SuspendRecording = 1; # Suspend recording. We don't wan't to record
192             # internal calls or state modifications.
193 25         38 my $context = wantarray();
194 25         33 my $context_index;
195             my $exception;
196 0         0 my @results;
197 0         0 my $stored_result;
198 0         0 my $failed;
199 25         82 my $eval_error = Devel::EvalError->new();
200             $eval_error->ExpectOne(
201 25         544 eval {
202 25 100       68 if ($context) {
    100          
203 1         2 $context_index = LIST_CONTEXT;
204 1         2 @results = &{$original_sub};
  1         4  
205 1         7 $stored_result = [ RETURN, encode( \@results, 1) ];
206             }
207             elsif (defined $context) {
208 12         17 $context_index = SCALAR_CONTEXT;
209 12         15 $results[0] = &{$original_sub};
  12         34  
210 11         149 $stored_result = [ RETURN, encode( $results[0], 0 ) ];
211             }
212             else {
213 12         21 $context_index = VOID_CONTEXT;
214 12         13 &{$original_sub};
  12         34  
215 12         715 $stored_result = [RETURN];
216             }
217 24         412 1;
218             }
219             );
220 25         910 $failed = $eval_error->Failed();
221 25 100       272 if ( $failed ) {
222 1         4 $exception = ( $eval_error->AllReasons() )[-1];
223 1         10 $stored_result = [ EXCEPTION, encode( $exception, 0 ) ];
224             }
225            
226             # Maintain records
227 25         82 push( @operation_sequence, [ $package, CODE_E, $sub, $arg_key, $context_index ] );
228 25   100     36 push( @{ $context_to_result->[$context_index] ||= [] }, ( $arg_signature, $stored_result ) );
  25         108  
229              
230             # Propagate original behavior
231 25 100       91 if ( $failed ) {
    100          
    100          
232 1         7 die $exception;
233             }
234             elsif ($context) {
235 1         13 return @results;
236             }
237             elsif ( defined $context ) {
238 11         40 return $results[0];
239             }
240             else {
241 12         41 return;
242             }
243 11         70 };
244            
245             # Handle prototypes
246 11         14 my $replacement;
247 11         25 my $proto = prototype($original_sub);
248 11 100   1   933 $replacement = eval "package $package; sub " . ( defined($proto) ? "($proto) " : '' )
  1         58  
  1         296  
  0         0  
  0         0  
  3         165  
  2         140  
  0         0  
  9         969  
  2         100  
  0         0  
  7         1220  
249             . "{ return \$wrapper_sub->(\@_); };";
250             # Saying 'package' in the eval allows us to record subroutines used by sort. If we don't $a and $b
251             # are not imported properly.
252 11         39 $extra{$package}{'PROTOTYPES'}{$sub} = $proto;
253            
254             # Redefine the original subroutine
255             {
256 1     1   14 no warnings 'redefine';
  1         3  
  1         84  
  11         20  
257 1     1   11 no strict 'refs';
  1         3  
  1         197  
258 11         12 *{ $package . '::' . $sub } = $replacement;
  11         67  
259             }
260             }
261             }
262              
263             # Accepts a class name. Returns the class hierarchy flattened into a hash ref and a list of all subroutines
264             # the class responds too (including inherited subroutines, excluding AUTOLOADED subroutines) also as a hash
265             # ref. An arbitrary element will exist in the proper hash iff the class isa element or the class can element
266             # for classes and subroutines respectively. The subroutine names are not fully qualified.
267             sub _get_hierarchy_info {
268 5     5   7 my ($class) = @_;
269            
270 5         11 my %full_ISA = ( $class => ARBITRARY ); # Certainly $class isa $class.
271 5         7 my %full_subs;
272            
273             # Find all the subroutines declared in the class.
274             my $symbol_table;
275             {
276 1     1   5 no strict 'refs';
  1         1  
  1         121  
  5         6  
277 5         4 $symbol_table = \%{ $class . '::' };
  5         15  
278             }
279 5         7 for my $symbol ( keys %{$symbol_table} ) {
  5         13  
280 28         38 my $typeglob = \$symbol_table->{$symbol};
281              
282             # Note if the symbol contains a subroutine.
283             # Ignore constant optimizations, handled in _record_package except for the
284             # case of inherited constants. Do we need to take care of this case?
285 28 100 100     87 if ( ( reftype($typeglob) eq 'GLOB' ) && defined( *{$typeglob}{'CODE'} ) ) {
  26         103  
286 11         161 $full_subs{$symbol} = ARBITRARY;
287             }
288             }
289            
290             # Get a copy of the actual @ISA array.
291 5         10 my @true_ISA;
292             {
293 1     1   10 no strict 'refs';
  1         2  
  1         178  
  5         4  
294 5         6 @true_ISA = @{ $class . '::ISA' };
  5         29  
295             }
296            
297             # Look through the class hierarchy for all ancestor classes and inherited subroutines.
298 5         64 for my $parent (@true_ISA) {
299 4 50       9 if ( ! exists $full_ISA{$parent} ) {
300 4         22 my ( $parent_full_ISA, $parent_full_subs ) = _get_hierarchy_info($parent);
301            
302             # Merge in the parent information.
303 4         5 @full_ISA{ keys %{$parent_full_ISA} } = values %{$parent_full_ISA};
  4         11  
  4         9  
304 4         6 @full_subs{ keys %{$parent_full_subs} } = values %{$parent_full_subs};
  4         19  
  4         8  
305             }
306             }
307            
308 5         14 return ( \%full_ISA, \%full_subs );
309             }
310              
311             # Write recording to disk
312             END {
313 1 50   1   33087 finish()
314             if ( ! $done_writing );
315             }
316              
317             1;
318             __END__