File Coverage

blib/lib/Test/DBIx/Class.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Test::DBIx::Class;
2              
3 13     13   98135 use 5.008;
  13         34  
  13         421  
4 13     13   49 use strict;
  13         15  
  13         368  
5 13     13   61 use warnings;
  13         14  
  13         421  
6              
7 13     13   53 use base 'Test::Builder::Module';
  13         13  
  13         1824  
8              
9             our $VERSION = '0.43';
10             our $AUTHORITY = 'cpan:JJNAPIORK';
11              
12 13     13   15388 use Config::Any;
  13         100934  
  13         336  
13 13     13   8288 use Data::Visitor::Callback;
  0            
  0            
14             use Digest::MD5;
15             use Hash::Merge;
16             use Path::Class;
17             use Scalar::Util ();
18             use Sub::Exporter;
19             use Test::DBIx::Class::SchemaManager;
20             use Test::Deep ();
21             use Test::More ();
22              
23             sub eq_or_diff2 {
24             my ($given, $expected, $message) = @_;
25             my ($ok, $stack) = Test::Deep::cmp_details($given, $expected);
26             if($ok) {
27             Test::More::pass($message);
28             } else {
29             my $diag = Test::Deep::deep_diag($stack);
30             Test::More::fail("$message: $diag");
31             }
32             }
33              
34             sub import {
35             my ($class, @opts) = @_;
36             my ($schema_manager, $merged_config, @exports) = $class->_initialize(@opts);
37             my $exporter = Sub::Exporter::build_exporter({
38             exports => [
39             dump_settings => sub {
40             return sub {
41             return $merged_config, @exports;
42             };
43             },
44             Schema => sub {
45             return sub {
46             return $schema_manager->schema;
47             }
48             },
49             ResultSet => sub {
50             my ($local_class, $name, $arg) = @_;
51             return sub {
52             my $source = shift @_;
53             my $search = shift @_;
54             my $resultset = $schema_manager->schema->resultset($source);
55              
56             if(my $global_search = $arg->{search}) {
57             my @global_search = ref $global_search eq 'ARRAY' ? @$global_search : ($global_search);
58             $resultset = $resultset->search(@global_search);
59             }
60              
61             if(my $global_cb = $arg->{exec}) {
62             $resultset = $global_cb->($resultset);
63             }
64              
65             if($search) {
66             my @search = ref $search eq 'ARRAY' ? @$search : ($search, @_);
67             $resultset = $resultset->search(@search);
68             }
69              
70             return $resultset;
71             }
72             },
73             is_result => sub {
74             my ($local_class, $name, $arg) = @_;
75             my $global_class = defined $arg->{isa_class} ? $arg->{isa_class} : '';
76             return sub {
77             my $rs = shift @_;
78             my $compare = shift @_ || $global_class || "DBIx::Class";
79             my $message = shift @_;
80             Test::More::isa_ok($rs, $compare, $message);
81             }
82             },
83             is_resultset => sub {
84             my ($local_class, $name, $arg) = @_;
85             my $global_class = defined $arg->{isa_class} ? $arg->{isa_class} : '';
86             return sub {
87             my $rs = shift @_;
88             my $compare = shift @_ || $global_class || "DBIx::Class::ResultSet";
89             my $message = shift @_;
90             Test::More::isa_ok($rs, $compare, $message);
91             }
92             },
93             eq_result => sub {
94             return sub {
95             my ($result1, $result2, $message) = @_;
96             $message = defined $message ? $message : ref($result1) . " equals " . ref($result2);
97             if( ref($result1) eq ref($result2) ) {
98             eq_or_diff2(
99             {$result2->get_columns},
100             {$result1->get_columns},
101             $message,
102             );
103             } else {
104             Test::More::fail($message ." :Result arguments not of same class");
105             }
106             },
107             },
108             eq_resultset => sub {
109             return sub {
110             my ($rs1, $rs2, $message) = @_;
111             $message = defined $message ? $message : ref($rs1) . " equals " . ref($rs2);
112             if( ref($rs1) eq ref($rs2) ) {
113             ($rs1, $rs2) = map {
114             my $me = $_->current_source_alias;
115             my @pks = map { "$me.$_"} $_->result_source->primary_columns;
116             my @result = $_->search({}, {
117             result_class => 'DBIx::Class::ResultClass::HashRefInflator',
118             order_by => [@pks],
119             })->all;
120             [@result];
121             } ($rs1, $rs2);
122              
123             eq_or_diff2([$rs2],[$rs1],$message);
124             } else {
125             Test::More::fail($message ." :ResultSet arguments not of same class");
126             }
127             },
128             },
129             hri_dump => sub {
130             return sub {
131             (shift)->search ({}, {
132             result_class => 'DBIx::Class::ResultClass::HashRefInflator'
133             });
134             }
135             },
136             fixtures_ok => sub {
137             return sub {
138             my ($arg, $message) = @_;
139             $message = defined $message ? $message : 'Fixtures Installed';
140              
141             if ($arg && ref $arg && (ref $arg eq 'CODE')) {
142             eval {
143             $arg->($schema_manager->schema);
144             }; if($@) {
145             Test::More::fail($message);
146             $schema_manager->builder->diag($@);
147              
148             } else {
149             Test::More::pass($message);
150             }
151             } elsif( $arg && ref $arg && (ref $arg eq 'HASH' || ref $arg eq 'ARRAY') ) {
152             my @return;
153             eval {
154             @return = $schema_manager->install_fixtures($arg);
155             }; if($@) {
156             Test::More::fail($message);
157             $schema_manager->builder->diag($@);
158             } else {
159             Test::More::pass($message);
160             return @return;
161             }
162             } elsif( $arg ) {
163             my @sets = ref $arg ? @$arg : ($arg);
164             my @fixtures = $schema_manager->get_fixture_sets(@sets);
165             my @return;
166             foreach my $fixture (@fixtures) {
167             eval {
168             push @return, $schema_manager->install_fixtures($fixture);
169             }; if($@) {
170             Test::More::fail($message);
171             $schema_manager->builder->diag($@);
172             } else {
173             Test::More::pass($message);
174             return @return;
175             }
176             }
177             } else {
178             Test::More::fail("Can't figure out what fixtures you want");
179             }
180             }
181             },
182             is_fields => sub {
183             my ($local_class, $name, $arg) = @_;
184             my @default_fields = ();
185             if(defined $arg && ref $arg eq 'HASH' && defined $arg->{fields}) {
186             @default_fields = ref $arg->{fields} ? @{$arg->{fields}} : ($arg->{fields});
187             }
188             return sub {
189             my @args = @_;
190             my @fields = @default_fields;
191             if(!ref($args[0]) || (ref($args[0]) eq 'ARRAY')) {
192             my $fields = shift(@args);
193             @fields = ref $fields ? @$fields : ($fields);
194             }
195             if(Scalar::Util::blessed($args[0]) &&
196             $args[0]->isa('DBIx::Class') &&
197             !$args[0]->isa('DBIx::Class::ResultSet')
198             ) {
199             my $result = shift(@args);
200             unless(@fields) {
201             my @pks = $result->result_source->primary_columns;
202             push @fields, grep {
203             my $field = $_;
204             $field ne ((grep { $field eq $_ } @pks)[0] || '')
205             } ($result->result_source->columns);
206             }
207             my $compare = shift(@args);
208             if(ref $compare eq 'HASH') {
209             } elsif(ref $compare eq 'ARRAY') {
210             my @localfields = @fields;
211             $compare = {map {
212             my $value = $_;
213             my $key = shift(@localfields);
214             $key => $value } @$compare};
215             Test::More::fail('Too many fields!') if @localfields;
216             } elsif(!ref $compare) {
217             my @localfields = @fields;
218             $compare = {map {
219             my $value = $_;
220             my $key = shift(@localfields);
221             $key => $value } ($compare)};
222             Test::More::fail('Too many fields!') if @localfields;
223             }
224             my $message = shift(@args) || 'Fields match';
225             my $compare_rs = {map {
226             die "$_ is not an available field"
227             unless $result->can($_);
228             $_ => $result->$_ } @fields};
229             eq_or_diff2($compare,$compare_rs,$message);
230             return $compare;
231             } elsif (Scalar::Util::blessed($args[0]) && $args[0]->isa('DBIx::Class::ResultSet')) {
232              
233             my $resultset = shift(@args);
234             unless(@fields) {
235             my @pks = $resultset->result_source->primary_columns;
236             push @fields, grep {
237             my $field = $_;
238             $field ne ((grep { $field eq $_ } @pks)[0] || '')
239             } ($resultset->result_source->columns);
240             }
241             my @compare = @{shift(@args)};
242             foreach (@compare) {
243             if(!ref $_) {
244             my @localfields = @fields;
245             $_ = {map {
246             my $value = $_;
247             my $key = shift(@localfields);
248             $key => $value } ($_)};
249             Test::More::fail('Too many fields!') if @localfields;
250             } elsif(ref $_ eq 'ARRAY') {
251             my @localfields = @fields;
252             $_ = {map {
253             my $value = $_;
254             my $key = shift(@localfields);
255             $key => $value } (@$_)};
256             Test::More::fail('Too many fields!') if @localfields;
257             }
258             }
259             my $message = shift(@args) || 'Fields match';
260              
261             my @resultset = $resultset->search({}, {
262             result_class => 'DBIx::Class::ResultClass::HashRefInflator',
263             columns => [@fields],
264             })->all;
265             my %compare_rs;
266             foreach my $row(@resultset) {
267             no warnings 'uninitialized';
268             my $id = Digest::MD5::md5_hex(join('.', map {$row->{$_}} sort keys %$row));
269             $compare_rs{$id} = { map { $_,"$row->{$_}"} keys %$row};
270             }
271             my %compare;
272             foreach my $row(@compare) {
273             no warnings 'uninitialized';
274             my $id = Digest::MD5::md5_hex(join('.', map {$row->{$_}} sort keys %$row));
275             ## Force comparison stuff in stringy form :(
276             $compare{$id} = { map { $_,"$row->{$_}"} keys %$row};
277             }
278             eq_or_diff2(\%compare,\%compare_rs,$message);
279             return \@compare;
280             } else {
281             die "I'm not sure what to do with your arguments";
282             }
283             };
284             },
285             reset_schema => sub {
286             return sub {
287             my $message = shift @_ || 'Schema reset complete';
288             $schema_manager->reset;
289             Test::More::pass($message);
290             }
291             },
292             cleanup_schema => sub {
293             return sub {
294             my $message = shift @_ || 'Schema cleanup complete';
295             $schema_manager->cleanup;
296             Test::More::pass($message);
297             }
298             },
299             map {
300             my $source = $_;
301             $source => sub {
302             my ($local_class, $name, $arg) = @_;
303             my $resultset = $schema_manager->schema->resultset($source);
304             if(my $search = $arg->{search}) {
305             my @search = ref $search eq 'ARRAY' ? @$search : ($search);
306             $resultset = $resultset->search(@search);
307             }
308             return sub {
309             my $search = shift @_;
310             if($search) {
311             my @search = ();
312             if(ref $search && ref $search eq 'HASH') {
313             @search = ($search, @_);
314             } else {
315             @search = ({$search, @_});
316             }
317             return $resultset->search(@search);
318             }
319             return $resultset->search_rs;
320             }
321             };
322             } $schema_manager->schema->sources,
323             ],
324             groups => {
325             resultsets => [$schema_manager->schema->sources],
326             },
327             into_level => 1,
328             });
329              
330             $class->$exporter(
331             qw/Schema ResultSet is_result is_resultset hri_dump fixtures_ok reset_schema
332             eq_result eq_resultset is_fields dump_settings cleanup_schema /,
333             @exports
334             );
335             }
336              
337             sub _initialize {
338             my ($class, @opts) = @_;
339             my ($config, @exports) = $class->_normalize_opts(@opts);
340             my $merged_config = $class->_prepare_config($config);
341              
342             if(my $resultsets = delete $merged_config->{resultsets}) {
343             if(ref $resultsets eq 'ARRAY') {
344             push @exports, @$resultsets;
345             } else {
346             die '"resultsets" options must be a Array Reference.';
347             }
348             }
349             my $merged_with_fixtures_config = $class->_prepare_fixtures($merged_config);
350             my $visitor = Data::Visitor::Callback->new(plain_value=>\&_visit_config_values);
351             $visitor->visit($merged_with_fixtures_config);
352              
353             my $schema_manager = $class->_initialize_schema($merged_with_fixtures_config);
354              
355             return (
356             $schema_manager,
357             $merged_config,
358             @exports,
359             );
360             }
361              
362             sub _visit_config_values {
363             return unless $_;
364              
365             &_config_substitutions($_);
366            
367             }
368              
369             sub _config_substitutions {
370             my $subs = {};
371             $subs->{ ENV } =
372             sub {
373             my ( $v ) = @_;
374             if (! defined($ENV{$v})) {
375             Test::More::fail("Missing environment variable: $v");
376             return '';
377             } else {
378             return $ENV{ $v };
379             }
380             };
381             $subs->{ literal } ||= sub { return $_[ 1 ]; };
382             my $subsre = join( '|', keys %$subs );
383              
384             for ( @_ ) {
385             s{__($subsre)(?:\((.+?)\))?__}{ $subs->{ $1 }->( $2 ? split( /,/, $2 ) : () ) }eg;
386             }
387             }
388              
389             sub _normalize_opts {
390             my ($class, @opts) = @_;
391             my ($config, @exports) = ({},());
392              
393             if(ref $opts[0]) {
394             if(ref $opts[0] eq 'HASH') {
395             $config = shift(@opts);
396             } else {
397             die 'First argument to "use Test::DBIx::Class @args" not properly formed.';
398             }
399             }
400              
401             while( my $opt = shift(@opts)) {
402             if($opt =~m/^-(.+)/) {
403             if($config->{$1}) {
404             die "$1 already is defined as $config->{$1}";
405             } else {
406             $config->{$1} = shift(@opts);
407             }
408             } else {
409             @exports = ($opt, @opts);
410             last;
411             }
412             }
413              
414             if(my $resultsets = delete $config->{resultsets}) {
415             if(ref $resultsets eq 'ARRAY') {
416             push @exports, @$resultsets;
417             } else {
418             die '"resultsets" options must be a Array Reference.';
419             }
420             }
421              
422             @exports = map { ref $_ && ref $_ eq 'ARRAY' ? @$_:$_ } @exports;
423              
424             return ($config, @exports);
425             }
426              
427             sub _prepare_fixtures {
428             my ($class, $config) = @_;
429              
430             my @dirs;
431             if(my $fixture_path = delete $config->{fixture_path}) {
432             @dirs = $class->_normalize_config_path(
433             $class->_default_fixture_paths, $fixture_path,
434             );
435             } else {
436             @dirs = $class->_normalize_config_path($class->_default_fixture_paths);
437             }
438              
439             my @extensions = $class->_allowed_extensions;
440             my @files = (
441             grep { $class->_is_allowed_extension($_) }
442             map {Path::Class::dir($_)->children}
443             grep { -e $_ }
444             @dirs
445             );
446              
447             my $fixture_definitions = Config::Any->load_files({
448             files => \@files,
449             use_ext => 1,
450             });
451              
452             my %merged_fixtures;
453             foreach my $fixture_definition(@$fixture_definitions) {
454             my ($path, $fixture) = %$fixture_definition;
455             ## hack to normalize arrayref fixtures. needs work!!!
456             $fixture = ref $fixture eq 'HASH' ? [$fixture] : $fixture;
457             my $file = Path::Class::file($path)->basename;
458             $file =~s/\..{1,4}$//;
459             if($merged_fixtures{$file}) {
460             my $old_fixture = $merged_fixtures{$file};
461             my $merged_fixture = Hash::Merge::merge($fixture, $old_fixture);
462             $merged_fixtures{$file} = $merged_fixture;
463             } else {
464             $merged_fixtures{$file} = $fixture;
465             }
466             }
467              
468             if(my $old_fixture_sets = delete $config->{fixture_sets}) {
469             ## hack to normalize arrayref fixtures. needs work!!!
470             my %normalized_old_fixture_sets = map {
471             ref($old_fixture_sets->{$_}) eq 'HASH' ? ($_, [$old_fixture_sets->{$_}]): ($_, $old_fixture_sets->{$_});
472             } keys %$old_fixture_sets;
473             my $new_fixture_sets = Hash::Merge::merge(\%normalized_old_fixture_sets, \%merged_fixtures );
474             $config->{fixture_sets} = $new_fixture_sets;
475             } else {
476             $config->{fixture_sets} = \%merged_fixtures;
477             }
478              
479             return $config;
480             }
481              
482             sub _is_allowed_extension {
483             my ($class, $file) = @_;
484             my @extensions = $class->_allowed_extensions;
485             foreach my $extension(@extensions) {
486             if($file =~ m/\.$extension$/) {
487             return $file;
488             }
489             }
490             return;
491             }
492              
493             sub _prepare_config {
494             my ($class, $config) = @_;
495              
496             if(my $extra_config = delete $config->{config_path}) {
497             my @config_data = $class->_load_via_config_any($extra_config);
498             foreach my $config_datum(reverse @config_data) {
499             $config = Hash::Merge::merge($config, $config_datum);
500             }
501             } else {
502             my @config_data = $class->_load_via_config_any();
503             foreach my $config_datum(reverse @config_data) {
504             $config = Hash::Merge::merge($config, $config_datum);
505             }
506             }
507              
508             if(my $post_config = delete $config->{config_path}) {
509             my @post_config_paths = $class->_normalize_external_paths($post_config);
510             my @extensions = $class->_allowed_extensions;
511             my @post_config_files = grep { -e $_} map {
512             my $path = $_;
513             map {
514             $ENV{TEST_DBIC_CONFIG_SUFFIX} ?
515             ("$path.$_", "$path$ENV{TEST_DBIC_CONFIG_SUFFIX}.$_") :
516             ("$path.$_");
517             } @extensions;
518             } map {
519             my @local_path = ref $_ ? @$_ : ($_);
520             Path::Class::file(@local_path);
521             } @post_config_paths;
522              
523             my $post_config = $class->_config_any_load_files(@post_config_files);
524             foreach my $config_datum(reverse map { values %$_ } @$post_config) {
525             $config = Hash::Merge::merge($config, $config_datum);
526             }
527             }
528              
529             return $config;
530             }
531              
532             sub _load_via_config_any {
533             my ($class, $extra_paths) = @_;
534             my @files = $class->_valid_config_files($class->_default_paths, $extra_paths);
535             my $config = $class->_config_any_load_files(@files);
536              
537             my @config_data = map { values %$_ } @$config;
538             return @config_data;
539             }
540              
541             sub _config_any_load_files {
542             my ($class, @files) = @_;
543              
544             return Config::Any->load_files({
545             files => \@files,
546             use_ext => 1,
547             });
548             }
549              
550             sub _valid_config_files {
551             my ($class, $default_paths, $extra_paths) = @_;
552             my @extensions = $class->_allowed_extensions;
553             my @paths = $class->_normalize_config_path($default_paths, $extra_paths);
554             my @config_files = grep { -e $_} map {
555             my $path = $_;
556             map {
557             $ENV{TEST_DBIC_CONFIG_SUFFIX} ?
558             ("$path.$_", "$path$ENV{TEST_DBIC_CONFIG_SUFFIX}.$_") :
559             ("$path.$_");
560             } @extensions;
561             } @paths;
562              
563             return @config_files;
564             }
565              
566             sub _allowed_extensions {
567             return @{ Config::Any->extensions };
568             }
569              
570             sub _normalize_external_paths {
571             my ($class, $extra_paths) = @_;
572             my @extra_paths;
573             if(!ref $extra_paths) {
574             @extra_paths = ([$extra_paths]); ## "t/etc" => (["t/etc"])
575             } elsif(ref $extra_paths eq 'ARRAY') {
576             if(!ref $extra_paths->[0]) {
577             @extra_paths = ($extra_paths); ## [qw( t etc )]
578             } elsif( ref $extra_paths->[0] eq 'ARRAY') {
579             @extra_paths = @$extra_paths;
580             }
581             }
582             return @extra_paths;
583             }
584              
585             sub _normalize_config_path {
586             my ($class, $default_paths, $extra_paths) = @_;
587              
588             if(defined $extra_paths) {
589             my @extra_paths = map { "$_" eq "+" ? @$default_paths : $_ } map {
590             my @local_path = ref $_ ? @$_ : ($_);
591             Path::Class::file(@local_path);
592             } $class->_normalize_external_paths($extra_paths);
593              
594             return @extra_paths;
595             } else {
596             return @$default_paths;
597             }
598             }
599              
600             sub _script_path {
601             return ($0 =~m/^(.+)\.t$/)[0];
602             }
603              
604             sub _default_fixture_paths {
605             my ($class) = @_;
606             my $script_path = Path::Class::file($class->_script_path);
607             my $script_dir = $script_path->dir;
608             my @dir_parts = $script_dir->dir_list(1);
609              
610             return [
611             Path::Class::file(qw/t etc fixtures/),
612             Path::Class::file(qw/t etc fixtures/, @dir_parts, $script_path->basename),
613             ];
614              
615             }
616              
617             sub _default_paths {
618             my ($class) = @_;
619             my $script_path = Path::Class::file($class->_script_path);
620             my $script_dir = $script_path->dir;
621             my @dir_parts = $script_dir->dir_list(1);
622              
623             if(
624             $script_path->basename eq 'schema' &&
625             (scalar(@dir_parts) == 0 )
626             ) {
627             return [
628             Path::Class::file(qw/t etc schema/),
629             ];
630              
631             } else {
632             return [
633             Path::Class::file(qw/t etc schema/),
634             Path::Class::file(qw/t etc /, @dir_parts, $script_path->basename),
635             ];
636             }
637             }
638              
639             sub _initialize_schema {
640             my $class = shift @_;
641             my $config = shift @_;
642             my $builder = __PACKAGE__->builder;
643            
644             my $fail_on_schema_break = delete $config->{fail_on_schema_break};
645             my $schema_manager;
646             eval {
647             $schema_manager = Test::DBIx::Class::SchemaManager->initialize_schema({
648             %$config,
649             builder => $builder,
650             });
651             }; if ($@ or !$schema_manager) {
652             Test::More::diag("Can't initialize a schema with the given configuration");
653             Test::More::diag("Returned Error: ".$@) if $@;
654             Test::More::diag(
655             Test::More::explain("configuration: " => $config)
656             );
657             # FIXME: make this optional.
658             if($fail_on_schema_break)
659             {
660             Test::More::fail("Failed remaining tests since we don't have a schema");
661             Test::More::done_testing();
662             $builder->finalize();
663             exit(0);
664             }
665             else
666             {
667             $builder->skip_all("Skipping remaining tests since we don't have a schema");
668             }
669             }
670              
671             return $schema_manager
672             }
673              
674             1;
675              
676             __END__