File Coverage

blib/lib/Spreadsheet/Compare/Config.pm
Criterion Covered Total %
statement 155 158 98.1
branch 39 54 72.2
condition 24 43 55.8
subroutine 21 21 100.0
pod 2 3 66.6
total 241 279 86.3


line stmt bran cond sub pod time code
1             package Spreadsheet::Compare::Config;
2              
3 14     21   107 use Mojo::Base -base, -signatures;
  14         31  
  14         115  
4 14     14   3839 use Spreadsheet::Compare::Common;
  14         33  
  14         101  
5 14     14   116 use Storable qw(dclone);
  14         33  
  14         1128  
6 14     14   8367 use Mojo::Template;
  14         58631  
  14         142  
7 14     14   644 use Mojo::Util qw(monkey_patch);
  14         34  
  14         32549  
8              
9             my( $trace, $debug );
10             my %defaults;
11             my %sources;
12             my %protected;
13              
14             #<<<
15             has from => undef;
16             has plan => sub { [] };
17             has globals => sub { {} }, ro => 1;
18             #>>>
19              
20              
21 40     40   124 sub import ( $class, $cfg = {}, %args ) {
  40         92  
  40         84  
  40         171  
  40         71  
22 40         250 my $caller = caller;
23 40         2547 while ( my( $key, $value ) = each $cfg->%* ) {
24             croak "duplicate definition for config option $key"
25             if exists( $defaults{$key} )
26 410 50 66     16889 and $protected{$key};
27 410         754 my $rt = ref($value);
28 410 100 100 27   1366 my $wrap = ( not $rt or $rt eq 'CODE' ) ? $value : sub { $value };
  27         225  
29 410         1077 $sources{$caller}{$key} = $defaults{$key} = $wrap;
30 410 100       938 $protected{$key} = 1 if $args{protected};
31 410 50       1206 Mojo::Base::attr( $caller, $key, $wrap ) if $args{make_attributes};
32             }
33 40     18   1512 monkey_patch( $caller, 'config_defaults', sub { $sources{$caller} } );
  18     18   59  
        18      
        18      
        18      
34 40         69847 return;
35             }
36              
37              
38 12     12 0 27 sub init ($self) {
  12         40  
  12         25  
39 12 100       93 $self->load( $self->{from} ) if $self->{from};
40 12         44 $self->{current} = 0;
41 12         51 return $self;
42             }
43              
44              
45 11     11 1 29 sub load ( $self, $src ) {
  11         23  
  11         38  
  11         20  
46 11         51 $self->_make_plan( _load($src) );
47 11         53 return $self;
48             }
49              
50              
51 67     67 1 578 sub next_test ($self) {
  67         114  
  67         101  
52 67         118 state $idx = 0;
53 67         190 my $test = $self->plan->[ $idx++ ];
54 67 100       405 $idx = 0 unless $test;
55 67         240 return $test;
56             }
57              
58              
59 15     15   40 sub _load ($src) {
  15         31  
  15         26  
60 15         31 my $cfg;
61 15 100       80 if ( my $rtype = ref($src) ) {
62 1 50 33     20 if ( $rtype eq 'ARRAY' or $rtype eq 'HASH' ) {
    0          
63 1         94 $cfg = dclone($src);
64             }
65             elsif ( $rtype eq 'GLOB' ) {
66 0         0 local $/ = undef;
67 0         0 $cfg = Load(<$src>);
68             }
69             else {
70 0         0 croak "invalid reference type '$rtype' for configuration";
71             }
72             }
73             else {
74 14         55 my $p = path($src);
75 14 50       468 $cfg = $p->is_file ? LoadFile($src) : Load($src);
76             }
77              
78 15         488924 my $rcfg = ref($cfg);
79 15 50 66     130 croak "invalid reference type '$rcfg' for configuration"
80             unless $rcfg eq 'ARRAY' or $rcfg eq 'HASH';
81 15 100       72 $cfg = [$cfg] if $rcfg eq 'HASH';
82              
83 15         101 return $cfg;
84             }
85              
86              
87 11     11   30 sub _make_plan ( $self, $cfg_main ) {
  11         33  
  11         30  
  11         22  
88              
89 11         78 my $globals = $self->_extract_globals($cfg_main);
90 11         79 $globals->{title} = _get_title_from_filename( $self->from );
91              
92 11         34 my $plan = [];
93 11         26 my %suite_globals;
94 11         39 for my $cfg (@$cfg_main) {
95 44         89 my $nbr = 1;
96 44 100       116 if ( $cfg->{suite} ) {
97             croak "'suite' parameter in config has to be an array of filenames\n"
98 2 50       11 unless ref( $cfg->{suite} ) eq 'ARRAY';
99 2   33     24 my $root = $cfg->{rootdir} // $globals->{rootdir};
100 2         9 for my $fn ( $cfg->{suite}->@* ) {
101 4 50       23 $fn = "$root/$fn" if $root;
102 4         32 DEBUG "reading suite file $fn";
103 4         69 my $sub_cfg = _load($fn);
104              
105 4         51 my $suite_title = _get_title_from_filename($fn);
106 4 50       16 $debug and DEBUG "suite title: $suite_title";
107 4         22 $suite_globals{$suite_title} = $self->_extract_globals($sub_cfg);
108              
109 4         13 my $snbr = 1;
110 4         15 for my $sub_entry (@$sub_cfg) {
111 14   33     75 $sub_entry->{$_} //= $cfg->{$_} for keys %$cfg;
112 14         29 $sub_entry->{suite_title} = $suite_title;
113 14   33     37 $sub_entry->{title} //= $suite_title . '_' . $snbr++;
114 14         36 push @$plan, $sub_entry;
115             }
116             }
117             }
118             else {
119 42         213 $cfg->{suite_title} = _get_title_from_filename( $self->from );
120 42   33     135 $cfg->{title} //= "Untitled_" . $nbr++;
121 42         110 push @$plan, $cfg;
122             }
123             }
124              
125 11         87 $self->_expand_plan( $plan, $globals, \%suite_globals );
126              
127 11         87 $self->plan($plan);
128 11         114 $self->{__ro__globals} = $globals;
129              
130 11         45 return $self;
131             }
132              
133              
134 15     15   38 sub _extract_globals ( $self, $cfg, $fn = '' ) {
  15         37  
  15         30  
  15         41  
  15         32  
135 15 50       89 $trace and TRACE '_extract_globals cfg:', Dump($cfg);
136 15   100     85 my @idx = grep { ( $cfg->[$_]{title} // '' ) eq '__GLOBAL__' } 0 .. $#$cfg;
  72         320  
137 15 50       76 croak "more than one __GLOBAL__ section in config $fn" if @idx > 1;
138 15 100       87 my $globals = @idx ? splice( @$cfg, $idx[0], 1 ) : undef;
139 15         54 delete $globals->{title};
140 15         56 return $globals;
141             }
142              
143              
144 11     11   30 sub _expand_plan ( $self, $plan, $globals, $sglobals ) {
  11         27  
  11         28  
  11         24  
  11         27  
  11         24  
145 11         782 my @t0 = localtime;
146 11         678 local $ENV{SC_DATE} = strftime( '%Y%m%d', @t0 );
147 11         369 local $ENV{SC_DATETIME} = strftime( '%Y%m%d%H%M%S', @t0 );
148 11         58 for my $test (@$plan) {
149 56 100 66     307 if ( $test->{suite_title} and my $sg = $sglobals->{ $test->{suite_title} } ) {
150 14         28 delete $sg->{title};
151 14   33     306 $test->{$_} //= $sg->{$_} for keys %$sg;
152             }
153 56   66     1039 $test->{$_} //= $globals->{$_} for keys %$globals;
154 56         208 $self->_expand_test($test);
155             }
156 11         146 $self->_expand_test($globals);
157 11         98 return $self;
158             }
159              
160              
161 1363     1363   1814 sub _expand_test ( $self, $test, $element = undef ) {
  1363         1779  
  1363         1720  
  1363         1995  
  1363         1725  
162 1363         1863 state $max_loop = 100;
163              
164 1363   66     2463 $element //= $test;
165 1363         1911 my $reftype = ref($element);
166 1363 100       3145 if ( $reftype eq 'ARRAY' ) {
    100          
    50          
167 170         364 $self->_expand_test( $test, $_ ) for grep { defined } @$element;
  356         878  
168             }
169             elsif ( $reftype eq 'HASH' ) {
170 108         319 $self->_expand_test( $test, $_ ) for grep { defined } values %$element;
  940         1697  
171             }
172             elsif ( not $reftype ) {
173 1085   50     1812 $_[2] //= '';
174 1085         1421 my $loop_count = 0;
175 1085         2831 while ( my( $sigil, $varname ) = $_[2] =~ /([\$\%])\{([^\}]+)\}/ ) {
176 114 100       286 my $src = $sigil eq '$' ? \%ENV : $test;
177             die "could not expand ${sigil}{$varname} in >>$test->{title}<<\n"
178 114 50       349 unless exists $src->{$varname};
179 114         308 my $rx = quotemeta("$sigil\{$varname\}");
180 114         1361 $_[2] =~ s/$rx/$src->{$varname}/g;
181 114 50       606 LOGDIE "continuous loop while expanding variable $sigil\{$varname\} in '$element'"
182             if ++$loop_count > $max_loop;
183             }
184             }
185              
186 1363         2723 return $self;
187             }
188              
189              
190 57     57   272 sub _get_title_from_filename ($filename) {
  57         115  
  57         82  
191 57 100 66     279 $filename = $0 if not $filename or ref($filename);
192 57         182 my $base = path($filename)->basename();
193 57         3370 $base =~ s/\.[^\.]+$//;
194 57         190 return $base;
195             }
196              
197              
198             1;
199              
200             =head1 NAME
201              
202             Spreadsheet::Compare::Config - Build Configuration from File or Reference
203              
204             =head1 SYNOPSIS
205              
206             use Spreadsheet::Compare::Config {
207             array => sub { [] },
208             hash => sub { {} },
209             param => undef,
210             }, make_attributes => 1, protected => 1;
211              
212             my $cfg = Spreadsheet::Compare::Config->new(from => 'test.yml');
213              
214             =head1 DESCRIPTION
215              
216             This modules is used to create attributes for the caller and keeps track of them,
217             so that it can check, if the same attribute is used in another module.
218              
219             It is also used for taking comparison configuration and expanding it with the
220             defined default values and references. It creates an execution plan consisting
221             of an array with all expanded configurations.
222              
223             =head1 ATTRIBUTES
224              
225             =head2 from
226              
227             If used directly in the constructor, will call L</load> directly with the attributes
228             value. Using it as a setter attribute at a later stage has no effect.
229              
230             =head2 globals
231              
232             A reference to a hash with the expanded values of the __GLOBAL__ configuration section.
233             Will be available after L</load> was called.
234              
235             =head2 plan
236              
237             A reference to an array of hashes containing the expanded parameters for comparisons.
238              
239              
240             =head1 METHODS
241              
242             =head2 load($source)
243              
244             Load a configuration. Source can be a reference to a hash with a single comparison definition,
245             a reference to an array with multiple definitions or a filename/filehandle of a YAML
246             configuration file containing either.
247              
248             =head2 next_test
249              
250             Return the next test configuration (a reference to a hash).
251             Will return undef once if the end is reached and restart at index 0 afterwards.
252              
253             =cut