line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Test::WWW::Mechanize::Maypole; |
2
|
1
|
|
|
1
|
|
35263
|
use strict; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
31
|
|
3
|
1
|
|
|
1
|
|
6
|
use warnings; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
28
|
|
4
|
|
|
|
|
|
|
|
5
|
1
|
|
|
1
|
|
802
|
use HTTP::Status(); |
|
1
|
|
|
|
|
3918
|
|
|
1
|
|
|
|
|
29
|
|
6
|
1
|
|
|
1
|
|
835
|
use HTTP::Headers::Util; |
|
1
|
|
|
|
|
793
|
|
|
1
|
|
|
|
|
93
|
|
7
|
1
|
|
|
1
|
|
762
|
use URI; |
|
1
|
|
|
|
|
7797
|
|
|
1
|
|
|
|
|
26
|
|
8
|
1
|
|
|
1
|
|
834
|
use UNIVERSAL::require; |
|
1
|
|
|
|
|
1898
|
|
|
1
|
|
|
|
|
13
|
|
9
|
1
|
|
|
1
|
|
1057
|
use NEXT; |
|
1
|
|
|
|
|
2324
|
|
|
1
|
|
|
|
|
8
|
|
10
|
|
|
|
|
|
|
|
11
|
1
|
|
|
1
|
|
1109
|
use Test::WWW::Mechanize; |
|
1
|
|
|
|
|
235150
|
|
|
1
|
|
|
|
|
23
|
|
12
|
1
|
|
|
1
|
|
1701
|
use Class::Data::Inheritable; |
|
1
|
|
|
|
|
353
|
|
|
1
|
|
|
|
|
426
|
|
13
|
|
|
|
|
|
|
|
14
|
1
|
|
|
1
|
|
50
|
use base qw/ Test::WWW::Mechanize Class::Data::Inheritable /; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
275
|
|
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
__PACKAGE__->mk_classdata( '_the_app' ); |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
our $VERSION = '0.23'; |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
sub import |
21
|
|
|
|
|
|
|
{ |
22
|
1
|
|
|
1
|
|
10
|
my ( $class, $app, @db_args ) = @_; |
23
|
|
|
|
|
|
|
|
24
|
1
|
50
|
|
|
|
5
|
if ( @db_args ) |
25
|
|
|
|
|
|
|
{ |
26
|
0
|
|
|
|
|
0
|
my $args = join ':', @db_args; |
27
|
|
|
|
|
|
|
|
28
|
0
|
|
|
|
|
0
|
eval "package $app; |
29
|
|
|
|
|
|
|
sub setup { shift->NEXT::DISTINCT::setup( '$args' ) }"; # qw(@db_args) fails |
30
|
0
|
0
|
|
|
|
0
|
die $@ if $@; |
31
|
|
|
|
|
|
|
} |
32
|
|
|
|
|
|
|
|
33
|
1
|
|
|
|
|
4
|
$class->_the_app( $app ); |
34
|
|
|
|
|
|
|
|
35
|
1
|
50
|
|
|
|
15
|
$app->require or die "Couldn't load Maypole app '$app': $@"; |
36
|
|
|
|
|
|
|
|
37
|
0
|
|
|
|
|
|
my @exports = qw/ send_output parse_location get_template_root parse_args /; |
38
|
|
|
|
|
|
|
|
39
|
1
|
|
|
1
|
|
6
|
no strict 'refs'; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
871
|
|
40
|
0
|
|
|
|
|
|
*{"$app\::$_"} = \&$_ for @exports; |
|
0
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
} |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
=head1 NAME |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
Test::WWW::Mechanize::Maypole - Test::WWW::Mechanize for Maypole |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=head1 SYNOPSIS |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
use Test::WWW::Mechanize::Maypole 'BeerDB'; |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
# or load a test database instead of the one configured in BeerDB.pm: |
52
|
|
|
|
|
|
|
# |
53
|
|
|
|
|
|
|
# use Test::WWW::Mechanize::Maypole 'BeerDB', 'dbi:SQLite:test-beerdb.db'; |
54
|
|
|
|
|
|
|
# use Test::WWW::Mechanize::Maypole 'BeerDB', 'dbi:mysql:beer_d_b', 'dhoworth', 'password'; |
55
|
|
|
|
|
|
|
|
56
|
|
|
|
|
|
|
$ENV{MAYPOLE_TEMPLATES} = 'path/to/templates'; |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
my $mech = Test::WWW::Mechanize::Maypole->new; |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
# |
61
|
|
|
|
|
|
|
# basic tests: |
62
|
|
|
|
|
|
|
# |
63
|
|
|
|
|
|
|
$mech->get_ok( "http://localhost/beerdb/" ); |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
is( $mech->ct, "text/html" ); |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
$mech->content_contains( 'This is the frontpage' ); |
68
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
# |
70
|
|
|
|
|
|
|
# logging in and storing cookies: |
71
|
|
|
|
|
|
|
# |
72
|
|
|
|
|
|
|
$mech->get_ok("http://localhost/beerdb/customer/buybeer"); |
73
|
|
|
|
|
|
|
$mech->content_contains( 'Login to BeerDB', 'got login page' ); |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
# specify which form we're interested in |
76
|
|
|
|
|
|
|
$mech->form_number(1); # the 1st form |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
# fill in credentials |
79
|
|
|
|
|
|
|
$mech->field( 'username' => 'landlord' ); |
80
|
|
|
|
|
|
|
$mech->field( 'password' => 'handpump' ); |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
# get a HTTP::Response back |
83
|
|
|
|
|
|
|
my $response = $mech->click_button( name => 'submit' ); |
84
|
|
|
|
|
|
|
like( $response->content, qr/Shop for beer/, 'got customer/buybeer page' ); |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
# check our cookies give access to other pages |
87
|
|
|
|
|
|
|
$mech->get_ok( "http://localhost/beerdb/customer/edit" ); |
88
|
|
|
|
|
|
|
$mech->content_contains( 'Update your details', "got customer account edit page"); |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# ... see Test::WWW::Mechanize for many more test methods |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=head1 DESCRIPTION |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
By inheriting from L, this module provides two key benefits |
96
|
|
|
|
|
|
|
over using L in test scripts. First, it inherits a plethora of methods |
97
|
|
|
|
|
|
|
for testing web content. Second, cookies are handled transparently, allowing |
98
|
|
|
|
|
|
|
you to test applications that use cookie-based sessions and authentication. |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
Testing web applications has always been a bit tricky, normally |
101
|
|
|
|
|
|
|
starting a web server for your application and making real HTTP |
102
|
|
|
|
|
|
|
requests to it. This module allows you to test L web |
103
|
|
|
|
|
|
|
applications but does not start a server or issue HTTP |
104
|
|
|
|
|
|
|
requests. Instead, it passes the HTTP request parameters directly to |
105
|
|
|
|
|
|
|
L. Thus you do not need to use a real hostname: |
106
|
|
|
|
|
|
|
"http://localhost/" will do. |
107
|
|
|
|
|
|
|
|
108
|
|
|
|
|
|
|
This makes testing fast and easy. L provides |
109
|
|
|
|
|
|
|
functions for common web testing scenarios. For example: |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
$mech->get_ok( $page ); |
112
|
|
|
|
|
|
|
$mech->title_is( "Invoice Status", "Make sure we're on the invoice page" ); |
113
|
|
|
|
|
|
|
$mech->content_contains( "David Baird", "My name somewhere" ); |
114
|
|
|
|
|
|
|
$mech->content_like( qr/(cpan|perl)\.org/, "Link to perl.org or CPAN" ); |
115
|
|
|
|
|
|
|
|
116
|
|
|
|
|
|
|
This module supports cookies automatically. |
117
|
|
|
|
|
|
|
|
118
|
|
|
|
|
|
|
=head1 LOADING |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
To use this module you must pass it the name of the application. |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
Additionally, you can pass an alternate set of database connection parameters, and |
123
|
|
|
|
|
|
|
these will override the settings configured in your application. Useful for connecting |
124
|
|
|
|
|
|
|
to a test database without having to alter your production code. This won't work if |
125
|
|
|
|
|
|
|
your application calls C inside a C block. |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=head1 CONSTRUCTOR |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
=head2 new |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
Inherited from L, which passes any parameters through to |
132
|
|
|
|
|
|
|
L. |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
Note that the name of the Maypole application should be passed to the C |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
use Test::WWW::Mechanize::Maypole 'BeerDB'; |
137
|
|
|
|
|
|
|
my $mech = Test::WWW::Mechanize::Maypole->new; |
138
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
=head1 ENVIRONMENT |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
Set C<$ENV{MAYPOLE_TEMPLATES}> to the path where the templates for the application |
142
|
|
|
|
|
|
|
can be found. Defaults to C<'.'>. |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
=head1 METHODS |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
Please see the documentation for L. |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
=cut |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
sub _make_request |
151
|
|
|
|
|
|
|
{ |
152
|
0
|
|
|
0
|
|
|
my ( $self, $request ) = @_; |
153
|
|
|
|
|
|
|
|
154
|
0
|
0
|
|
|
|
|
$self->cookie_jar->add_cookie_header($request) if $self->cookie_jar; |
155
|
|
|
|
|
|
|
|
156
|
|
|
|
|
|
|
# make an HTTP::Response object, to be populated during the handler() call |
157
|
0
|
|
|
|
|
|
my $response = HTTP::Response->new; |
158
|
0
|
|
|
|
|
|
$response->date( time ); |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# parse_location() normally takes the url from @ARGV, here we provide $request. |
161
|
|
|
|
|
|
|
# $response is taken by send_output |
162
|
0
|
|
|
|
|
|
local @ARGV = ( $request, $response ); |
163
|
|
|
|
|
|
|
|
164
|
|
|
|
|
|
|
# handler() calls send_output with no args, so we provide $response via @ARGV |
165
|
0
|
|
|
|
|
|
my $status = $self->_the_app->handler; |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
# Translate Maypole codes to HTTP::Status codes. Maypole only has 2 codes, OK (0) |
168
|
|
|
|
|
|
|
# and everything else (-1). We'll assume -1 is an error. Note that other codes can |
169
|
|
|
|
|
|
|
# be returned by custom application code - we assume anything else is a proper |
170
|
|
|
|
|
|
|
# HTTP status |
171
|
0
|
0
|
|
|
|
|
if ( defined $status ) |
172
|
|
|
|
|
|
|
{ |
173
|
0
|
0
|
|
|
|
|
$status = 200 if $status == 0; |
174
|
0
|
0
|
|
|
|
|
$status = 500 if $status == -1; |
175
|
|
|
|
|
|
|
} |
176
|
|
|
|
|
|
|
else |
177
|
|
|
|
|
|
|
{ |
178
|
0
|
|
|
|
|
|
warn "Undefined response code"; |
179
|
0
|
|
|
|
|
|
$status = 500; |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
# $response has now been populated during the handler() call |
183
|
0
|
|
|
|
|
|
$response->code( $status ); |
184
|
0
|
|
|
|
|
|
$response->message( HTTP::Status::status_message( $status ) ); |
185
|
|
|
|
|
|
|
|
186
|
0
|
|
|
|
|
|
$response->header( 'Content-Base', $request->uri ); |
187
|
0
|
|
|
|
|
|
$response->request( $request ); |
188
|
|
|
|
|
|
|
|
189
|
0
|
0
|
|
|
|
|
$self->cookie_jar->extract_cookies($response) if $self->cookie_jar; |
190
|
|
|
|
|
|
|
|
191
|
0
|
|
|
|
|
|
return $response; |
192
|
|
|
|
|
|
|
} |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=head2 Exported methods |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
These methods are exported into the application's namespace, and override methods that would |
198
|
|
|
|
|
|
|
otherwise be inherited from Maypole or the Maypole frontend. |
199
|
|
|
|
|
|
|
|
200
|
|
|
|
|
|
|
You will not normally need to use these methods in your test scripts. |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
If you need to replace these methods with custom versions, let me know, and I'll make exporting |
203
|
|
|
|
|
|
|
more flexible. |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
=over 4 |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
=item send_output |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
=item parse_location |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=item parse_args |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
=item get_template_root |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=back |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
=cut |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
# Called by Maypole::handler(), with no arguments, so $response is placed in @ARGV for |
220
|
|
|
|
|
|
|
# retrieval here. This method, and _make_request, are the only places that use the |
221
|
|
|
|
|
|
|
# $response object. |
222
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
# Grabs Maypole::Headers and populates the HTTP::Response object. |
224
|
|
|
|
|
|
|
sub send_output |
225
|
|
|
|
|
|
|
{ |
226
|
0
|
|
|
0
|
1
|
|
my ( $maypole ) = @_; |
227
|
|
|
|
|
|
|
|
228
|
0
|
|
|
|
|
|
my $response = shift @ARGV; |
229
|
|
|
|
|
|
|
|
230
|
0
|
0
|
|
|
|
|
$response->content_type( |
231
|
|
|
|
|
|
|
$maypole->{content_type} =~ m/^text/ |
232
|
|
|
|
|
|
|
? $maypole->{content_type} . "; charset=" . $maypole->{document_encoding} |
233
|
|
|
|
|
|
|
: $maypole->{content_type} |
234
|
|
|
|
|
|
|
); |
235
|
|
|
|
|
|
|
|
236
|
1
|
|
|
1
|
|
7
|
$response->content_length( do { use bytes; length $maypole->{output} } ); |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
10
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
# if there are cookies, this is where they get passed on |
239
|
0
|
|
|
|
|
|
foreach ($maypole->headers_out->field_names) |
240
|
|
|
|
|
|
|
{ |
241
|
0
|
0
|
|
|
|
|
next if /^Content-(Type|Length)/; |
242
|
0
|
|
|
|
|
|
$response->header( $_ => $maypole->headers_out->get($_) ); |
243
|
|
|
|
|
|
|
} |
244
|
|
|
|
|
|
|
|
245
|
0
|
|
|
|
|
|
$response->content( $maypole->{output} ); |
246
|
|
|
|
|
|
|
} |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
# Called by Maypole::handler() with no arguments. |
249
|
|
|
|
|
|
|
sub parse_location |
250
|
|
|
|
|
|
|
{ |
251
|
0
|
|
|
0
|
1
|
|
my ( $self ) = @_; |
252
|
|
|
|
|
|
|
|
253
|
0
|
|
|
|
|
|
my $request = shift @ARGV; |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
# This is a HTTP::Headers object. |
256
|
0
|
|
|
|
|
|
my $headers_in = $request->headers; |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
# Maypole::Headers is a simple subclass of HTTP::Headers |
259
|
0
|
|
|
|
|
|
bless $headers_in, 'Maypole::Headers'; |
260
|
|
|
|
|
|
|
|
261
|
0
|
|
|
|
|
|
$self->headers_in( $headers_in ); |
262
|
|
|
|
|
|
|
|
263
|
0
|
|
|
|
|
|
my $uri = $request->uri; |
264
|
|
|
|
|
|
|
|
265
|
0
|
|
|
|
|
|
( my $uri_base = $self->config->uri_base ) =~ s:/$::; |
266
|
|
|
|
|
|
|
|
267
|
0
|
|
|
|
|
|
my $root = URI->new( $uri_base )->path; |
268
|
|
|
|
|
|
|
|
269
|
0
|
|
|
|
|
|
$self->{path} = $uri->path; |
270
|
0
|
|
|
|
|
|
$self->{path} =~ s:^$root/?::i; |
271
|
|
|
|
|
|
|
|
272
|
0
|
|
|
|
|
|
$self->parse_path; |
273
|
0
|
|
|
|
|
|
$self->parse_args( $request ); |
274
|
|
|
|
|
|
|
} |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
sub parse_args |
277
|
|
|
|
|
|
|
{ |
278
|
0
|
|
|
0
|
1
|
|
my ( $self, $request ) = @_; |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
# this code stolen from Catalyst::Engine::HTTP::Base::prepare_parameters(), |
281
|
|
|
|
|
|
|
# with **file uploads removed** |
282
|
|
|
|
|
|
|
|
283
|
0
|
|
|
|
|
|
my @params; |
284
|
|
|
|
|
|
|
|
285
|
0
|
|
|
|
|
|
push( @params, $request->uri->query_form ); |
286
|
|
|
|
|
|
|
|
287
|
0
|
0
|
|
|
|
|
if ( $request->content_type eq 'application/x-www-form-urlencoded' ) |
288
|
|
|
|
|
|
|
{ |
289
|
0
|
|
|
|
|
|
my $uri = URI->new('http:'); |
290
|
0
|
|
|
|
|
|
$uri->query( $request->content ); |
291
|
0
|
|
|
|
|
|
push( @params, $uri->query_form ); |
292
|
|
|
|
|
|
|
} |
293
|
|
|
|
|
|
|
|
294
|
0
|
0
|
|
|
|
|
if ( $request->content_type eq 'multipart/form-data' ) |
295
|
|
|
|
|
|
|
{ |
296
|
0
|
|
|
|
|
|
for my $part ( $request->parts ) |
297
|
|
|
|
|
|
|
{ |
298
|
0
|
|
|
|
|
|
my $disposition = $part->header('Content-Disposition'); |
299
|
0
|
|
|
|
|
|
my %parameters = @{ ( HTTP::Headers::Util::split_header_words($disposition) )[0] }; |
|
0
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
|
301
|
0
|
0
|
|
|
|
|
die 'File uploads not supported' if $parameters{filename}; |
302
|
|
|
|
|
|
|
|
303
|
0
|
|
|
|
|
|
push( @params, $parameters{name}, $part->content ); |
304
|
|
|
|
|
|
|
} |
305
|
|
|
|
|
|
|
} |
306
|
|
|
|
|
|
|
|
307
|
0
|
|
|
|
|
|
my %parameters; |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
# this from Catalyst::Request::param() |
310
|
0
|
|
|
|
|
|
while ( my ( $field, $value ) = splice( @params, 0, 2 ) ) |
311
|
|
|
|
|
|
|
{ |
312
|
0
|
0
|
|
|
|
|
next unless defined $field; |
313
|
|
|
|
|
|
|
|
314
|
0
|
0
|
|
|
|
|
if ( exists $parameters{$field} ) |
315
|
|
|
|
|
|
|
{ |
316
|
0
|
|
|
|
|
|
for ( $parameters{$field} ) |
317
|
|
|
|
|
|
|
{ |
318
|
0
|
0
|
|
|
|
|
$_ = [$_] unless ref($_) eq 'ARRAY'; |
319
|
0
|
|
|
|
|
|
push( @$_, $value ); |
320
|
|
|
|
|
|
|
} |
321
|
|
|
|
|
|
|
} |
322
|
|
|
|
|
|
|
else |
323
|
|
|
|
|
|
|
{ |
324
|
0
|
|
|
|
|
|
$parameters{$field} = $value; |
325
|
|
|
|
|
|
|
} |
326
|
|
|
|
|
|
|
} |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
# back to Maypole... |
329
|
0
|
|
|
|
|
|
$self->params( \%parameters ); |
330
|
0
|
|
|
|
|
|
$self->query( \%parameters ); |
331
|
|
|
|
|
|
|
} |
332
|
|
|
|
|
|
|
|
333
|
0
|
0
|
|
0
|
1
|
|
sub get_template_root { $ENV{MAYPOLE_TEMPLATES} || '.' } |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
1; |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
__END__ |