File Coverage

blib/lib/App/GitHooks/Test.pm
Criterion Covered Total %
statement 136 157 86.6
branch 15 36 41.6
condition 9 25 36.0
subroutine 21 25 84.0
pod 5 5 100.0
total 186 248 75.0


line stmt bran cond sub pod time code
1             package App::GitHooks::Test;
2              
3 4     4   2056 use strict;
  4         6  
  4         126  
4 4     4   13 use warnings;
  4         5  
  4         125  
5              
6             # Parent class.
7 4     4   19 use base 'Exporter';
  4         6  
  4         303  
8              
9             # External dependencies.
10 4     4   2108 use Capture::Tiny;
  4         121323  
  4         232  
11 4     4   33 use Carp;
  4         7  
  4         200  
12 4     4   25 use Cwd;
  4         4  
  4         230  
13 4     4   1853 use Data::Section -setup;
  4         89957  
  4         34  
14 4     4   4946 use Data::Validate::Type;
  4         29140  
  4         281  
15 4     4   36 use File::Spec;
  4         7  
  4         81  
16 4     4   18 use File::Temp;
  4         8  
  4         360  
17 4     4   3567 use Path::Tiny qw();
  4         50404  
  4         123  
18 4     4   2151 use Test::Exception;
  4         51465  
  4         23  
19 4     4   3487 use Test::Git;
  4         67093  
  4         223  
20 4     4   2358 use Test::More;
  4         19710  
  4         33  
21              
22             # Internal dependencies.
23 4     4   1102 use App::GitHooks::Constants qw( :PLUGIN_RETURN_CODES );
  4         8  
  4         8212  
24              
25              
26             ## no critic (RegularExpressions::RequireExtendedFormatting)
27              
28             =head1 NAME
29              
30             App::GitHooks::Test - Shared test functions for App::GitHooks.
31              
32              
33             =head1 VERSION
34              
35             Version 1.7.3
36              
37             =cut
38              
39             our $VERSION = '1.7.3';
40              
41             our @EXPORT_OK = qw(
42             ok_add_file
43             ok_add_files
44             ok_setup_repository
45             );
46              
47              
48             =head1 FUNCTIONS
49              
50             =head2 ok_add_file()
51              
52             Create a file and add it to the git index.
53              
54             ok_add_file(
55             repository => $repository,
56             path => $path,
57             content => $content,
58             );
59              
60             Arguments:
61              
62             =over 4
63              
64             =item * repository I<(mandatory)>
65              
66             A C object.
67              
68             =item * path I<(mandatory)>
69              
70             The path of the file to write, relative to the root of the git repository
71             passed.
72              
73             =item * content I<(optional)>
74              
75             The content of the file to write.
76              
77             =back
78              
79             =cut
80              
81             sub ok_add_file
82             {
83 9     9 1 84 my ( %args ) = @_;
84 9         67 my $repository = delete( $args{'repository'} );
85 9         22 my $path = delete( $args{'path'} );
86 9         228 my $content = delete( $args{'content'} );
87 9 50       44 croak 'Invalid argument(s): ' . join( ', ', keys %args )
88             if scalar( keys %args ) != 0;
89              
90             return
91             subtest(
92             "Add file >$path<.",
93             sub
94             {
95 9     9   4718 plan( tests => 2 );
96              
97             lives_ok(
98             sub
99             {
100 9         256 Path::Tiny::path( $repository->work_tree(), $path )
101             ->spew( $content );
102             },
103 9         1828 'Write file.',
104             );
105              
106             lives_ok(
107             sub
108             {
109 9         221 $repository->run( 'add', $path );
110             },
111 9         6412 'Add the file to the git index.',
112             );
113             }
114 9         113 );
115             }
116              
117              
118             =head2 ok_add_files()
119              
120             Create files and add them to the git index.
121              
122             ok_add_files(
123             repository => $repository,
124             files =>
125             {
126             $file_name => $file_content,
127             ...
128             },
129             );
130              
131             Arguments:
132              
133             =over 4
134              
135             =item * repository I<(mandatory)>
136              
137             A C object.
138              
139             =item * files I<(optional)>
140              
141             A hashref with file names as keys and the content of each file as the
142             corresponding value.
143              
144             =back
145              
146             =cut
147              
148             sub ok_add_files
149             {
150 0     0 1 0 my ( %args ) = @_;
151 0         0 my $repository = delete( $args{'repository'} );
152 0   0     0 my $files = delete( $args{'files'} ) // {};
153 0 0       0 croak 'Unknown argument(s): ' . join( ', ', keys %args )
154             if scalar( keys %args ) != 0;
155              
156             return
157             subtest(
158             'Set up test files.',
159             sub
160             {
161 0     0   0 plan( tests => scalar( keys %$files ) );
162              
163 0         0 foreach my $filename ( keys %$files )
164             {
165 0         0 ok_add_file(
166             repository => $repository,
167             path => $filename,
168             content => $files->{ $filename },
169             );
170             }
171             }
172 0         0 );
173             }
174              
175              
176             =head2 ok_setup_repository()
177              
178             Set up a test repository.
179              
180             ok_setup_repository(
181             cleanup_test_repository => $cleanup_test_repository, # default 1
182             config => $config, # default ''
183             hooks => \@hooks, # default []
184             plugins => \@plugins, # default []
185             );
186              
187             Arguments:
188              
189             =over 4
190              
191             =item * cleanup_test_repository
192              
193             Whether the test repository created in order to run a test should be destroyed
194             at the end of the test (default: 1).
195              
196             =item * config
197              
198             Elements to add to the C<.githooksrc> file set up at the root of the test
199             repository.
200              
201             =item * hooks
202              
203             An arrayref of the names of the hooks to set up for this test (for example,
204             C).
205              
206             =item * plugins
207              
208             An arrayref of the module names of the plugins to run for this test (for
209             example, C).
210              
211             =back
212              
213             =cut
214              
215             sub ok_setup_repository
216             {
217 9     9 1 80 my ( %args ) = @_;
218 9   50     56 my $cleanup_test_repository = delete( $args{'cleanup_test_repository'} ) // 1;
219 9   50     109 my $config = delete( $args{'config'} ) // '';
220 9   50     35 my $hooks = delete( $args{'hooks'} ) // [];
221 9   50     38 my $plugins = delete( $args{'plugins'} ) // [];
222              
223             # Validate the parameters.
224 9 50       80 croak "The 'plugins' argument must be an arrayref"
225             if !Data::Validate::Type::is_arrayref( $plugins );
226 9 50       299 croak "The 'hooks' argument must be an arrayref"
227             if !Data::Validate::Type::is_arrayref( $hooks );
228 9 50       148 croak 'Unknown argument(s): ' . join( ', ', keys %args )
229             if scalar( keys %args ) != 0;
230              
231             # Check if we need to propagate test coverage checks to the hooks.
232 9 50 33     117 my $test_coverage = ( ( $ENV{'HARNESS_PERL_SWITCHES'} // '' ) =~ /Devel::Cover/ )
233             || ( ( $ENV{'PERL5OPT'} // '' ) =~ /Devel::Cover/ )
234             ? 1
235             : 0;
236              
237 9         12 my $repository;
238             subtest(
239             'Set up temporary test repository.',
240             sub
241             {
242 9     9   4740 plan( tests => 10 + scalar( @$hooks ) );
243              
244             # Create a temporary repository.
245 9         1981 ok(
246             defined(
247             $repository = test_repository(
248             temp => [ CLEANUP => $cleanup_test_repository ],
249             )
250             ),
251             'Create the test repository.',
252             );
253 9         565349 note( 'Using test repository ' . $repository->work_tree() );
254              
255             lives_ok(
256             sub
257             {
258 9         453 $repository->run( 'config', 'user.email', 'author1@example.com' );
259             },
260 9         1951 'Set the test author\'s email.',
261             );
262              
263             lives_ok(
264             sub
265             {
266 9         395 $repository->run( 'config', 'user.name', 'Test Author' );
267             },
268 9         74217 'Set the test author\'s name.',
269             );
270              
271             # Make sure we have a hook template available.
272 9 50       75450 my $hook_template_ref = __PACKAGE__->section_data(
273             $test_coverage
274             ? 'devel_cover'
275             : 'default'
276             );
277 9 50       5151 my $hook_template = defined( $hook_template_ref )
278             ? $$hook_template_ref
279             : undef;
280 9 50 33     180 ok(
281             defined( $hook_template )
282             && ( $hook_template =~ /\w/ ),
283             'The hook template exists.',
284             ) || diag( explain( $hook_template_ref ) );
285              
286             # The hooks are perl processes instantiated by git, so they will not have the
287             # same @INC necessarily, which is a problem for testing in particular when
288             # specific libs are included on the command line of the test. To that effect,
289             # we hardcode the current @INC into the hook startup files.
290 108         1090 my $libs = join(
291             ' ',
292 9         3385 map { File::Spec->rel2abs( $_ ) } @INC
293             );
294 9         123 $hook_template =~ s/\Q{libs}\E/$libs/g;
295              
296             # Template replacements.
297 9         95 $hook_template =~ s/\Q{interpreter_path}\E/$^X/g;
298              
299             # Specific actions for when test coverage is enabled.
300 9         17 my $cover_db_path;
301 9 50       37 SKIP:
302             {
303 9         18 skip(
304             'Test coverage not enabled.',
305             5,
306             ) if !$test_coverage;
307              
308             # Find out in which directory the coverage database should be stored.
309 9 50 50     158 SKIP:
310             {
311 9         20 skip(
312             'The COVER_DB_PATH environment variable is not set.',
313             1,
314             ) if ( $ENV{'COVER_DB_PATH'} // '' ) eq '';
315              
316 0         0 my $is_valid =
317             ok(
318             -e $ENV{'COVER_DB_PATH'},
319             'The coverage database directory specified in the COVER_DB_PATH environment variable is valid.',
320             );
321              
322 0 0       0 $cover_db_path = $ENV{'COVER_DB_PATH'}
323             if $is_valid;
324             };
325              
326             # Use File::Spec->catfile() for portability.
327 9   33     2515 ok(
328             defined(
329             $cover_db_path //= File::Spec->catfile( Cwd::getcwd(), 'cover_db' )
330             ),
331             'The coverage database directory is set.',
332             );
333 9         3185 note( "Using the coverage database directory >$cover_db_path<." );
334 9 50       1631 $hook_template =~ s/\Q{cover_db_path}\E/$cover_db_path/g
335             if defined( $cover_db_path );
336              
337             # Note: this is required because Devel::Coverage only analyzes the coverage
338             # for files in -dir, which defaults to the current directory. It can be changed
339             # to '/home' or '/' to make it cover both the test repository and the main lib/
340             # directory in which the code for the hooks lives, but this wouldn't be portable.
341             # Instead, we symlink the lib directory into the test repository, and the
342             # coverage-specific version of the test githook template will use that symlink as
343             # the source for the App::GitHooks modules. As long as the target system supports
344             # symlinks, it then allows for coverage testing.
345             # Note: lib/ is necessary for testing coverage via 'prove', but
346             # blib/lib/ is necessary for testing coverage via 'cover'.
347 9         123 ok(
348             symlink( Cwd::getcwd() . '/lib', $repository->work_tree() . '/lib' ),
349             'Symlink lib/ into the test repository to allow proper merging of coverage databases (with "prove").',
350             );
351 9         3818 ok(
352             mkdir( $repository->work_tree() . '/blib' ),
353             'Create a blib/ directory in the test repository.',
354             );
355 9         3765 ok(
356             symlink( Cwd::getcwd() . '/lib', $repository->work_tree() . '/blib/lib' ),
357             'Symlink blib/lib/ into the test repository to allow proper merging of coverage databases (with "cover").',
358             );
359             };
360              
361             # Set up the hooks.
362 9         3405 foreach my $hook_name ( @$hooks )
363             {
364             subtest(
365             "Set up the $hook_name hook.",
366             sub
367             {
368 9         6034 plan( tests => 2 );
369              
370 9         1909 my $hook_path = $repository->work_tree() . '/.git/hooks/' . $hook_name;
371             lives_ok(
372             sub
373             {
374 9         372 Path::Tiny::path( $hook_path )
375             ->spew( $hook_template );
376             },
377 9         167 'Write the hook.',
378             );
379              
380 9         7763 ok(
381             chmod( 0755, $hook_path ),
382             "Make the $hook_name hook executable.",
383             );
384             }
385 9         148 );
386             }
387              
388             # Set up a .githooksrc config.
389             lives_ok(
390             sub
391             {
392 9         190 my $content = "";
393              
394             # Main section.
395             {
396             # Only run specific plugins.
397 9 50       12 $content .= "force_plugins = " . join( ', ', @$plugins ) . "\n"
  9         65  
398             if defined( $plugins );
399             }
400              
401             # Testing section.
402             {
403 9         14 $content .= "[testing]\n";
  9         19  
404              
405             # Pretend we're in an interactive terminal even if we're doing automated testing.
406 9         18 $content .= "force_interactive = 1\n";
407              
408             # Disable color, to make it easier to match output.
409 9         15 $content .= "force_use_colors = 0\n";
410              
411             # Disable utf-8 characters, to make it easier to match output.
412 9         19 $content .= "force_is_utf8 = 0\n";
413              
414             # Just have commit-msg exit with the result of the checks, instead
415             # of forcing to correct the issue.
416 9         16 $content .= "commit_msg_no_edit = 1\n";
417             }
418              
419             # Add any custom config passed.
420 9         20 $content .= $config;
421              
422             # Write the file.
423 9         36 Path::Tiny::path( $repository->work_tree(), '.githooksrc' )
424             ->spew( $content );
425             },
426 9         7893 'Write a .githooksrc config file.',
427             );
428             }
429 9         140 );
430              
431 9         791428 return $repository;
432             }
433              
434              
435             =head2 ok_reset_githooksrc()
436              
437             Ensures that an empty C<.githooksrc> is used.
438              
439             ok_reset_githooksrc();
440              
441             Arguments:
442              
443             =over 4
444              
445             =item * content I<(optional)>
446              
447             Content for the C<.githooksrc> file.
448              
449             By default, this function generates an empty C<.githooksrc> file, which has the
450             effect of using the defaults of L.
451              
452             =back
453              
454             =cut
455              
456             sub ok_reset_githooksrc
457             {
458 0     0 1 0 my ( %args ) = @_;
459 0   0     0 my $content = delete( $args{'content'} ) // '';
460              
461 0 0       0 croak 'Invalid argument(s): ' . join( ', ', keys %args )
462             if scalar( keys %args ) != 0;
463              
464             subtest(
465             'Set up .githooksrc file.',
466             sub
467             {
468 0     0   0 plan( tests => 4 );
469              
470 0         0 ok(
471             my ( $file_handle, $filename ) = File::Temp::tempfile(),
472             'Create a temporary file to store the githooks config.',
473             );
474              
475 0         0 ok(
476             ( print $file_handle $content ),
477             'Write the githooks config.',
478             );
479              
480 0         0 ok(
481             close( $file_handle ),
482             'Close githooks config.',
483             );
484              
485 0         0 note( "GITHOOKSRC_FORCE will be set to $filename." );
486              
487             # Note: we need to make a global change to %ENV here, so that it
488             # propagates to the caller's scope.
489 0         0 ok(
490             $ENV{'GITHOOKSRC_FORCE'} = $filename, ## no critic (Variables::RequireLocalizedPunctuationVars)
491             'Set the environment variable GITHOOKSRC_FORCE to point to the new config.',
492             );
493             }
494 0         0 );
495              
496 0         0 return;
497             }
498              
499              
500             =head2 test_hook()
501              
502             Test a git hook.
503              
504             App::GitHooks::Test::test_hook(
505             tests => \@tests,
506             hook_name => $hook_name,
507             plugins => \@plugins,
508             cleanup_test_repository => $cleanup_test_repository, # default 1
509             );
510              
511             Mandatory arguments:
512              
513             =over 4
514              
515             =item * tests
516              
517             A set of tests to run.
518              
519             # TODO: document tests format.
520              
521             =item * hook_name
522              
523             The name of the git hook to test (for example, C).
524              
525             =item * plugins
526              
527             An arrayref of the module names of the plugins to run for this test (for
528             example, C).
529              
530             =back
531              
532             Optional arguments:
533              
534             =over 4
535              
536             =item * cleanup_test_repository
537              
538             Whether the test repository created in order to run a test should be destroyed
539             at the end of the test (default: 1).
540              
541             =back
542              
543             =cut
544              
545             sub test_hook
546             {
547 3     3 1 137 my ( %args ) = @_;
548 3         10 my $tests = delete( $args{'tests'} );
549 3         9 my $hook_name = delete( $args{'hook_name'} );
550 3         10 my $plugins = delete( $args{'plugins'} );
551 3   50     15 my $cleanup_test_repository = delete( $args{'cleanup_test_repository'} ) // 1;
552 3 50       17 croak "Invalid arguments passed: " . join( ', ', keys %args )
553             if scalar( keys %args ) != 0;
554 3 50       21 croak "A hook name must be specified"
555             if !defined( $hook_name );
556 3 50       37 croak "The hook name is not valid"
557             if $hook_name !~ /^[\w-]+$/;
558              
559             # Bail out if Git isn't available.
560 3         20 has_git( '1.7.4.1' );
561 3         59835 plan( tests => scalar( @$tests ) );
562              
563 3         1290 foreach my $test ( @$tests )
564             {
565             subtest(
566             $test->{'name'},
567             sub
568             {
569 9     9   6148 plan( tests => 5 );
570              
571 9         1822 my $repository = ok_setup_repository(
572             cleanup_test_repository => $cleanup_test_repository,
573             config => $test->{'config'},
574             hooks => [ $hook_name ],
575             plugins => $plugins,
576             );
577              
578             # Set up a test file.
579 9         85 ok_add_file(
580             repository => $repository,
581             path => 'test.pl',
582             content => "#!perl\n\nuse strict;\nbareword;\n",
583             );
584              
585             # Try to commit.
586 9         99040 my $stderr;
587             my $exit_status;
588             lives_ok(
589             sub
590             {
591             $stderr = Capture::Tiny::capture_stderr(
592             sub
593             {
594 9         7619 $repository->run( 'commit', '-m', 'Test message.' );
595 9         6264614 $exit_status = $? >> 8;
596             }
597 9         826 );
598 9         95665 note( $stderr );
599             },
600 9         132 'Commit the changes.',
601             );
602              
603 9         11179 like(
604             $stderr,
605             $test->{'expected'},
606             "The output matches expected results.",
607             );
608              
609 9         9881 is(
610             $exit_status,
611             $test->{'exit_status'},
612             'The exit status is correct.',
613             );
614             }
615 9         12635 );
616             }
617              
618 3         7789 return;
619             }
620              
621              
622             =head1 BUGS
623              
624             Please report any bugs or feature requests through the web interface at
625             L.
626             I will be notified, and then you'll automatically be notified of progress on
627             your bug as I make changes.
628              
629              
630             =head1 SUPPORT
631              
632             You can find documentation for this module with the perldoc command.
633              
634             perldoc App::GitHooks::Test
635              
636              
637             You can also look for information at:
638              
639             =over
640              
641             =item * GitHub's request tracker
642              
643             L
644              
645             =item * AnnoCPAN: Annotated CPAN documentation
646              
647             L
648              
649             =item * CPAN Ratings
650              
651             L
652              
653             =item * MetaCPAN
654              
655             L
656              
657             =back
658              
659              
660             =head1 AUTHOR
661              
662             L,
663             C<< >>.
664              
665              
666             =head1 COPYRIGHT & LICENSE
667              
668             Copyright 2013-2015 Guillaume Aubert.
669              
670             This program is free software: you can redistribute it and/or modify it under
671             the terms of the GNU General Public License version 3 as published by the Free
672             Software Foundation.
673              
674             This program is distributed in the hope that it will be useful, but WITHOUT ANY
675             WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
676             PARTICULAR PURPOSE. See the GNU General Public License for more details.
677              
678             You should have received a copy of the GNU General Public License along with
679             this program. If not, see http://www.gnu.org/licenses/
680              
681             =cut
682              
683             1;
684              
685              
686             __DATA__