File Coverage

blib/lib/App/GitHooks/Test.pm
Criterion Covered Total %
statement 150 160 93.7
branch 16 36 44.4
condition 11 25 44.0
subroutine 24 26 92.3
pod 5 5 100.0
total 206 252 81.7


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