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   168 use Mojo::Base -base, -signatures;
  14         30  
  14         121  
4 14     14   4153 use Spreadsheet::Compare::Common;
  14         27  
  14         90  
5 14     14   116 use Storable qw(dclone);
  14         35  
  14         1052  
6 14     14   8582 use Mojo::Template;
  14         58421  
  14         170  
7 14     14   759 use Mojo::Util qw(monkey_patch);
  14         34  
  14         32720  
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   118 sub import ( $class, $cfg = {}, %args ) {
  40         113  
  40         83  
  40         145  
  40         76  
22 40         287 my $caller = caller;
23 40         2525 while ( my( $key, $value ) = each $cfg->%* ) {
24             croak "duplicate definition for config option $key"
25             if exists( $defaults{$key} )
26 410 50 66     29903 and $protected{$key};
27 410         710 my $rt = ref($value);
28 410 100 100 27   1362 my $wrap = ( not $rt or $rt eq 'CODE' ) ? $value : sub { $value };
  27         233  
29 410         1028 $sources{$caller}{$key} = $defaults{$key} = $wrap;
30 410 100       899 $protected{$key} = 1 if $args{protected};
31 410 50       1154 Mojo::Base::attr( $caller, $key, $wrap ) if $args{make_attributes};
32             }
33 40     18   1409 monkey_patch( $caller, 'config_defaults', sub { $sources{$caller} } );
  18     18   77  
        18      
        18      
        18      
34 40         67385 return;
35             }
36              
37              
38 12     12 0 29 sub init ($self) {
  12         36  
  12         25  
39 12 100       96 $self->load( $self->{from} ) if $self->{from};
40 12         56 $self->{current} = 0;
41 12         48 return $self;
42             }
43              
44              
45 11     11 1 41 sub load ( $self, $src ) {
  11         23  
  11         25  
  11         23  
46 11         49 $self->_make_plan( _load($src) );
47 11         56 return $self;
48             }
49              
50              
51 67     67 1 598 sub next_test ($self) {
  67         153  
  67         97  
52 67         112 state $idx = 0;
53 67         201 my $test = $self->plan->[ $idx++ ];
54 67 100       393 $idx = 0 unless $test;
55 67         218 return $test;
56             }
57              
58              
59 15     15   40 sub _load ($src) {
  15         32  
  15         25  
60 15         39 my $cfg;
61 15 100       65 if ( my $rtype = ref($src) ) {
62 1 50 33     8 if ( $rtype eq 'ARRAY' or $rtype eq 'HASH' ) {
    0          
63 1         105 $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         53 my $p = path($src);
75 14 50       497 $cfg = $p->is_file ? LoadFile($src) : Load($src);
76             }
77              
78 15         483819 my $rcfg = ref($cfg);
79 15 50 66     128 croak "invalid reference type '$rcfg' for configuration"
80             unless $rcfg eq 'ARRAY' or $rcfg eq 'HASH';
81 15 100       71 $cfg = [$cfg] if $rcfg eq 'HASH';
82              
83 15         91 return $cfg;
84             }
85              
86              
87 11     11   33 sub _make_plan ( $self, $cfg_main ) {
  11         28  
  11         28  
  11         22  
88              
89 11         59 my $globals = $self->_extract_globals($cfg_main);
90 11         80 $globals->{title} = _get_title_from_filename( $self->from );
91              
92 11         31 my $plan = [];
93 11         29 my %suite_globals;
94 11         34 for my $cfg (@$cfg_main) {
95 44         85 my $nbr = 1;
96 44 100       117 if ( $cfg->{suite} ) {
97             croak "'suite' parameter in config has to be an array of filenames\n"
98 2 50       12 unless ref( $cfg->{suite} ) eq 'ARRAY';
99 2   33     18 my $root = $cfg->{rootdir} // $globals->{rootdir};
100 2         9 for my $fn ( $cfg->{suite}->@* ) {
101 4 50       19 $fn = "$root/$fn" if $root;
102 4         30 DEBUG "reading suite file $fn";
103 4         54 my $sub_cfg = _load($fn);
104              
105 4         45 my $suite_title = _get_title_from_filename($fn);
106 4 50       19 $debug and DEBUG "suite title: $suite_title";
107 4         20 $suite_globals{$suite_title} = $self->_extract_globals($sub_cfg);
108              
109 4         12 my $snbr = 1;
110 4         13 for my $sub_entry (@$sub_cfg) {
111 14   33     66 $sub_entry->{$_} //= $cfg->{$_} for keys %$cfg;
112 14         32 $sub_entry->{suite_title} = $suite_title;
113 14   33     35 $sub_entry->{title} //= $suite_title . '_' . $snbr++;
114 14         33 push @$plan, $sub_entry;
115             }
116             }
117             }
118             else {
119 42         107 $cfg->{suite_title} = _get_title_from_filename( $self->from );
120 42   33     118 $cfg->{title} //= "Untitled_" . $nbr++;
121 42         100 push @$plan, $cfg;
122             }
123             }
124              
125 11         81 $self->_expand_plan( $plan, $globals, \%suite_globals );
126              
127 11         99 $self->plan($plan);
128 11         122 $self->{__ro__globals} = $globals;
129              
130 11         43 return $self;
131             }
132              
133              
134 15     15   37 sub _extract_globals ( $self, $cfg, $fn = '' ) {
  15         35  
  15         33  
  15         48  
  15         32  
135 15 50       87 $trace and TRACE '_extract_globals cfg:', Dump($cfg);
136 15   100     83 my @idx = grep { ( $cfg->[$_]{title} // '' ) eq '__GLOBAL__' } 0 .. $#$cfg;
  72         302  
137 15 50       86 croak "more than one __GLOBAL__ section in config $fn" if @idx > 1;
138 15 100       84 my $globals = @idx ? splice( @$cfg, $idx[0], 1 ) : undef;
139 15         54 delete $globals->{title};
140 15         53 return $globals;
141             }
142              
143              
144 11     11   25 sub _expand_plan ( $self, $plan, $globals, $sglobals ) {
  11         54  
  11         26  
  11         22  
  11         25  
  11         23  
145 11         715 my @t0 = localtime;
146 11         643 local $ENV{SC_DATE} = strftime( '%Y%m%d', @t0 );
147 11         361 local $ENV{SC_DATETIME} = strftime( '%Y%m%d%H%M%S', @t0 );
148 11         55 for my $test (@$plan) {
149 56 100 66     304 if ( $test->{suite_title} and my $sg = $sglobals->{ $test->{suite_title} } ) {
150 14         26 delete $sg->{title};
151 14   33     285 $test->{$_} //= $sg->{$_} for keys %$sg;
152             }
153 56   66     1027 $test->{$_} //= $globals->{$_} for keys %$globals;
154 56         199 $self->_expand_test($test);
155             }
156 11         109 $self->_expand_test($globals);
157 11         105 return $self;
158             }
159              
160              
161 1363     1363   1806 sub _expand_test ( $self, $test, $element = undef ) {
  1363         1738  
  1363         1725  
  1363         1912  
  1363         1682  
162 1363         1698 state $max_loop = 100;
163              
164 1363   66     2432 $element //= $test;
165 1363         1892 my $reftype = ref($element);
166 1363 100       2978 if ( $reftype eq 'ARRAY' ) {
    100          
    50          
167 170         307 $self->_expand_test( $test, $_ ) for grep { defined } @$element;
  356         912  
168             }
169             elsif ( $reftype eq 'HASH' ) {
170 108         348 $self->_expand_test( $test, $_ ) for grep { defined } values %$element;
  940         1624  
171             }
172             elsif ( not $reftype ) {
173 1085   50     1833 $_[2] //= '';
174 1085         1442 my $loop_count = 0;
175 1085         2716 while ( my( $sigil, $varname ) = $_[2] =~ /([\$\%])\{([^\}]+)\}/ ) {
176 114 100       278 my $src = $sigil eq '$' ? \%ENV : $test;
177             die "could not expand ${sigil}{$varname} in >>$test->{title}<<\n"
178 114 50       309 unless exists $src->{$varname};
179 114         306 my $rx = quotemeta("$sigil\{$varname\}");
180 114         1357 $_[2] =~ s/$rx/$src->{$varname}/g;
181 114 50       585 LOGDIE "continuous loop while expanding variable $sigil\{$varname\} in '$element'"
182             if ++$loop_count > $max_loop;
183             }
184             }
185              
186 1363         2823 return $self;
187             }
188              
189              
190 57     57   278 sub _get_title_from_filename ($filename) {
  57         90  
  57         85  
191 57 100 66     251 $filename = $0 if not $filename or ref($filename);
192 57         175 my $base = path($filename)->basename();
193 57         3175 $base =~ s/\.[^\.]+$//;
194 57         183 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