File Coverage

blib/lib/Spreadsheet/Compare.pm
Criterion Covered Total %
statement 204 276 73.9
branch 41 90 45.5
condition 17 50 34.0
subroutine 21 31 67.7
pod 1 2 50.0
total 284 449 63.2


line stmt bran cond sub pod time code
1             package Spreadsheet::Compare 0.13;
2              
3             # TODO: (issue) allow list for reporters
4              
5 14     14   11979 use Mojo::Base 'Mojo::EventEmitter', -signatures;
  14         31  
  14         131  
6 14     14   27495 use Module::Load qw(autoload load);
  14         35  
  14         123  
7 14     14   8331 use Mojo::IOLoop;
  14         1643523  
  14         97  
8 14     14   766 use Config;
  14         30  
  14         582  
9              
10 14     14   83 use Spreadsheet::Compare::Common;
  14         33  
  14         124  
11 14     14   8170 use Spreadsheet::Compare::Config {}, protected => 1;
  14         44  
  14         101  
12 14     14   9085 use Spreadsheet::Compare::Single;
  14         49  
  14         188  
13              
14             my( $trace, $debug );
15              
16             my @counter_names = Spreadsheet::Compare::Single->counter_names;
17              
18             #<<<
19             has _cfo => undef;
20             has config => undef;
21             has errors => sub { [] }, ro => 1;
22             has exit_code => 0;
23             has jobs => 1;
24             has log_level => sub { $ENV{SPREADSHEET_COMPARE_DEBUG} };
25             has quiet => undef;
26             has result => sub { {} }, ro => 1;
27             has stdout => undef;
28             #>>>
29              
30             # emitted by Spreadsheet::Compare::Single
31             has _reporter_events => sub { [ qw(
32             _after_reader_setup
33             add_stream
34             mark_header
35             write_fmt_row
36             write_header
37             write_row
38             ) ] };
39              
40             # emitted by Spreadsheet::Compare::Single
41             has _test_events => sub { [ qw(
42             after_fetch
43             counters
44             final_counters
45             ) ] };
46              
47              
48 18     18   50 sub _setup_readers ( $self, $test ) {
  18         56  
  18         33  
  18         26  
49              
50 18         109 my $modname = "Spreadsheet::Compare::Reader::$test->{type}";
51 18         74 INFO "loading $modname";
52 18         174 load($modname);
53              
54 18         1465 my %args = map { $_ => $test->{$_} } grep { $modname->can($_) } keys %$test;
  75         313  
  209         1645  
55              
56 18         56 my @readers;
57 18         49 for my $index ( 0, 1 ) {
58 36 50       98 $debug and DEBUG "creating $modname instance $index";
59 36 50       1131 my $reader = $modname->new(
60             %args,
61             ) or LOGDIE "could not create $modname object";
62              
63 36         118 $reader->{__ro__index} = $index;
64              
65 36 100       115 my $side_name = $index ? $test->{right} : $test->{left};
66 36 50       84 $reader->{__ro__side_name} = $side_name if $side_name;
67              
68 36         92 push @readers, $reader;
69             }
70              
71 18         53 $test->{readers} = \@readers;
72              
73 18         63 return $self;
74             }
75              
76              
77 18     18   35 sub _setup_reporter ( $self, $test, $single ) {
  18         42  
  18         31  
  18         34  
  18         34  
78              
79 18 100 66     119 if ( not $test->{reporter} or $test->{reporter} =~ /^none$/i ) {
80 11         24 $self->{_current_reporter} = undef;
81 11         20 return;
82             }
83              
84 7         27 my $modname = "Spreadsheet::Compare::Reporter::$test->{reporter}";
85 7 50       23 $debug and DEBUG "creating $modname instance";
86 7         32 load($modname);
87              
88 7         503 my %args = map { $_ => $test->{$_} } grep { $modname->can($_) } keys %$test;
  14         65  
  116         606  
89              
90 7 50       98 $args{report_filename} =~ s/\s+/_/g if $args{report_filename};
91              
92 7         59 INFO "Reporter Args: ", Dump( \%args );
93 7         25683 $self->{_current_reporter} = my $rep_obj = $modname->new( \%args );
94 7         290530 $rep_obj->{__ro__test_title} = $test->{title};
95 7         77 $rep_obj->setup();
96 7         59 for my $ev ( $self->_reporter_events->@* ) {
97 42 50       369 $trace and TRACE "subscribe event $ev";
98 5436         9880 $single->on(
99             $ev,
100 5436     5436   7842 sub ( $em, @args ) {
  5436         77163  
  5436         12604  
101 5436 50       16073 $trace and TRACE "calling $ev for reporter $test->{reporter}";
102 5436         32001 $rep_obj->$ev(@args);
103             }
104 42         677 );
105             }
106              
107 7         99 return $self;
108             }
109              
110              
111 12     12 0 25 sub init ($self) {
  12         26  
  12         23  
112 12 50       43 die "jobs has to be an integer > 0\n" if $self->jobs < 1;
113              
114 12 0 33     717 unless ( $Config{d_fork} or $Config{d_pseudofork} ) {
115 0         0 warn "cannot use fork, resetting jobs to 1\n";
116 0         0 $self->jobs(1);
117             }
118              
119 12         91 $self->_setup_logging();
120 12         50 ( $trace, $debug ) = get_log_settings();
121              
122 12         218 $self->_cfo( Spreadsheet::Compare::Config->new( from => $self->config ) );
123              
124 12         126 return $self;
125             }
126              
127              
128 12     12 1 2093 sub run ($self) {
  12         31  
  12         22  
129              
130 12 50       63 local $| = 1 if $self->stdout;
131              
132 12 100       106 unless ($self->_cfo->plan->@*) {
133 2 100       13 croak "no configuration given!" unless $self->config;
134 1         6 $self->_cfo->load($self->config);
135             }
136 11         131 my $cfg = $self->_cfo;
137              
138 11         50 my %summary;
139 11         70 my $result = $self->result;
140 11         29 my @sp_queue;
141 11         81 $self->errors->@* = ();
142              
143 28         126 $self->on(
144 28     28   169 summary => sub ( $s, $sdata ) {
  28         1120  
  28         86  
145 28         617 INFO "Adding result to $sdata->{type} summary info";
146 28   100     830 my $suite_sum = $summary{ $sdata->{type} }{ $sdata->{suite_title} } //= [];
147 28         127 push @$suite_sum, $sdata;
148             }
149 11         167 );
150              
151 11   33     163 my $no_counters = $self->quiet || $ENV{HARNESS_ACTIVE};
152 11 50 33     179 if ( $self->stdout and not $no_counters ) {
153 0     0   0 $self->on( counters => sub ( $s, @data ) { $self->_output_line_counter(@data) } );
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
154             }
155              
156             $self->on(
157 56     56   145 final_counters => sub ( $s, @data ) {
  56         2923  
  56         142  
  56         183  
158 56         209 my( $title, $counter ) = @data;
159 56         346 $result->{$title} = $counter;
160             }
161 11         164 );
162              
163 11         107 $self->_mod_check($cfg);
164              
165 11         83 while ( my $test = $cfg->next_test ) {
166              
167 56 100       147 if ( $self->jobs == 1 ) { # run instantly
168 18     0   259 try { $self->_single_run($test) } catch { $self->_handle_error( $test, $_ ) };
  18         1105  
  0         0  
169             }
170             else { # queue for running in subprocess
171 0         0 push(
172             @sp_queue, {
173             test => $test,
174 0     0   0 sub => sub ($sp) { $self->_single_run( $test, $sp ) },
  0         0  
  0         0  
175             }
176 38         356 );
177             }
178              
179             }
180              
181 11 100       91 $self->_run_subprocesses( \@sp_queue ) if @sp_queue;
182              
183 11 50       180 if ( $self->stdout ) {
184 0 0       0 say "" unless $self->quiet;
185 0         0 for my $title ( sort keys %$result ) {
186 0         0 say $title;
187 0         0 my $cnt = $result->{$title};
188             my $cline = sprintf "LEF:%06s RIG:%06s SAM:%06s DIF:%06s LIM:%06s MIS:%06s ADD:%06s DUP:%06s",
189 0         0 @$cnt{@counter_names};
190 0         0 say $cline;
191             }
192             }
193              
194 11         337 my $globals = $cfg->globals;
195 11         232 for my $stype ( keys %summary ) {
196 4         90 INFO "Writing $stype summary file";
197 4         58 my $modname = "Spreadsheet::Compare::Reporter::$stype";
198 4         109 load($modname);
199 4   50     365 my $reporter = $modname->new( rootdir => $globals->{rootdir} // '.' );
200 4   33     58 $reporter->write_summary( $summary{$stype}, $globals->{summary_filename} // $globals->{title} );
201             }
202              
203 11   50     1261 my $ec = $self->{failed} // 0;
204 11 50       102 $ec = 255 if $ec > 255;
205 11         126 $self->exit_code($ec);
206              
207 11         241 return $self;
208             }
209              
210              
211 11     11   29 sub _mod_check ( $self, $cfg ) {
  11         30  
  11         31  
  11         29  
212              
213 11 50 66     57 if ( $self->jobs > 1 and $^O eq 'MSWin32' ) {
214             try {
215 0     0   0 load('Mojo::IOLoop::Thread');
216 0         0 my $ver = $Mojo::IOLoop::Thread::VERSION;
217 0 0       0 croak "$ver" if $ver < 0.10;
218             }
219             catch {
220 0     0   0 warn "--jobs with a value > 1 needs Mojo::IOLoop::Thread >= 0.10 , resetting to 1!\n";
221 0         0 $self->jobs(1);
222 0         0 };
223             }
224              
225 11     15   265 my $has_csv = any { $_->{type} eq 'CSV' } $cfg->plan->@*;
  15         140  
226 11 50 66     78 if ( $self->jobs > 1 and $^O eq 'MSWin32' and $has_csv ) {
      33        
227 0         0 warn "--jobs with a value > 1 uses Text::CSV_PP, which may be slower than\n";
228 0         0 warn "using the default (--jobs 1) and being able to use the non thread safe\n";
229 0         0 warn "Text::CSV_XS.\n";
230 0         0 $ENV{PERL_TEXT_CSV} = 'Text::CSV_PP';
231             }
232              
233 11         214 return $self;
234             }
235              
236              
237 7     7   15 sub _run_subprocesses ( $self, $queue ) {
  7         15  
  7         23  
  7         16  
238 7         33 my $max = $self->jobs;
239 7         101 my $ioloop = Mojo::IOLoop->singleton;
240              
241 7         35 my $todo = @$queue;
242 7         16 my $started = my $finished = 0;
243 787         2382 $ioloop->recurring(
244 787     787   1721 0.1 => sub ($loop) {
  787         62638627  
245 787 100       4465 if ( $started - $finished < $max ) {
246 499         4595 my $sproc = Mojo::IOLoop->subprocess;
247 499         47543 $sproc->on( progress => sub ( $sp, @data ) { $self->emit(@data) } );
  979         1475  
  979         3507  
248 499 100       10939 if ( my $job = shift @$queue ) {
249             $sproc->run(
250             $job->{sub},
251 38         104 sub ( $sp, $err, @results ) {
252 38         121 $finished++;
253 38 50       293 return unless $err;
254 0         0 $self->_handle_error( $job->{test}, $err );
255             },
256 38         933 );
257 38         5776 $started++;
258             }
259             }
260 787 100       3966 $loop->stop if $finished == $todo;
261             },
262 7         135 );
263 7         837 $ioloop->start;
264              
265 7         882 return $self;
266             }
267              
268              
269 18     18   37 sub _single_run ( $self, $test, $sp = undef ) {
  18         34  
  18         34  
  18         39  
  18         30  
270 18         89 $self->_setup_logging($test);
271              
272 18         51 $self->{new_test} = 1;
273              
274 18         83 INFO '';
275 18         193 INFO '=' x 50;
276 18         204 INFO "|| RUNNING TEST >>$test->{title}<<";
277 18         138 INFO '=' x 50;
278              
279 18 50   0   171 $debug and DEBUG 'running compare with config:', sub { Dump($test) };
  0         0  
280 18         98 $self->_setup_readers($test);
281              
282 18         520 my $single = Spreadsheet::Compare::Single->new($test);
283              
284 18         90 my $title = join( '/', $test->{suite_title}, $test->{title} );
285 18         101 INFO "running comparison $title";
286              
287 18         245 $self->_setup_reporter( $test, $single );
288              
289 18 0       96 my $emit =
    50          
290             $sp
291             ? $^O eq 'MSWin32'
292             ? \&Mojo::IOLoop::Thread::progress
293             : \&Mojo::IOLoop::Subprocess::progress
294             : \&Mojo::EventEmitter::emit;
295 18   33     132 $sp //= $self;
296              
297 18         73 for my $ev ( $self->_test_events->@* ) {
298 54     298   845 $single->on( $ev => sub ( $e, @data ) { $sp->$emit( $ev, $title, @data ) } );
  298         556  
  298         1093  
  298         4656  
  298         550  
  298         735  
299             }
300              
301 18         265 my $result = $single->compare();
302              
303 18 100       73 if ( my $reporter = $self->{_current_reporter} ) {
304 7         55 $sp->$emit( 'report_finished', $title, ref($reporter), $reporter->report_fullname->stringify );
305 7         597 $reporter->save_and_close;
306 7 50       71 if ( my $stype = $test->{summary} ) {
307 7         107 ( my $ttitle = $test->{title} ) =~ s/[^\w]/_/g;
308 7         42 ( my $stitle = $test->{suite_title} ) =~ s/[^\w]/_/g;
309             $sp->$emit(
310             'summary', {
311             type => $stype,
312             full => "${ttitle}_$stitle",
313             title => $ttitle,
314             suite_title => $test->{suite_title},
315 7 50       794 result => {%$result}, # in case the reporter changes the hash
316             report => $reporter ? path( $reporter->report_fullname )->absolute : '',
317             }
318             );
319             }
320             }
321              
322 18         4827 return $self;
323             }
324              
325              
326 0     0   0 sub _handle_error ( $self, $test, $err ) {
  0         0  
  0         0  
  0         0  
  0         0  
327 0         0 my $msg = "failed to run compare for '$test->{title}', $err";
328 0 0       0 say $msg if $self->stdout;
329 0         0 ERROR $msg;
330 0 0       0 ERROR call_stack() if $debug;
331 0         0 push $self->errors()->@*, $msg;
332 0         0 $self->{failed}++;
333 0         0 return;
334             }
335              
336              
337 0     0   0 sub _output_line_counter ( $self, $title, $counters ) {
  0         0  
  0         0  
  0         0  
  0         0  
338 0         0 state $all = {};
339 0         0 my $first = !scalar( keys %$all );
340 0         0 $all->{$title} = $counters;
341 0     0   0 my $cstr = sprintf( '%010d', reduce { $a + $b->{left} + $b->{right} } 0, values %$all );
  0         0  
342 0 0       0 print "\b" x length($cstr) unless $first;
343 0         0 print $cstr;
344 0         0 return $self;
345             }
346              
347              
348 30     30   75 sub _setup_logging ( $self, $test = {} ) {
  30         59  
  30         59  
  30         57  
349              
350 30         103 my $gll = $self->log_level;
351 30 50 33     267 my $logfn = $test->{log_file} // ( $gll ? 'STDERR' : undef );
352 30 50       130 return unless $logfn;
353              
354 0   0       my $level_name = $test->{log_level} // $gll // 'INFO';
      0        
355 0           my $level = Log::Log4perl::Level::to_priority($level_name);
356              
357 0 0         if ( my $appender = Log::Log4perl->appender_by_name('app001') ) {
358 0           my $logger = Log::Log4perl->get_logger('');
359 0 0         $logger->level($level) if $logger->level ne $level;
360 0 0 0       if ( $appender->isa('Log::Log4perl::Appender::File') and $logfn ne 'STDERR' ) {
    0 0        
361 0 0         if ( $logfn ne $appender->filename ) {
362 0           INFO "Switching log to '$logfn'";
363 0           $appender->file_switch($logfn);
364             }
365 0           return;
366             }
367             elsif ( $appender->isa('Log::Log4perl::Appender::Screen') and $logfn eq 'STDERR' ) {
368 0           return;
369             }
370             }
371              
372             my $layout =
373             $ENV{HARNESS_ACTIVE} ? '[%P] %M(%L) %m{chomp}%n'
374 0 0 0       : $test->{log_layout} // $logfn eq 'STDERR' ? '[%P] %m{chomp}%n'
    0          
375             : '%d{ISO8601} %p [%P] (%M) %m{chomp}%n';
376              
377 0           Log::Log4perl->easy_init( {
378             file => $logfn,
379             level => $level,
380             layout => $layout,
381             } );
382              
383 0     0     $SIG{__WARN__} = sub { WARN @_ };
  0            
384              
385 0           return;
386             }
387              
388              
389             1;
390              
391             =head1 NAME
392              
393             Spreadsheet::Compare - Module for comparing spreadsheet-like datasets
394              
395             =head1 SYNOPSIS
396              
397             use Spreadsheet::Compare;
398             my $cfg = {
399             type => 'CSV',
400             title => 'Test 01',
401             files => ['left.csv', 'right.csv'],
402             identity => ['RowId'],
403             }
404             Spreadsheet::Compare->new(config => $cfg)->run();
405              
406             or
407              
408             Spreadsheet::Compare->new->config($cfg)->run;
409              
410             =head1 DESCRIPTION
411              
412             Spreadsheet::Compare analyses differences between two similar record sets.
413             It is designed to be used for regression testing of a large number of files,
414             databases or spreadsheets.
415              
416             The record sets can be read from a variety of different sources like CSV files,
417             fixed column input, databases, Excel or Open/Libre Office Spreadsheets.
418              
419             The differences can be saved in XLSX or HTML format and are visually highlighted
420             to allow fast access to the relevant parts.
421              
422             Configuration of a single comparison, sets of comparisons or whole suites
423             can be defined as YAML configuration files in order to persist a setup that can be run
424             multiple times.
425              
426             Spreadsheet::Compare is the central component normally used by executing the commandline utility
427             L<spreadcomp> with a configuration file. It feeds the relevant parts of the configuration data
428             to the reader, compare and reporting classes.
429              
430             Reading is done by subclasses of L<Spreadsheet::Compare::Reader>, The actual comparison is done
431             by L<Spreadsheet::Compare::Single> while the reporting output is handled by subclasses of
432             L<Spreadsheet::Compare::Reporter>.
433              
434             =head1 ATTRIBUTES
435              
436             $sc = Spreadsheet::Compare->new;
437              
438             All attributes will return the Spredsheet::Compare object when called as setter to allow
439             method chaining.
440              
441             =head2 config
442              
443             $sc->config($cfg);
444             my $conf = $sc->config;
445              
446             Get/Set the configuration for subsequent calls to run(). It hast to be either a hash reference
447             with a single comparison configuration, a reference to an array with multiple configurations or
448             the path to a YAML file.
449              
450             =head2 errors
451              
452             say "found error: $_" for $sc->run->errors->@*;
453              
454             (B<readonly>) Returns a reference to an array of error messages. These are errors that prevented a single
455             comparison from being executed.
456              
457             =head2 exit_code;
458              
459             my $ec = $sc->run->exit_code;
460              
461             (B<readonly>) Exit code, will contain the number of comparisons with detected differences
462             or 255 if the nuber exceeds 254.
463              
464             =head2 log_level
465              
466             $sc->log_level('INFO');
467             my $lev = $sc->log_level;
468              
469             The global log level (valid are the strings TRACE, DEBUG, INFO, WARN, ERROR or FATAL).
470             Default is no logging at all. The level can also be set with the environment variable
471             B<C<SPREADSHEET_COMPARE_DEBUG>>. Using the attribute has precedence.
472              
473             For a single comparison the log level can also be set with the C<log_level> option.
474              
475             =head2 quiet
476              
477             $sc->quiet(1);
478             my $is_quiet = $sc->quiet;
479              
480             Suppress the line counter when using L</stdout>.
481              
482             =head2 result
483              
484             my $res = $sc->run->result;
485             say "$_ found $res->{$_}{diff} differences" for sort keys %$res;
486              
487             (B<readonly>) The result is a reference to a hash with the test titles as keys and the comparison
488             counters as result.
489              
490             {
491             <title1> => {
492             add => <number of additional records on the right>,
493             diff => <number of found differences>,
494             dup => <number of duplicate rows (maximum of left and right)>,
495             left => <number of records on the left>,
496             limit => <number of record with differences below set ste limits>,
497             miss => <number of records missing on the right>,
498             right => <number of records on the right>,
499             same => <number of identical records>,
500             },
501             <title2> => {
502             ...
503             }
504              
505             =head2 stdout
506              
507             $sc->stdout(1);
508             my $use_stdout = $sc->stdout;
509              
510             Report progress and results to stdout.
511              
512             =head1 METHODS
513              
514             C<Spreadsheet::Compare> is a L<Mojo::EventEmitter> and additionally implements the following methods.
515              
516             =head2 run
517              
518             Run all defined comparisons. Returns the object itself. Throws exceptions on global errors.
519             Exceptions during a single comparison are trapped and will be accessible via the L</errors>
520             attribute.
521              
522              
523             =head1 CONFIGURATION
524              
525             A configuration can contain one or more comparison definitions. They have to be defined as a
526             single hashref or a reference to an array of hashes, each hash defining one comparison. The keys of
527             the hash specify the names of the options. Options that are common to all specified comparisons
528             can be given in a special hash with C<title> set to B<__GLOBAL__>.
529              
530             An example for a very basic configuration with 2 CSV comparisons:
531              
532             [
533             {
534             title => '__GLOBAL__',
535             type => 'CSV',
536             rootdir => 'my/data/dir',
537             reporter => 'XSLX',
538             report_filename => '%{title}.xlsx',
539             },
540             {
541             title => 'all defaults',
542             files => [
543             'left/simple01.csv',
544             'right/simple01.csv'
545             ],
546             identity => ['A'],
547             },
548             {
549             title => 'semicolon separator',
550             files => [
551             'left/simple02.csv',
552             'right/simple02.csv',
553             ],
554             identity => ['A'],
555             limit_abs => {
556             D => '0.1',
557             B => '1',
558             },
559             ignore => [
560             'Z',
561             ],
562             csv_options => {
563             sep_char => ';',
564             },
565             },
566             ];
567              
568             or as YAML config file
569              
570             ---
571             - title: __GLOBAL__
572             type: CSV
573             rootdir : my/data/dir
574             reporter: XLSX
575             report_filename: %{title}.xlsx,
576             - title : all defaults
577             files :
578             - left/simple01.csv
579             - right/simple01.csv
580             identity: [A]
581             - title: semicolon separator
582             files:
583             - left/simple02.csv
584             - right/simple02.csv
585             identity: [A]
586             ignore:
587             - Z
588             limit_abs:
589             D: 0.1
590             B: 1
591             csv_options:
592             sep_char: ';'
593              
594             To define a suite of compare batches, a central config using the L</suite> option can be used.
595              
596             To save unneccesary typing, configuration values can contain references to either environment
597             variables or other single configuration values. To use environment variables refer to them as
598             ${<VARNAME>}, for configuration references use %{<OPTION>}. This is a simple search and
599             replace mechanism, so don't expect fancy things like referencing non scalar options to work.
600              
601              
602             =head1 OPTION REFERENCE
603              
604             The following configuration options can be used to define a comparison. Options for Readers
605             are documented in L<Spreadsheet::Compare::Reader> or it's specific modules:
606              
607             =over 4
608              
609             =item * L<Spreadsheet::Compare::Reader::CSV> for CSV files
610              
611             =item * L<Spreadsheet::Compare::Reader::DB> for database tables
612              
613             =item * L<Spreadsheet::Compare::Reader::FIX> for fixed record files
614              
615             =item * L<Spreadsheet::Compare::Reader::WB> for various spreadsheet formats (XLS, XLSX, ODS, ...)
616              
617             =back
618              
619             The the reporting options are documented in L<Spreadsheet::Compare::Reporter> or it's specific modules:
620              
621             =over 4
622              
623             =item * L<Spreadsheet::Compare::Reporter::XLSX> for generating XLSX reports
624              
625             =item * L<Spreadsheet::Compare::Reporter::HTML> for generating static HTML reports
626              
627             =back
628              
629             =head2 left
630              
631             possible values: <string>
632             default: 'left'
633              
634             A descriptive name for the left side of the comparison
635              
636             =head2 log_file
637              
638             The file to write logs to.
639              
640             =head2 log_level
641              
642             possible values: TRACE|DEBUG|INFO|WARN|ERROR|FATAL
643             default: undef
644              
645             The log level for the current comparison. To set this globally, use the -d option
646             when using L<spreadcomp> or set the environment variable B<C<SPREADSHEET_COMPARE_DEBUG>>.
647             This option takes precedence over the global options.
648              
649             =head2 reporter
650              
651             possible values: <string>
652             default: 'None'
653              
654             Choose a reporter module (e.g. XLSX or HTML)
655              
656             =head2 right
657              
658             possible values: <string>
659             default: 'right'
660              
661             A descriptive name for the right side of the comparison
662              
663             =head2 rootdir
664              
665             possible values: <string>
666             default: 0
667              
668             Root directory for configuration, report or input files. Options containing relative
669             file names will be interpreted as relative to this directory.
670              
671             =head2 suite
672              
673             possible values: <list of filenames>
674             default: undef
675              
676             Use a list of config files. The configurations will inherit all settings defined in
677             the master config file. For an example please have a look at the tests from the t directory
678             of the distribution.
679              
680             =head2 summary
681              
682             possible values: <string>
683             default: undef
684              
685             Reporter engine for the summary. This will mostly be set in a global section, but can
686             be used to pick only selected comparisons for inclusion into the summary. You could
687             even specify a different summary engine for each comparison.
688              
689             =head2 summary_filename
690              
691             possible values: <string>
692             default: undef
693              
694             Specify a summary filename. If not specified the filename will be derived from the
695             filename of the configuration file or the name of the calling program.
696              
697             =head2 title
698              
699             possible values: <string>
700             default: ''
701              
702             The title of the current comparison. If not set, the title will be the title of the
703             current suite file or 'Untitled', followed by the current comparison count.
704              
705              
706             =head1 EVENTS
707              
708             Spreadsheet::Compare is a L<Mojo::EventEmitter> and emits the same events as
709             L<Spreadsheet::Compare::Single> (see L<Spreadsheet::Compare::Single/EVENTS>).
710              
711             B<WARNING> The events from Spreadsheet::Compare will have the title of the
712             single comparison as an additional second parameter. So instead of:
713              
714             $single->on(final_counters => sub ($obj, $counters) {
715             # code
716             });
717              
718             it will be:
719              
720             $sc->on(final_counters => sub ($obj, $title, $counters) {
721             # code
722             });
723              
724             In addition it emits the following events:
725              
726             =head2 report_finished
727              
728             $sc->on(report_finished => sub ($obj, $title, $reporter_class, $report_filename) {
729             say "finished writing $report_filename";
730             });
731              
732             Emitted after a single report is finished by the reporter
733              
734             =head2 summary
735              
736             require Data::Dumper;
737             $sc->on(summary => sub ($obj, $title, $counters) {
738             say "next fetch for $title:", Dumper($counters);
739             });
740              
741             Emitted after a single comparison
742              
743             =head1 LIMITING MEMORY USAGE
744              
745             Per default Spreadsheet::Compare will load all records into memory before comparing them.
746             This maybe not the best approach in cases where the number of records is very large and
747             may not even fit into memory (which will terminate the perl interpreter).
748              
749             There are two ways to handle these situations:
750              
751             =head2 Sorted Data with option L<Spreadsheet::Compare::Reader/fetch_size>
752              
753             The option L<Spreadsheet::Compare::Single/fetch_size> can be used to limit the number of
754             records that are read into memory at a time (for each side, so the read number is twice
755             the number given). For that to work the records have to sorted by the configured
756             L<Spreadsheet::Compare::Reader/identity> and L<Spreadsheet::Compare::Single/is_sorted>
757             have to be set to a true value. Possibly overlapping identity values at the borders of a
758             fetch will be handled correctly.
759              
760             =head2 Chunking
761              
762             When sorting is not an option, memory usage can be reduced by sacrificing speed using the
763             L</chunk> option with Readers that support this (e.g. CSV and FIX, it is not implemented
764             for the DB reader because sorting by identity can be done in SQL).
765              
766             Chunking means that the the data will be read twice. First to determine the chunk name for
767             the record and keeping only that and the record location for reference. In the second pass
768             data will be read one chunk at a time. Since the chunk naming can be freely configured
769             (e.g with regular expressions) the possibilities for limiting the chunk size are numerous.
770              
771             See L<Spreadsheet::Compare::Reader/chunk> for examples.
772              
773             =head1 Limiting the number of records for testing configuration options
774              
775             While testing the configuration of a comparison (for example finding the correct
776             identity, the columns you want to ignore or fine tune the limits), always comparing
777             whole data sets can be tedious. For sorted data this can be achieved with the option
778             L<Spreadsheet::Compare::Single/fetch_limit> limiting the number of fetches.
779              
780             For unsorted data it is best to simply use selected subsets of the data.
781              
782             =cut