File Coverage

blib/lib/Test/DatabaseRow/Object.pm
Criterion Covered Total %
statement 278 283 98.2
branch 101 106 95.2
condition 40 45 88.8
subroutine 50 52 96.1
pod 19 32 59.3
total 488 518 94.2


line stmt bran cond sub pod time code
1             package Test::DatabaseRow::Object;
2              
3             # require at least a version of Perl that is merely ancient, but not
4             # prehistoric
5 11     11   200178 use 5.006;
  11         38  
  11         602  
6              
7 11     11   67 use strict;
  11         20  
  11         390  
8 11     11   61 use warnings;
  11         24  
  11         568  
9              
10             our $VERSION = "2.01";
11              
12 11     11   72 use Scalar::Util qw(blessed);
  11         26  
  11         1588  
13 11     11   75 use Carp qw(croak);
  11         19  
  11         807  
14             our @CARP_NOT = qw(Test::DatabaseRow::Object);
15              
16 11     11   8149 use Test::DatabaseRow::Result;
  11         415  
  11         362  
17 11     11   78 use Test::Builder;
  11         18  
  11         50173  
18              
19             # okay, try loading Regexp::Common
20              
21             # if we couldn't load Regexp::Common then we use the one regex that I
22             # copied and pasted from there that we need. We could *always* do
23             # this, but at least this way if there's a bug in this regex they can
24             # upgrade Regexp::Common when it changes and they don't have to wait
25             # for me to upgrade this module too
26              
27             our %RE;
28             unless (eval { require Regexp::Common; Regexp::Common->import; 1 }) {
29             ## no critic (ProhibitComplexRegexes)
30             $RE{num}{real} = qr/
31             (?:(?i)(?:[+-]?)(?:(?=[0123456789]|[.])
32             (?:[0123456789]*)(?:(?:[.])(?:[0123456789]{0,}))?)
33             (?:(?:[E])(?:(?:[+-]?)(?:[0123456789]+))|))
34             /x;
35             }
36              
37             ########################################################################
38             # constructor and accessors
39             #
40             # note that this is all written in the Moose style, even though we
41             # don't actually use Moose (because it's too big a dependency
42             # for this widely used Test module.) Hopefully if you use Moose
43             # a lot (and you should) then the conventions used in this code will
44             # be understandable.
45             #
46             # The design pattern we use here is the "pull through accessor" meaning
47             # that a large amount of the work is done by 'read only' lazy accessors
48             # the first time they are read and then subsequently cached for future
49             # reads. As the lazy accessors in turn request other lazy accessors
50             # simply requesting an accessor from a method may have a chain effect
51             # and do the majority of calculation that would traditionally be done
52             # within the method.
53             #
54             # Some of this code could have been made shorter via meta programing
55             # e.g. programming to dynamically create methods, making the has_XXX
56             # attributes automatic. I've deliberately not done that since that
57             # would drastically reduce the readability of this code. I'm not against
58             # that kind of thing, but it should be packaged up in it's own module,
59             # and that would end up re-inventing Moose...
60             #
61             ########################################################################
62              
63             ## constructor #########################################################
64              
65             # emulate moose somewhat by calling a _coerce_and_verify_XXX method
66             # if one exists
67             sub new {
68 66     66 1 11151 my $class = shift;
69 66         227 my $self = bless {}, $class;
70 66         205 while (@_) {
71 424         491 my $key = shift;
72 424         455 my $value = shift;
73 424         1807 my $method = $self->can("_coerce_and_verify_$key");
74 424 100       1554 $self->{ $key } = $method ? $method->($self,$value) : $value;
75             }
76 60         196 return $self;
77             }
78              
79             ## database related accessors ##########################################
80              
81             # has dbh => ( is => 'ro' );
82 182     182 1 212 sub dbh { my $self = shift; return $self->{dbh} }
  182         720  
83 40     40 0 852 sub has_dbh { my $self = shift; return exists $self->{dbh} }
  40         135  
84              
85             # has table => ( is => 'ro', predicate => 'has_table' );
86 39     39 1 54 sub table { my $self = shift; return $self->{table} }
  39         173  
87 45     45 0 871 sub has_table { my $self = shift; return exists $self->{table} }
  45         175  
88              
89             # has where => ( is => 'ro', isa => 'HashRef[HashRef]'. coerce => 1, predicate => "has_where");
90 39     39 1 77 sub where { my $self = shift; return $self->{where} }
  39         82  
91 42     42 0 883 sub has_where { my $self = shift; return exists $self->{where} }
  42         137  
92             sub _coerce_and_verify_where {
93 43     43   62 my $self = shift;
94 43         63 my $where = shift;
95              
96             # coerce it all to equals tests if we were using the
97             # shorthand array notation
98 43 100       137 if (ref $where eq "ARRAY") {
99 38         65 $where = { "=" => { @{ $where } } };
  38         159  
100             }
101              
102             # check we've got a hash of hashes
103 43 100       145 unless (ref($where) eq "HASH")
104 1         14 { croak "Can't understand the argument passed in 'where'" }
105 42         66 foreach my $valuehash (values %{ $where }) {
  42         155  
106 42 100       171 unless (ref($valuehash) eq "HASH")
107 1         11 { croak "Can't understand the argument passed in 'where'" }
108             }
109              
110 41         170 return $where;
111             }
112              
113             # has sql_and_bind => ( is => 'ro', predicate => "has_sql_and_bind", lazy_build => 1 );
114             sub sql_and_bind {
115 63     63 1 2301 my $self = shift;
116 63   100     349 return $self->{sql_and_bind} ||= $self->_build_sql_and_bind;
117             }
118 2     2 0 1193 sub has_sql_and_bind { my $self = shift; return exists $self->{sql_and_bind} }
  2         11  
119             sub _coerce_and_verify_sql_and_bind {
120 16     16   33 my $self = shift;
121 16         30 my $sql_and_bind = shift;
122              
123 16 100       91 return $sql_and_bind if ref $sql_and_bind eq "ARRAY";
124 11 100 66     90 return [$sql_and_bind] if ref $sql_and_bind eq "" || blessed($sql_and_bind);
125 1         19 croak "Can't understand the sql";
126             }
127             sub _build_sql_and_bind {
128 41     41   61 my $self = shift;
129              
130 41 100       109 unless ($self->has_table)
131 1         18 { croak "Needed to build SQL but no 'table' defined" }
132 40 100       110 unless ($self->has_where)
133 1         15 { croak "Needed to build SQL but no 'where' defined" }
134              
135 39         59 my @conditions;
136 39         89 my $where = $self->where;
137 39         66 foreach my $oper (sort keys %{$where}) {
  39         147  
138 39         59 my $valuehash = $where->{ $oper };
139              
140 39         60 foreach my $field (sort keys %{$valuehash}) {
  39         136  
141             # get the value
142 41         87 my $value = $valuehash->{ $field };
143              
144             # should this be "IS NULL" rather than "= ''"?
145 41 100 100     232 if ($oper eq "=" && !defined($value)) {
146 2         7 push @conditions, "$field IS NULL";
147 2         9 next;
148             }
149              
150             # just an undef? I hope $oper is "IS" or "IS NOT"
151 39 100       99 if (!defined($value)) {
152 1         3 push @conditions, "$field $oper NULL";
153 1         4 next;
154             }
155              
156             # proper value, quote it properly
157             # we do this instead of adding to bind because it makes the
158             # error messages much more readable
159 38 100       134 unless ($self->has_dbh)
160 1         14 { croak "Needed to quote SQL during SQL building but no 'dbh' defined" }
161 37         124 push @conditions, "$field $oper ".$self->dbh->quote($value);
162             }
163             }
164              
165 38         289 return ["SELECT * FROM @{[ $self->table ]} WHERE @{[ join ' AND ', @conditions ]}"];
  38         220  
  38         290  
166             }
167              
168             # has force_utf8 => ( is => "ro" )
169 107     107 1 124 sub force_utf8 { my $self = shift; return $self->{force_utf8} }
  107         305  
170 2     2 0 904 sub has_force_utf8 { my $self = shift; return exists $self->{force_utf8} }
  2         10  
171              
172             # has db_results => ( is => "ro", lazy_build => 1 );
173             sub db_results {
174 106     106 1 181 my $self = shift;
175 106   66     512 return $self->{db_results} ||= $self->_build_db_results;
176             }
177 3     3 0 24 sub has_db_results { my $self = shift; return exists $self->{db_results} }
  3         23  
178             sub _build_db_results {
179 47     47   65 my $self = shift;
180              
181 47 100       111 unless ($self->dbh)
182 1         29 { croak "Needed fetch results but no 'dbh' defined" }
183              
184             # make all database problems fatal
185 46         186 local $self->dbh->{RaiseError} = 1;
186              
187             # load "Encode" if we need to do utf8 munging
188 46 100       130 if ($self->force_utf8) {
189 2 50       3 eval { require Encode; 1 }
  2         1328  
  2         12786  
190             or croak "Can't load Encode, but force_utf8 is enabled";
191             }
192              
193             # get the SQL and execute it
194 46         76 my ($sql, @bind) = @{ $self->sql_and_bind };
  46         115  
195 44         142 my $sth = $self->dbh->prepare($sql);
196 43         698 $sth->execute( @bind );
197              
198             # store the results
199 43         113 my @db_results;
200 43         128 while (my $row_data = $sth->fetchrow_hashref) {
201              
202             # munge the utf8 flag if we need to
203 61 100       983 if ($self->force_utf8)
204 2         3 { Encode::_utf8_on($_) foreach values %{ $row_data } }
  2         23  
205              
206             # store the data
207 61         193 push @db_results, $row_data;
208             }
209              
210 43         698 return \@db_results;
211             }
212              
213             # has db_results_dumped => ( is => "ro", lazy_build => 1 );
214             sub db_results_dumped {
215 1     1 1 2 my $self = shift;
216 1   33     7 return $self->{db_results_dumped} ||= $self->_build_db_results_dumped;
217             }
218 0     0 0 0 sub has_db_results_dumped { my $self = shift; return exists $self->{db_results_dumped} }
  0         0  
219             sub _build_db_results_dumped {
220 1     1   2 my $self = shift;
221              
222             # get the results iff some was already fetched, otherwise we don't have any
223 1 50       3 my $results = $self->has_db_results ? $self->db_results : [];
224              
225 1         4 my $builder = Test::Builder->new;
226 1 50       11 if ($builder->can("explain")) {
227 1         4 my ($str) = $builder->explain($results);
228 1         134 return $str;
229             }
230              
231 0         0 croak "Cannot dump db results since the version of Test::Builder installed does not support 'explain'";
232             }
233              
234             ## test related accessors ##############################################
235              
236              
237             # has "test" => ( is => "ro", isa => "Bool", predicate => "has_check_all_rows")
238             sub check_all_rows {
239 14     14 1 22 my $self = shift;
240 14         89 return $self->{check_all_rows}
241             }
242              
243             # has "test" => ( is => "ro", predicate => "has_tests", isa =>"tests", coerce => 1 )
244 140     140 1 185 sub tests { my $self = shift; return $self->{tests} }
  140         523  
245 2     2 0 6553 sub has_tests { my $self = shift; return exists $self->{tests} }
  2         45  
246             sub _coerce_and_verify_tests {
247 32     32   57 my $self = shift;
248 32         58 my $tests = shift;
249              
250             # if this is a an array, coerce it into a hash
251 32 100       103 if (ref $tests eq "ARRAY") {
252 25         42 my @tests = @{ $tests };
  25         87  
253              
254 25 100       100 if (@tests % 2 != 0)
255 1         22 { croak "Can't understand the passed test arguments" }
256              
257             # for each key/value pair
258 24         64 $tests = {};
259 24         379 while (@tests) {
260 44         75 my $key = shift @tests;
261 44         61 my $value = shift @tests;
262              
263             # set the comparator based on the type of value we're comparing
264             # against. This can lead to some annoying cases, but if they
265             # want proper comparison they can use the non dwim mode
266              
267 44 100       112 if (!defined($value)) {
268 1         4 $tests->{'eq'}{ $key } = $value;
269 1         3 next;
270             }
271              
272 43 100       131 if (ref($value) eq "Regexp") {
273 11         33 $tests->{'=~'}{ $key } = $value;
274 11         39 next;
275             }
276              
277 32 100       1338 if ($value =~ /\A $RE{num}{real} \z/x) {
278 17         68 $tests->{'=='}{ $key } = $value;
279 17         89 next;
280             }
281              
282             # default to string comparison
283 15         90 $tests->{'eq'}{ $key } = $value;
284             }
285             }
286              
287             # check we've got a hash of hashes
288 31 100       113 unless (ref($tests) eq "HASH")
289 1         29 { croak "Can't understand the argument passed in 'tests': not a hashref or arrayref" }
290 30         55 foreach my $valuekey (keys %{ $tests }) {
  30         113  
291 54 100       199 unless (ref($tests->{ $valuekey }) eq "HASH")
292 1         13 { croak "Can't understand the argument passed in 'tests': key '$valuekey' didn't contain a hashref" }
293             }
294              
295 29         145 return $tests;
296             }
297              
298             # has results => ( is => "ro", predicate => "has_results")
299 10     10 1 24 sub results { my $self = shift; return $self->{results} }
  10         56  
300 79     79 0 1315 sub has_results { my $self = shift; return exists $self->{results} }
  79         474  
301              
302             # has max_results => ( is => "ro", predicate => "has_max_results")
303 3     3 1 5 sub max_results { my $self = shift; return $self->{max_results} }
  3         18  
304 79     79 0 1128 sub has_max_results { my $self = shift; return exists $self->{max_results} }
  79         11612  
305              
306             # has min_results => ( is => "ro", predicate => "has_rmin_esults")
307 3     3 1 5 sub min_results { my $self = shift; return $self->{min_results} }
  3         13  
308 82     82 0 1052 sub has_min_results { my $self = shift; return exists $self->{min_results} }
  82         391  
309              
310             ## output accessors ####################################################
311              
312             # has verbose => (is => 'ro', default => 0)
313 19   100 19 1 27 sub verbose { my $self = shift;return $self->{verbose} || 0 }
  19         143  
314 2     2 0 779 sub has_verbose { my $self = shift; return exists $self->{verbose} }
  2         11  
315              
316             # has verbose_data => (is => 'ro', default => 0)
317 19   100 19 1 31 sub verbose_data { my $self = shift;return $self->{verbose_data} || 0 }
  19         176  
318 0     0 0 0 sub has_verbose_data { my $self = shift; return exists $self->{verbose_data} }
  0         0  
319              
320             ########################################################################
321             # methods
322             ########################################################################
323              
324             # check the number of results returned from the database
325             sub number_of_results_ok {
326 46     46 1 94 local $Test::Builder::Level = $Test::Builder::Level + 1;
327              
328 46         64 my $self = shift;
329 46         63 my $num_rows_from_db = @{ $self->db_results };
  46         132  
330              
331             # fail the test if we're running just one test and no matching row was
332             # returned
333 42 100 100     138 if(!$self->has_min_results &&
      100        
334             !$self->has_max_results &&
335             !$self->has_results &&
336             $num_rows_from_db == 0) {
337 3         9 return $self->_fail("No matching row returned");
338             }
339              
340             # check we got the exected number of rows back if they specified exactly
341 39 100 100     119 if($self->has_results && $num_rows_from_db != $self->results) {
342 1         4 return $self->_fail("Got the wrong number of rows back from the database.",
343             " got: $num_rows_from_db rows back",
344 1         3 " expected: @{[ $self->results ]} rows back");
345             }
346              
347             # check we got enough matching rows back
348 38 100 100     101 if($self->has_min_results && $num_rows_from_db < $self->min_results) {
349 1         5 return $self->_fail("Got too few rows back from the database.",
350             " got: $num_rows_from_db rows back",
351 1         2 " expected: @{[ $self->min_results ]} rows or more back");
352             }
353              
354             # check we got didn't get too many matching rows back
355 37 100 100     136 if($self->has_max_results && $num_rows_from_db > $self->max_results) {
356 1         4 return $self->_fail("Got too many rows back from the database.",
357             " got: $num_rows_from_db rows back",
358 1         4 " expected: @{[ $self->max_results ]} rows or fewer back");
359             }
360              
361 36         113 return $self->_pass;
362             }
363              
364             sub row_at_index_ok {
365 31     31 1 42 my $self = shift;
366 31   100     107 my $row_index = shift || 0;
367              
368             # check we have data for this index
369 31         108 my $row_index_th = ($row_index + 1) . _th($row_index + 1);
370 31         93 my $data = $self->db_results->[ $row_index ];
371 31 100       74 unless ($data)
372 1         7 { return $self->_fail("No $row_index_th row") }
373              
374             # pass unless there's some tests to be run
375 30 100       66 return $self->_pass unless $self->tests;
376              
377             # check each of the comparisons, sorted asciibetically
378 29         53 foreach my $oper (sort keys %{ $self->tests }) {
  29         57  
379 44         8427 my $valuehash = $self->tests->{ $oper };
380              
381             # process each field in turn, sorted asciibetically
382 44         70 foreach my $colname (sort keys %{$valuehash}) {
  44         158  
383              
384             # check the column we're comparing exists
385 44 100       616 unless (exists($data->{ $colname })) {
386 2 100       7 croak "No column '$colname' returned from table '@{[ $self->table ]}'"
  1         3  
387             if $self->has_table;
388 1         19 croak "No column '$colname' returned from sql";
389             }
390              
391              
392              
393             # try the comparison
394 42         1329 my $expect = $valuehash->{ $colname };
395 42         65 my $got = $data->{ $colname };
396 42         54 my $passed;
397             {
398             # disable warnings as we might compare undef
399 42     5   48 local $SIG{__WARN__} = sub {}; # $^W not work
  42         308  
  5         139  
400              
401             # do a string eval because $oper could be any
402             # arbitary comparison operator here. Note the
403             # the use of backslashes here so that we create
404             # a string containing varaible names *not* the
405             # values.
406 42 100       3591 eval "\$passed = \$got $oper \$expect; 1"
407             or croak "Invalid operator test '$oper': $@";
408             };
409              
410 41 100       238 unless ($passed) {
411 12 100       132 return $self->_fail(
412             "While checking column '$colname' on $row_index_th row",
413             ( $oper =~ /\A (?:eq|==) \z/x )
414             ? $self->_is_diag($got, $oper, $expect)
415             : $self->_cmp_diag($got, $oper, $expect)
416             );
417             }
418             }
419             }
420              
421 14         45 return $self->_pass;
422             }
423              
424             sub db_results_ok {
425 36     36 1 46 my $self = shift;
426 36 100       109 return $self->_pass unless $self->tests;
427 23         37 foreach my $row_index (0..@{ $self->db_results }-1) {
  23         57  
428 29         83 my $result = $self->row_at_index_ok( $row_index );
429 26 100       94 return $result if $result->is_error;
430 14 100       44 last unless $self->check_all_rows;
431             }
432 8         23 return $self->_pass;
433             }
434              
435             sub test_ok {
436 46     46 1 72 my $self = shift;
437 46         141 my $result = $self->number_of_results_ok;
438 42 100       137 return $result if $result->is_error;
439 36         104 return $self->db_results_ok;
440             }
441              
442             ########################################################################
443             # methods for creating Test::DatabaseRow::Result objects
444             ########################################################################
445              
446             sub _pass {
447 72     72   90 my $self = shift;
448 72         330 return Test::DatabaseRow::Result->new();
449             }
450              
451             sub _fail {
452 19     19   38 my $self = shift;
453 19 100       61 return Test::DatabaseRow::Result->new(
    100          
454             is_error => 1,
455             diag => [
456             @_,
457              
458             # include the SQL diagnostics if we're verbose
459             ($self->verbose ? $self->_sql_diag : ()),
460              
461             # include a dumper of the results if we're verbose_data
462             ($self->verbose_data ?
463             ("Data returned from the database:",$self->db_results_dumped)
464             : ()
465             ),
466             ],
467             );
468             }
469              
470             # prints out handy diagnostic text if we're printing out verbose text
471             sub _sql_diag {
472 8     8   14 my $self = shift;
473              
474 8         22 my $database_name = $self->dbh->{Name};
475 8         12 my ($sql, @bind) = @{ $self->sql_and_bind };
  8         20  
476              
477 8         15 my @diags;
478              
479             # print out the SQL
480 8         20 push @diags, "The SQL executed was:";
481 8         40 push @diags, map { " $_\n" } split /\n/x, $sql;
  10         34  
482              
483             # print out the bound parameters
484 8 100       33 if (@bind) {
485 2         5 push @diags, "The bound parameters were:";
486 2         9 foreach my $bind (@bind) {
487 3 100       13 if (defined($bind)) {
488 2         9 push @diags, " '$bind'";
489             } else {
490 1         4 push @diags, " undef";
491             }
492             }
493             }
494              
495             # print out the database
496 8         22 push @diags, "on database '$database_name'";
497              
498 8         41 return @diags;
499             }
500              
501             # _cmp_diag and is__diag were originally private functions in
502             # Test::Builder (and were written by Schwern).
503              
504             sub _cmp_diag {
505 4     4   9 my($self, $got, $type, $expect) = @_;
506              
507 4 50       20 $got = defined $got ? "'$got'" : 'undef';
508 4 50       15 $expect = defined $expect ? "'$expect'" : 'undef';
509              
510 4         28 return sprintf <<"DIAGNOSTIC", $got, $type, $expect;
511             %s
512             %s
513             %s
514             DIAGNOSTIC
515             }
516              
517             sub _is_diag {
518 8     8   21 my($self, $got, $type, $expect) = @_;
519              
520 8         25 foreach my $val (\$got, \$expect) {
521 16 100       19 unless( defined ${$val} ) {
  16         49  
522 1         1 ${$val} = 'NULL';
  1         4  
523 1         3 next;
524             }
525              
526 15 100       43 if( $type eq 'eq' ) {
527             # quote and force string context
528 9         15 ${$val} = "'${$val}'";
  9         17  
  9         21  
529 9         24 next;
530             }
531              
532             # otherwise force numeric context
533 6         8 ${$val} = ${$val}+0;
  6         15  
  6         8  
534             }
535              
536 8         68 return sprintf <<"DIAGNOSTIC", $got, $expect;
537             got: %s
538             expected: %s
539             DIAGNOSTIC
540             }
541              
542             ########################################################################
543             # stolen from Lingua::EN::Numbers::Ordinate
544             # Copyright (c) 2000 Sean M. Burke. All rights reserved.
545             # This library is free software; you can redistribute it and/or
546             # modify it under the same terms as Perl itself.
547              
548             # simple subroutine to
549             sub _th {
550 233 100 66 233   111663 return 'th' if not(defined($_[0])) or not( 0 + $_[0] );
551             # 'th' for undef, 0, or anything non-number.
552 231         372 my $n = abs($_[0]); # Throw away the sign.
553 231 100       482 return 'th' unless $n == int($n); # Best possible, I guess.
554 230         320 $n %= 100;
555 230 100 100     1468 return 'th' if $n == 11 or $n == 12 or $n == 13;
      100        
556 224         289 $n %= 10;
557 224 100       536 return 'st' if $n == 1;
558 183 100       396 return 'nd' if $n == 2;
559 161 100       358 return 'rd' if $n == 3;
560 141         558 return 'th';
561             }
562              
563             1;
564              
565             __END__