line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Test::Nightly; |
2
|
|
|
|
|
|
|
|
3
|
7
|
|
|
7
|
|
223769
|
use strict; |
|
7
|
|
|
|
|
19
|
|
|
7
|
|
|
|
|
301
|
|
4
|
7
|
|
|
7
|
|
148
|
use warnings; |
|
7
|
|
|
|
|
13
|
|
|
7
|
|
|
|
|
377
|
|
5
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
our $VERSION = '0.03'; |
7
|
|
|
|
|
|
|
|
8
|
7
|
|
|
7
|
|
39
|
use Carp; |
|
7
|
|
|
|
|
15
|
|
|
7
|
|
|
|
|
821
|
|
9
|
7
|
|
|
7
|
|
48
|
use File::Spec; |
|
7
|
|
|
|
|
15
|
|
|
7
|
|
|
|
|
183
|
|
10
|
7
|
|
|
7
|
|
14874
|
use File::Find::Rule; |
|
7
|
|
|
|
|
73544
|
|
|
7
|
|
|
|
|
75
|
|
11
|
|
|
|
|
|
|
|
12
|
7
|
|
|
7
|
|
4483
|
use Test::Nightly::Test; |
|
7
|
|
|
|
|
267
|
|
|
7
|
|
|
|
|
93
|
|
13
|
7
|
|
|
7
|
|
1008
|
use Test::Nightly::Email; |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
31
|
|
14
|
7
|
|
|
7
|
|
4458
|
use Test::Nightly::Report; |
|
7
|
|
|
|
|
26
|
|
|
7
|
|
|
|
|
76
|
|
15
|
|
|
|
|
|
|
|
16
|
7
|
|
|
7
|
|
271
|
use base qw(Test::Nightly::Base Class::Accessor::Fast); |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
5441
|
|
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
my @methods = qw( |
19
|
|
|
|
|
|
|
base_directories |
20
|
|
|
|
|
|
|
email_report |
21
|
|
|
|
|
|
|
build_script |
22
|
|
|
|
|
|
|
build_type |
23
|
|
|
|
|
|
|
modules |
24
|
|
|
|
|
|
|
report_output |
25
|
|
|
|
|
|
|
report_template |
26
|
|
|
|
|
|
|
test |
27
|
|
|
|
|
|
|
test_directory_format |
28
|
|
|
|
|
|
|
test_file_format |
29
|
|
|
|
|
|
|
test_report |
30
|
|
|
|
|
|
|
version_result |
31
|
|
|
|
|
|
|
install_module |
32
|
|
|
|
|
|
|
skip_tests |
33
|
|
|
|
|
|
|
); |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
__PACKAGE__->mk_accessors(@methods); |
36
|
|
|
|
|
|
|
my @run_these = qw(version_control run_tests coverage_report generate_report); |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
=head1 NAME |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
Test::Nightly - Run all your tests and produce a report on the results. |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=head1 DESCRIPTION |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
The idea behind this module is to have one script, most probably a cron job, to run all your tests once a night (or once a week). This module will then produce a report on the whether those tests passed or failed. From this report you can see at a glance what tests are failing. This is alpha software! Please try it out, email me bugs suggestions etc. |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
=head1 SYNOPSIS |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
# SCENARIO ONE # |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
Pass in all the options direct into the constructor. |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
use Test::Nightly; |
53
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
my $nightly = Test::Nightly->new({ |
55
|
|
|
|
|
|
|
base_directories => ['/base/dir/from/which/to/search/for/modules/'], |
56
|
|
|
|
|
|
|
run_tests => {}, |
57
|
|
|
|
|
|
|
generate_report => { |
58
|
|
|
|
|
|
|
email_report => { |
59
|
|
|
|
|
|
|
to => 'kirstinbettiol@gmail.com', |
60
|
|
|
|
|
|
|
}, |
61
|
|
|
|
|
|
|
report_output => '/report/output/dir/test_report.html', |
62
|
|
|
|
|
|
|
}, |
63
|
|
|
|
|
|
|
debug => 1, |
64
|
|
|
|
|
|
|
}); |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
# SCENARIO TWO # |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
Call each method individually. |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
use Test::Nightly; |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
my $nightly = Test::Nightly->new({ |
73
|
|
|
|
|
|
|
base_directories => ['/base/dir/from/which/to/search/for/modules/'], |
74
|
|
|
|
|
|
|
}); |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
$nightly->run_tests(); |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
$nightly->generate_report({ |
79
|
|
|
|
|
|
|
email_report => { |
80
|
|
|
|
|
|
|
to => 'kirstinbettiol@gmail.com', |
81
|
|
|
|
|
|
|
}, |
82
|
|
|
|
|
|
|
report_output => '/report/output/dir/test_report.html', |
83
|
|
|
|
|
|
|
}); |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
# SCENARIO THREE |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
Use build instead of make. |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
use Test::Nightly; |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
my $nightly = Test::Nightly->new({ |
92
|
|
|
|
|
|
|
base_directories => ['/base/dir/from/which/to/search/for/modules/'], |
93
|
|
|
|
|
|
|
build_script => 'Build.PL', |
94
|
|
|
|
|
|
|
run_tests => { |
95
|
|
|
|
|
|
|
build_type => 'build', |
96
|
|
|
|
|
|
|
}, |
97
|
|
|
|
|
|
|
}); |
98
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
=cut |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
=head2 new() |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
my $nightly = Test::Nightly->new({ |
104
|
|
|
|
|
|
|
base_directories => \@directories, # Required. Array of base directories to search in. |
105
|
|
|
|
|
|
|
build_script => 'Build.PL', # Defaults to 'Makefile.PL'. |
106
|
|
|
|
|
|
|
run_tests => { |
107
|
|
|
|
|
|
|
test_directory_format => ['t/', 'tests/'], # Optional, defaults to 't/'. |
108
|
|
|
|
|
|
|
test_file_format => ['.t', '.pl'], # Optional, defaults to '.t'. |
109
|
|
|
|
|
|
|
build_type => 'make', # || 'build'. Defaults to 'make'. |
110
|
|
|
|
|
|
|
install_module => 'all', # || 'passed'. 'all' is default. |
111
|
|
|
|
|
|
|
skip_tests => 1, # skips the tests. |
112
|
|
|
|
|
|
|
test_order => 'ordered', # || 'random'. 'ordered' is default. |
113
|
|
|
|
|
|
|
}, |
114
|
|
|
|
|
|
|
generate_report => { |
115
|
|
|
|
|
|
|
email_report => \%email_config, # Emails the report. See L for config. |
116
|
|
|
|
|
|
|
report_template => '/dir/somewhere/template.txt', # Defaults to internal template. |
117
|
|
|
|
|
|
|
report_output => '/dir/somewhere/output.txt', # File to output the report to. |
118
|
|
|
|
|
|
|
test_report => 'all', # 'failed' || 'passed'. Defaults to all. |
119
|
|
|
|
|
|
|
}, |
120
|
|
|
|
|
|
|
}); |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
This is the constructor used to create the main object. |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
Does a search for all modules on your system, matching the build script description (C). You can choose to run all your tests and generate your report directly from this module, by supplying C and C. Or you can simply supply C and it call the other methods separately. |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
=cut |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
sub new { |
129
|
|
|
|
|
|
|
|
130
|
8
|
|
|
8
|
1
|
123197
|
my ($class, $conf) = @_; |
131
|
|
|
|
|
|
|
|
132
|
8
|
|
|
|
|
32
|
my $self = bless {}, $class; |
133
|
|
|
|
|
|
|
|
134
|
8
|
|
|
|
|
109
|
$self->_init($conf, \@methods); |
135
|
|
|
|
|
|
|
|
136
|
8
|
100
|
|
|
|
36
|
if (!defined $self->base_directories()) { |
137
|
1
|
|
|
|
|
253
|
croak 'Test::Nightly::new() - "base_directories" must be supplied'; |
138
|
|
|
|
|
|
|
} else { |
139
|
|
|
|
|
|
|
|
140
|
7
|
100
|
|
|
|
70
|
$self->build_script('Makefile.PL') unless defined $self->build_script(); |
141
|
|
|
|
|
|
|
|
142
|
7
|
|
|
|
|
113
|
$self->_find_modules(); |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
# See if any methods should be called from new |
145
|
6
|
|
|
|
|
54
|
foreach my $run (@run_these) { |
146
|
|
|
|
|
|
|
|
147
|
24
|
100
|
|
|
|
1760
|
if(defined $conf->{$run}) { |
148
|
|
|
|
|
|
|
# user wants to run this one |
149
|
5
|
|
|
|
|
32
|
$self->$run($conf->{$run}); |
150
|
|
|
|
|
|
|
} |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
6
|
|
|
|
|
227
|
return $self; |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
} |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
} |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
=head2 run_tests() |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
$nightly->run_tests({ |
162
|
|
|
|
|
|
|
build_type => 'make' # || 'build'. 'make' is default. |
163
|
|
|
|
|
|
|
install_module => 'all', # || 'passed'. 'all' is default. |
164
|
|
|
|
|
|
|
skip_tests => 1, # skips the tests. |
165
|
|
|
|
|
|
|
test_directory_format => ['t/', 'tests/'], # Optional, defaults to ['t/']. |
166
|
|
|
|
|
|
|
test_file_format => ['.t', '.pl'], # Optional, defaults to ['.t']. |
167
|
|
|
|
|
|
|
test_order => 'ordered', # || 'random'. 'ordered' is default. |
168
|
|
|
|
|
|
|
}); |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
Runs all the tests on the directories that are stored in the object. |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
Results are stored back in the object so they can be reported on. |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
=cut |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
sub run_tests { |
177
|
|
|
|
|
|
|
|
178
|
5
|
|
|
5
|
1
|
22
|
my ($self, $conf) = @_; |
179
|
|
|
|
|
|
|
|
180
|
5
|
|
|
|
|
31
|
$self->_init($conf, \@methods); |
181
|
|
|
|
|
|
|
|
182
|
5
|
|
|
|
|
54
|
my $test = Test::Nightly::Test->new($self); |
183
|
|
|
|
|
|
|
|
184
|
5
|
|
|
|
|
30
|
$test->run(); |
185
|
|
|
|
|
|
|
|
186
|
5
|
|
|
|
|
1668
|
$self->test($test); |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
} |
189
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
=head2 generate_report() |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
$nightly->generate_report({ |
193
|
|
|
|
|
|
|
email_report => \%email_config, # Emails the report. See L for config options. |
194
|
|
|
|
|
|
|
report_template => '/dir/somewhere/template.txt', # Defaults to internal template. |
195
|
|
|
|
|
|
|
report_output => '/dir/somewhere/output.txt', # File to output the report to. |
196
|
|
|
|
|
|
|
test_report => 'all', # 'failed' || 'passed'. Defaults to all. |
197
|
|
|
|
|
|
|
}); |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
Based on the methods that have been run, produces a report on these. |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
Depending on what you pass in, defines what report is generated. If you pass in an email address to L then the report will be |
202
|
|
|
|
|
|
|
emailed. If you specify an output file to C then the report will be outputted to that file. |
203
|
|
|
|
|
|
|
If you specify both, then both will be done. |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
Default behavior is to use the internal template that is in L, however you can overwrite this with your own template (C). Uses Template Toolkit logic. |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
=cut |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
sub generate_report { |
210
|
|
|
|
|
|
|
|
211
|
4
|
|
|
4
|
1
|
480
|
my ($self, $conf) = @_; |
212
|
|
|
|
|
|
|
|
213
|
4
|
|
|
|
|
88
|
$self->_init($conf, \@methods); |
214
|
|
|
|
|
|
|
|
215
|
4
|
|
|
|
|
122
|
my $report = Test::Nightly::Report->new($self); |
216
|
|
|
|
|
|
|
|
217
|
4
|
|
|
|
|
57
|
$report->run(); |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
} |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
sub _find_modules { |
222
|
|
|
|
|
|
|
|
223
|
7
|
|
|
7
|
|
20
|
my ($self, $conf) = @_; |
224
|
|
|
|
|
|
|
|
225
|
7
|
100
|
|
|
|
78
|
if ($self->build_script() =~ /\s/) { |
226
|
1
|
|
|
|
|
211
|
croak 'Test::Nightly::_find_modules(): Supplied "build_script" can not contain a space'; |
227
|
|
|
|
|
|
|
} |
228
|
|
|
|
|
|
|
|
229
|
6
|
|
|
|
|
72
|
my @modules; |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
# Search through all the base directories supplied. |
232
|
6
|
|
|
|
|
17
|
foreach my $dir (@{$self->base_directories()}) { |
|
6
|
|
|
|
|
34
|
|
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
# Continue if that directory exists |
235
|
6
|
50
|
|
|
|
239
|
if (-d $dir) { |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
# Search for files matching the build script description. |
238
|
6
|
|
|
|
|
318
|
my @found_build_scripts = File::Find::Rule->file()->name( $self->build_script() )->in($dir); |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
# do i need to do this? |
241
|
6
|
|
|
|
|
11926
|
foreach my $found_build_script (@found_build_scripts) { |
242
|
|
|
|
|
|
|
|
243
|
5
|
|
|
|
|
117
|
my ($volume, $directory, $build_script) = File::Spec->splitpath( $found_build_script ); |
244
|
|
|
|
|
|
|
|
245
|
5
|
|
|
|
|
15
|
my %module; |
246
|
5
|
|
|
|
|
20
|
$module{'directory'} = $directory; |
247
|
5
|
|
|
|
|
16
|
$module{'build_script'} = $build_script; |
248
|
|
|
|
|
|
|
|
249
|
5
|
|
|
|
|
39
|
push(@modules, \%module); |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
} |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
} else { |
254
|
0
|
|
|
|
|
0
|
carp 'Test::Nightly::_find_modules() - directory: "'.$dir.'" is not a valid directory'; |
255
|
0
|
|
|
|
|
0
|
next; |
256
|
|
|
|
|
|
|
} |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
} |
259
|
|
|
|
|
|
|
|
260
|
6
|
|
|
|
|
44
|
$self->modules(\@modules); |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
} |
263
|
|
|
|
|
|
|
|
264
|
|
|
|
|
|
|
=head1 List of methods: |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
=over 4 |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
=item base_directories |
269
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
Required. Array ref of base directories to search in. |
271
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
=item build_script |
273
|
|
|
|
|
|
|
|
274
|
|
|
|
|
|
|
Searches for the specified build_script names. Defaults to Makefile.PL |
275
|
|
|
|
|
|
|
|
276
|
|
|
|
|
|
|
=item build_type |
277
|
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
Pass this in so we know how you build your modules. There are two options: 'build' and 'make'. Defaults to 'make'. |
279
|
|
|
|
|
|
|
|
280
|
|
|
|
|
|
|
=item debug |
281
|
|
|
|
|
|
|
|
282
|
|
|
|
|
|
|
Turns debugging messages on or off. |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
=item email_report |
285
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
If set will email the report. Takes a hash ref of \%email_config, refer to Test::Nightly::Email for the options. |
287
|
|
|
|
|
|
|
|
288
|
|
|
|
|
|
|
=item install_module |
289
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
Pass this in if you wish to have the module installed. |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
=item modules |
293
|
|
|
|
|
|
|
|
294
|
|
|
|
|
|
|
List of modules that have been found, returns an array ref of undef. |
295
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
=item skip_tests |
297
|
|
|
|
|
|
|
|
298
|
|
|
|
|
|
|
Pass this in if you wish to skip running the tests. |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
=item report_output |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
Set this to a file somewhere and the report will be outputted here. |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=item report_template |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
Pass this in if you wish to use your own customised report template. Otherwise uses the default template is in Test::Nightly::Report::Template |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=item test |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
Holds the Test::Nightly::Test object. |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
=item test_directory_format |
313
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
An array of what format the test directories can be. By default it searches for the tests in 't/' |
315
|
|
|
|
|
|
|
|
316
|
|
|
|
|
|
|
=item test_file_format |
317
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
An array of the test file formats you have. |
319
|
|
|
|
|
|
|
|
320
|
|
|
|
|
|
|
=item test_report |
321
|
|
|
|
|
|
|
|
322
|
|
|
|
|
|
|
This is where you specify what you wish to report on after the outcome of the test. Specifying 'passed' will only report on tests that passed, specifying 'failed' will only report on tests that failed and specifying 'all' will report on both. |
323
|
|
|
|
|
|
|
|
324
|
|
|
|
|
|
|
=item test_order |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
Pass this in if you wish to influence the way the tests are run. Either 'ordered' or 'random'. Detauls to 'ordered'. |
327
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
=back |
329
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
=head1 DISCLAIMERS |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
This module assumes that you only need installed modules to test your module. So if the module you're testing requires the changes you've made to another module in the tree that you haven't installed, testing will fail. |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
If your module asks interactive questions in the build script or test scripts then this won't work. |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
=head1 TODO |
337
|
|
|
|
|
|
|
|
338
|
|
|
|
|
|
|
Soon I would like to implement a module that will handle version control, so you are able to checkout and update your modules for testing. As well as this it would be nice to incorporate in a wrapper for L. |
339
|
|
|
|
|
|
|
|
340
|
|
|
|
|
|
|
L, |
341
|
|
|
|
|
|
|
L. |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=head1 AUTHOR |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
Kirstin Bettiol |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
=head1 SEE ALSO |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
L, |
350
|
|
|
|
|
|
|
L, |
351
|
|
|
|
|
|
|
L, |
352
|
|
|
|
|
|
|
L, |
353
|
|
|
|
|
|
|
L. |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
=head1 COPYRIGHT |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
(c) 2005 Kirstin Bettiol |
358
|
|
|
|
|
|
|
This library is free software, you can use it under the same terms as perl itself. |
359
|
|
|
|
|
|
|
|
360
|
|
|
|
|
|
|
=head1 THANKS |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
Thanks to Leo Lapworth for helping me with this and Foxtons for letting me develop this on their time. |
363
|
|
|
|
|
|
|
|
364
|
|
|
|
|
|
|
=cut |
365
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
1; |
367
|
|
|
|
|
|
|
|