File Coverage

blib/lib/LWP/UserAgent/Snapshot.pm
Criterion Covered Total %
statement 21 81 25.9
branch 0 26 0.0
condition 0 6 0.0
subroutine 7 14 50.0
pod 3 3 100.0
total 31 130 23.8


line stmt bran cond sub pod time code
1             package LWP::UserAgent::Snapshot;
2 1     1   1386 use strict;
  1         3  
  1         43  
3 1     1   5 use warnings;
  1         2  
  1         35  
4 1     1   4 use Carp;
  1         2  
  1         84  
5 1     1   6 use Digest::MD5 ();
  1         2  
  1         15  
6 1     1   994 use HTTP::Response;
  1         42801  
  1         39  
7 1     1   16 use base 'LWP::UserAgent';
  1         3  
  1         1266  
8              
9 1     1   22896 use version; our $VERSION = qv('0.2');
  1         2152  
  1         6  
10              
11             =head1 NAME
12              
13             LWP::UserAgent::Snapshot - modifies the behaviour of C to record and playback data.
14              
15             =head1 SYNOPSIS
16              
17             use WWW::Mechanize;
18             use LWP::UserAgent::Snapshot;
19              
20             @WWW::Mechanize::ISA = ('LWP::UserAgent::Snapshot');
21              
22             my $mech = WWW::Mechanize->new;
23              
24             $mech->record_to("data_dir"); # turn on recording to data_dir/
25              
26             # naviate some web pages
27              
28             WWW::Mechanize->record_to(undef); # turn off recording
29             WWW::Mechanize->mock_from("data_dir"); # turn on playback
30              
31             # Navigating the same urls should now work as before, but without
32             # any network access. This is useful for testing.
33              
34              
35             =head1 DESCRIPTION
36              
37             If this subclass of C is inserted into the C<@ISA>
38             inheritance list of C, it allows it to record request
39             and response data to a set of files, then play back responses from
40             that data.
41              
42             For suggestions on the use of this class in testing, see
43             L.
44              
45             =cut
46              
47             our ($MOCK_DIR, $DUMP_DIR);
48             our $INDEX = 0;
49              
50             # reads the content of a file as a single scalar
51              
52             sub _read_from_file
53             {
54 0     0     my $file = shift;
55 0 0         Carp::croak "can't open file $file: $!"
56             unless open my $in, "<", $file;
57 0           local $/;
58 0           my $content = <$in>;
59 0           close $in;
60 0           utime +(time) x 2, $file; # touch file, so we can see which ones were used
61 0           return $content;
62             }
63              
64             # writes a scalar as the content of a file
65              
66             sub _write_to_file
67             {
68 0     0     my $file = shift;
69 0 0         Carp::croak "can't open file $file: $!"
70             unless open my $out, ">", $file;
71 0           print $out @_;
72 0           close $out;
73             }
74              
75             # appends a scalar to the content of a file
76              
77             sub _append_to_file
78             {
79 0     0     my $file = shift;
80 0 0         Carp::croak "can't open file $file: $!"
81             unless open my $out, ">>", $file;
82 0           print $out @_;
83 0           close $out;
84             }
85              
86              
87             # a mock version of simple_request which gets its responses from $MOCK_DIR
88             # based on the MD5 hash of the request
89              
90             sub _mock_simple_request
91             {
92 0 0   0     return shift->SUPER::simple_request(@_)
93             unless $MOCK_DIR;
94              
95 0           my ($self, $request, $content_handler, $read_size_hint) = @_;
96              
97 0           my $uri = $request->uri;
98 0           my $method = $request->method;
99             # print ">>> $INDEX $method $uri\n"; # DEBUG
100              
101 0           my $digest = Digest::MD5::md5_hex($request->as_string);
102 0           $request = $self->prepare_request($request);
103              
104 0           my @response_file = glob "$MOCK_DIR/$digest-response-*";
105 0 0         Carp::croak "no cached response for request digest $digest:\n",$request->as_string
106             unless @response_file;
107 0 0         Carp::carp "multiple cached responses for request digest $digest to ",
108             $request->uri,", using first" if @response_file>1;
109              
110 0           my $response = HTTP::Response->parse(_read_from_file $response_file[0]);
111 0           $response->request($request);
112              
113 0           my $cookie_jar = $self->cookie_jar;
114 0 0         $cookie_jar->extract_cookies($response) if $cookie_jar;
115              
116 0           my $response_status = $response->status_line;
117             # print ">>> $INDEX status $response_status $digest\n"; # DEBUG
118              
119             # handle extra arguments
120 0 0         if ($content_handler)
121             {
122 0 0         if (ref $content_handler eq 'CODE')
123             {
124 0           $content_handler->($response->content(undef));
125             }
126             else
127             {
128 0 0         Carp::croak "could not open file '$content_handler' for writing: $!"
129             unless open my $fh, ">", $content_handler;
130 0           print $fh $response->content(undef);
131 0           close $fh;
132             }
133             }
134            
135              
136 0           return $response;
137             }
138              
139              
140             =head1 CLASS METHODS
141              
142             =head2 C<< $class->record_to($dir) >>
143              
144             If C<$dir> is supplied, turns on recording to that directory. Otherwise,
145             turns off recording.
146              
147             =cut
148              
149             sub record_to
150             {
151 0     0 1   my $class = shift;
152 0           my $dir = shift;
153 0 0 0       Carp::croak "no such directory '$dir'" unless !defined $dir or -d $dir;
154              
155 0           $DUMP_DIR = $dir;
156             }
157              
158              
159             =head2 C<< $class->mock_from($dir) >>
160              
161             If C<$dir> is supplied, turns on playback from that
162             directory. Otherwise, turns off playback.
163              
164             =cut
165              
166             sub mock_from
167             {
168 0     0 1   my $class = shift;
169 0           my $dir = shift;
170 0 0 0       Carp::croak "no such directory '$dir'" unless !defined $dir or -d $dir;
171              
172 0           $MOCK_DIR = $dir;
173             }
174              
175              
176              
177              
178             =head1 PUBLIC INSTANCE METHODS
179              
180             =head2 C<< $response = $obj->simple_request($request) >>
181              
182             Overrides C<< LWP::UserAgent->simple_request >> and implements the
183             recording/playback mechanism, when enabled.
184              
185             =cut
186              
187             sub simple_request
188             {
189 0     0 1   my $self = shift;
190 0           my $request = shift;
191 0           $INDEX++;
192              
193 0 0         return $self->_mock_simple_request($request, @_)
194             unless $DUMP_DIR;
195              
196 0           my $digest = Digest::MD5::md5_hex($request->as_string);
197              
198 0           my $request_file = sprintf "$DUMP_DIR/$digest-request-%03d.txt", $INDEX;
199 0           my $response_file = sprintf "$DUMP_DIR/$digest-response-%03d.html", $INDEX;
200 0           my $index_file = "$DUMP_DIR/index.txt";
201              
202 0           _write_to_file $request_file, $request->as_string;
203              
204 0           my $response = $self->_mock_simple_request($request, @_);
205              
206 0           _write_to_file $response_file, $response->as_string;
207              
208 0           my $uri = $request->uri;
209 0           my $method = $request->method;
210 0           my $response_status = $response->code;
211 0           _append_to_file $index_file, "$digest $method $uri $response_status\n";
212              
213 0           return $response;
214             }
215              
216             =head1 CAVEATS
217              
218             Because we associate each URL visited with its content as downloaded
219             on the first visit, this means we assume the website does not change -
220             in particular, that a given URL's content does not depend on when it's
221             visited, by what route, or other stateful information.
222              
223             =head1 SEE ALSO
224              
225             L and L for general information.
226              
227             Similar tools include the unix C command.
228              
229             =head1 AUTHOR
230              
231             Nick Woolley C<< >>
232              
233             =head1 LICENCE AND COPYRIGHT
234              
235             Copyright (c) 2008, Nick Woolley C<< >>. All rights reserved.
236              
237             This module is free software; you can redistribute it and/or
238             modify it under the same terms as Perl itself. See L.
239              
240              
241             =head1 DISCLAIMER OF WARRANTY
242              
243             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
244             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
245             OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
246             PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
247             EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
248             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
249             ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
250             YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
251             NECESSARY SERVICING, REPAIR, OR CORRECTION.
252              
253             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
254             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
255             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
256             LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
257             OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
258             THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
259             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
260             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
261             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
262             SUCH DAMAGES.
263              
264              
265             =cut
266              
267             1;