]> sipb.mit.edu Git - ikiwiki.git/blob - t/cvs.t
Test checkconfig().
[ikiwiki.git] / t / cvs.t
1 #!/usr/bin/perl
2 use warnings;
3 use strict;
4 use Test::More; my $total_tests = 53;
5 use IkiWiki;
6
7 my $default_test_methods = '^test_*';
8 my @required_programs = qw(
9         cvs
10         cvsps
11 );
12 my @required_modules = qw(
13         File::chdir
14         File::MimeInfo
15         Date::Parse
16         File::Temp
17         File::ReadBackwards
18 );
19 my $dir = "/tmp/ikiwiki-test-cvs.$$";
20
21 # TESTS FOR GENERAL META-BEHAVIOR
22
23 sub test_web_comments {
24         # how much of the web-edit workflow are we actually testing?
25         # because we want to test comments:
26         # - when the first comment for page.mdwn is added, and page/ is
27         #   created to hold the comment, page/ isn't added to CVS control,
28         #   so the comment isn't either
29         #   - can't reproduce after chmod g+s ikiwiki.cgi (20120204)
30         # - side effect for moderated comments: after approval they
31         #   show up normally AND are still pending, too
32         # - comments.pm treats rcs_commit_staged() as returning conflicts?
33 }
34
35 sub test_chdir_magic {
36         # cvs.pm operations are always occurring inside $config{srcdir}
37         # other ikiwiki operations are occurring wherever, and are unaffected
38         # when are we bothering with "local $CWD" and when aren't we?
39         # after commit, presumably only with post-commit hook enabled:
40         #> Use of chdir('') or chdir(undef) as chdir() is deprecated at
41         #> /usr/pkg/lib/perl5/vendor_perl/5.14.0/File/chdir.pm line 45.
42 }
43
44 sub test_cvs_info {
45         # inspect "Repository revision" (used in code)
46         # inspect "Sticky Options" (used in tests to verify existence of "-kb")
47 }
48
49 sub test_cvs_run_cvs {
50         # extract the stdout-redirect thing
51         # - prove that it silences stdout
52         # - prove that stderr comes through just fine
53         # prove that when cvs exits nonzero (fail), function exits false
54         # prove that when cvs exits zero (success), function exits true
55         # always pass -f, just in case
56         # steal from git.pm: safe_git(), run_or_{die,cry,non}
57         # - open() instead of system()
58         # always call cvs_run_cvs(), don't ever run 'cvs' directly
59 }
60
61 sub test_cvs_run_cvsps {
62         # parameterize command like run_cvs()
63         # expose config vars for e.g. "--cvs-direct -z 30"
64         # always pass -x (unless proven otherwise)
65         # - but diff doesn't! optimization alert
66         # always pass -b HEAD (configurable like gitmaster_branch?)
67 }
68
69 sub test_cvs_parse_cvsps {
70         # extract method from rcs_recentchanges
71         # document expected changeset format
72         # document expected changeset delimiter
73         # try: cvsps -q -x -p && ls | sort -rn | head -100
74         # - benchmark against current impl (that uses File::ReadBackwards)
75 }
76
77 sub test_cvs_parse_log_accum {
78         # add new, preferred method for rcs_recentchanges to use
79         # teach log_accum to record commits (into transient?)
80         # script cvsps to bootstrap (or replace?) commit history
81         # teach ikiwiki-makerepo to set up log_accum and commit_prep
82         # why are NetBSD commit mails unreliable?
83         # - is it working for CVS commits and failing for web commits?
84 }
85
86 sub test_cvs_is_controlling {
87         # with no args:
88         # - if srcdir is in CVS, return true
89         # - else, return false
90         # with a dir arg:
91         # - if dir is in CVS, return true
92         # - else, return false
93         # with a file arg:
94         # - is there anything that wants the answer? if so, answer
95         # - else, die
96 }
97
98
99 # TESTS FOR GENERAL PLUGIN API CALLS
100
101 sub test_checkconfig {
102         my $default_cvspath = 'ikiwiki';
103
104         undef $config{cvspath}; IkiWiki::checkconfig();
105         is(
106                 $config{cvspath}, $default_cvspath,
107                 q{can provide default cvspath},
108         );
109
110         $config{cvspath} = "/$default_cvspath/"; IkiWiki::checkconfig();
111         is(
112                 $config{cvspath}, $default_cvspath,
113                 q{can set typical cvspath and strip well-meaning slashes},
114         );
115
116         $config{cvspath} = "/$default_cvspath//subdir"; IkiWiki::checkconfig();
117         is(
118                 $config{cvspath}, "$default_cvspath/subdir",
119                 q{can really sanitize cvspath as assumed by rcs_recentchanges},
120         );
121
122         my $default_num_wrappers = @{$config{wrappers}};
123         undef $config{cvs_wrapper}; IkiWiki::checkconfig();
124         is(
125                 @{$config{wrappers}}, $default_num_wrappers,
126                 q{can start with no wrappers configured},
127         );
128
129         $config{cvs_wrapper} = $config{cvsrepo} . "/CVSROOT/post-commit";
130         IkiWiki::checkconfig();
131         is(
132                 @{$config{wrappers}}, ++$default_num_wrappers,
133                 q{can add cvs_wrapper},
134         );
135 }
136
137 sub test_getsetup {
138         # anything worth testing?
139 }
140
141 sub test_genwrapper {
142         # testable directly? affects rcs_add, but are we exercising this?
143 }
144
145
146 # TESTS FOR VCS PLUGIN API CALLS
147
148 sub test_rcs_update {
149         # can it assume we're under CVS control? or must it check?
150         # anything else worth testing?
151 }
152
153 sub test_rcs_prepedit {
154         # can it assume we're under CVS control? or must it check?
155         # for existing file, returns latest revision in repo
156         # - what's this used for? should it return latest revision in checkout?
157         # for new file, returns empty string
158
159         # netbsd web log says "could not open lock file"
160         # XXX does this work right?
161         # how about with un-added dirs in the srcdir?
162         # how about with cvsps.core lying around?
163 }
164
165 sub test_rcs_commit {
166         # can it assume we're under CVS control? or must it check?
167         # if someone else changed the page since rcs_prepedit was called:
168         # - try to merge into our working copy
169         # - if merge succeeds, proceed to commit
170         # - else, return page content with the conflict markers in it
171         # commit:
172         # - if success, return undef
173         # - else, revert + return content with the conflict markers in it
174         # git.pm receives "session" param -- useful here?
175         # web commits start with "web commit {by,from} "
176         # seeing File::chdir errors on commit?
177
178         # XXX commit can fail due to "could not open lock file"
179 }
180
181 sub test_rcs_commit_staged {
182         # if commit succeeds, return undef
183         # else, warn and return error message (really? or just non-undef?)
184 }
185
186 sub test_rcs_add {
187         my @changes = IkiWiki::rcs_recentchanges(3);
188         is_total_number_of_changes(\@changes, 0);
189
190         my $message = "add a top-level ASCII (non-UTF-8) page via VCS API";
191         my $file = q{test0.mdwn};
192         add_and_commit($file, $message, qq{# \$Id\$\n* some plain ASCII text});
193         is_newly_added($file);
194         is_in_keyword_substitution_mode($file, undef);
195         like(
196                 readfile($config{srcdir} . "/$file"),
197                 qr/^# \$Id: $file,v 1.1 .+\$$/m,
198                 q{can expand RCS Id keyword},
199         );
200         @changes = IkiWiki::rcs_recentchanges(3);
201         is_total_number_of_changes(\@changes, 1);
202         is_most_recent_change(\@changes, stripext($file), $message);
203
204         $message = "add a top-level dir via VCS API";
205         my $dir1 = q{test3};
206         can_mkdir($dir1);
207         IkiWiki::rcs_add($dir1);
208         # XXX test that the wrapper hangs here without our genwrapper()
209         # XXX test that the wrapper doesn't hang here with it
210         @changes = IkiWiki::rcs_recentchanges(3);
211         is_total_number_of_changes(\@changes, 1);       # despite the dir add
212         IkiWiki::rcs_commit(
213                 file => $dir1,
214                 message => $message,
215                 token => "oom",
216         );
217         @changes = IkiWiki::rcs_recentchanges(3);
218         is_total_number_of_changes(\@changes, 1);       # dirs aren't tracked
219
220         $message = "add a non-ASCII (UTF-8) text file in an un-added dir";
221         can_mkdir($_) for (qw(test4 test4/test5));
222         $file = q{test4/test5/test1.mdwn};
223         add_and_commit($file, $message, readfile("t/test1.mdwn"));
224         is_newly_added($file);
225         is_in_keyword_substitution_mode($file, undef);
226         @changes = IkiWiki::rcs_recentchanges(3);
227         is_total_number_of_changes(\@changes, 2);
228         is_most_recent_change(\@changes, stripext($file), $message);
229
230         $message = "add a binary file in an un-added dir, and commit_staged";
231         can_mkdir(q{test6});
232         $file = q{test6/test7.ico};
233         my $bindata_in = readfile("doc/favicon.ico", 1);
234         my $bindata_out = sub { readfile($config{srcdir} . "/$file", 1) };
235         writefile($file, $config{srcdir}, $bindata_in, 1);
236         is(&$bindata_out(), $bindata_in, q{binary files match before commit});
237         IkiWiki::rcs_add($file);
238         IkiWiki::rcs_commit_staged(message => $message);
239         is_newly_added($file);
240         is_in_keyword_substitution_mode($file, q{-kb});
241         is(&$bindata_out(), $bindata_in, q{binary files match after commit});
242         @changes = IkiWiki::rcs_recentchanges(3);
243         is_total_number_of_changes(\@changes, 3);
244         is_most_recent_change(\@changes, $file, $message);
245         ok(
246                 unlink($config{srcdir} . "/$file"),
247                 q{can remove file in order to re-fetch it from repo},
248         );
249         ok(! -e $config{srcdir} . "/$file", q{really removed file});
250         IkiWiki::rcs_update();
251         is(&$bindata_out(), $bindata_in, q{binary files match after re-fetch});
252
253         $message = "add a UTF-8 and a binary file in different dirs";
254         my $file1 = "test8/test9.mdwn";
255         my $file2 = "test10/test11.ico";
256         can_mkdir($_) for (qw(test8 test10));
257         writefile($file1, $config{srcdir}, readfile('t/test2.mdwn'));
258         writefile($file2, $config{srcdir}, $bindata_in, 1);
259         IkiWiki::rcs_add($_) for ($file1, $file2);
260         IkiWiki::rcs_commit_staged(message => $message);
261         is_newly_added($_) for ($file1, $file2);
262         is_in_keyword_substitution_mode($file1, undef);
263         is_in_keyword_substitution_mode($file2, '-kb');
264         @changes = IkiWiki::rcs_recentchanges(3);
265         is_total_number_of_changes(\@changes, 3);
266         @changes = IkiWiki::rcs_recentchanges(4);
267         is_total_number_of_changes(\@changes, 4);
268         # XXX test for both files in the commit, and no other files
269         is_most_recent_change(\@changes, $file2, $message);
270
271         # prevent web edits from attempting to create .../CVS/foo.mdwn
272         # on case-insensitive filesystems, also prevent .../cvs/foo.mdwn
273         # unless your "CVS" is something else and we've made it configurable
274
275         # can it assume we're under CVS control? or must it check?
276
277         # extract method: filetype-guessing
278         # add a binary file, remove it, add a text file by same name, no -kb?
279         # add a text file, remove it, add a binary file by same name, -kb?
280 }
281
282 sub test_rcs_remove {
283         # can it assume we're under CVS control? or must it check?
284         # remove a top-level file
285         # - rcs_commit
286         # - inspect recentchanges: one new change, file removed
287         # remove two files (in different dirs)
288         # - rcs_commit_staged
289         # - inspect recentchanges: one new change, both files removed
290 }
291
292 sub test_rcs_rename {
293         # can it assume we're under CVS control? or must it check?
294         # rename a file in the same dir
295         # - rcs_commit_staged
296         # - inspect recentchanges: one new change, one file removed, one added
297         # rename a file into a different dir
298         # - rcs_commit_staged
299         # - inspect recentchanges: one new change, one file removed, one added
300         # rename a file into a not-yet-existing dir
301         # - rcs_commit_staged
302         # - inspect recentchanges: one new change, one file removed, one added
303         # is it safe to use "mv"? what if $dest is somehow outside the wiki?
304 }
305
306 sub test_rcs_recentchanges {
307         my @changes = IkiWiki::rcs_recentchanges(3);
308         is_total_number_of_changes(\@changes, 0);
309
310         my $message = "Add a page via CVS directly";
311         my $file = q{test2.mdwn};
312         writefile($file, $config{srcdir}, readfile(q{t/test2.mdwn}));
313         system "cd $config{srcdir}"
314                 . " && cvs add $file >/dev/null 2>&1";
315         system "cd $config{srcdir}"
316                 . " && cvs commit -m \"$message\" $file >/dev/null";
317
318         @changes = IkiWiki::rcs_recentchanges(3);
319         is_total_number_of_changes(\@changes, 1);
320         is_most_recent_change(\@changes, stripext($file), $message);
321
322         # CVS commits run ikiwiki once for every committed file (!)
323         # - commit_prep alone should fix this
324         # CVS multi-dir commits show only the first dir in recentchanges
325         # - commit_prep might also fix this?
326         # CVS post-commit hook is amped off to avoid locking against itself
327         # - commit_prep probably doesn't fix this... but maybe?
328         # can it assume we're under CVS control? or must it check?
329         # don't worry whether we're called with a number (we always are)
330         # other rcs tests already inspect much of the returned structure
331         # CVS commits say "cvs" and get the right committer
332         # web commits say "web" and get the right committer
333         # - and don't start with "web commit {by,from} "
334         # "nickname" -- can we ever meaningfully set this?
335
336         # prefer log_accum, then cvsps, else die
337         # run the high-level recentchanges tests 2x (once for each method)
338         # - including in other test subs that check recentchanges?
339 }
340
341 sub test_rcs_diff {
342         my @changes = IkiWiki::rcs_recentchanges(3);
343         is_total_number_of_changes(\@changes, 0);
344
345         my $message = "add a UTF-8 and an ASCII file in different dirs";
346         my $file1 = "rcsdiff1/utf8.mdwn";
347         my $file2 = "rcsdiff2/ascii.mdwn";
348         my $contents2 = ''; $contents2 .= "$_. foo\n" for (1..11);
349         can_mkdir($_) for (qw(rcsdiff1 rcsdiff2));
350         writefile($file1, $config{srcdir}, readfile('t/test2.mdwn'));
351         writefile($file2, $config{srcdir}, $contents2);
352         IkiWiki::rcs_add($_) for ($file1, $file2);
353         IkiWiki::rcs_commit_staged(message => $message);
354
355         # XXX we rely on rcs_recentchanges() to be called first!
356         # XXX or else for no cvsps cache to exist yet...
357         # XXX because rcs_diff() doesn't pass -x (as an optimization)
358         @changes = IkiWiki::rcs_recentchanges(3);
359         is_total_number_of_changes(\@changes, 1);
360
361         my $changeset = 1;
362
363         my $maxlines = undef;
364         my $scalar_diffs = IkiWiki::rcs_diff($changeset, $maxlines);
365         like(
366                 $scalar_diffs,
367                 qr/^\+11\. foo$/m,
368                 q{unbounded scalar diffs go all the way to 11},
369         );
370         my @array_diffs = IkiWiki::rcs_diff($changeset, $maxlines);
371         is(
372                 $array_diffs[$#array_diffs],
373                 "+11. foo\n",
374                 q{unbounded array diffs go all the way to 11},
375         );
376
377         $maxlines = 8;
378         $scalar_diffs = IkiWiki::rcs_diff($changeset, $maxlines);
379         unlike(
380                 $scalar_diffs,
381                 qr/^\+11\. foo$/m,
382                 q{bounded scalar diffs don't go all the way to 11},
383         );
384         @array_diffs = IkiWiki::rcs_diff($changeset, $maxlines);
385         isnt(
386                 $array_diffs[$#array_diffs],
387                 "+11. foo\n",
388                 q{bounded array diffs don't go all the way to 11},
389         );
390         is(
391                 scalar @array_diffs,
392                 $maxlines,
393                 q{bounded array diffs contain expected maximum number of lines},
394         );
395
396         # can it assume we're under CVS control? or must it check?
397 }
398
399 sub test_rcs_getctime {
400         # can it assume we're under CVS control? or must it check?
401         # given a file, find its creation time, else return 0
402         # first implement in the obvious way
403         # then cache
404 }
405
406 sub test_rcs_getmtime {
407         # can it assume we're under CVS control? or must it check?
408         # given a file, find its modification time, else return 0
409         # first implement in the obvious way
410         # then cache
411 }
412
413 sub test_rcs_receive {
414         my $description = q{rcs_receive doesn't make sense for CVS};
415         exists $IkiWiki::hooks{rcs}{rcs_receive}
416                 ? fail($description)
417                 : pass($description);
418 }
419
420 sub test_rcs_preprevert {
421         # can it assume we're under CVS control? or must it check?
422         # given a patchset number, return structure describing what'd happen:
423         # - see doc/plugins/write.mdwn:rcs_receive()
424         # don't forget about attachments
425 }
426
427 sub test_rcs_revert {
428         # test rcs_recentchanges() real darn well
429         # extract read-backwards patchset parser from rcs_recentchanges()
430         # recentchanges: given max, return list of changeset/files/etc.
431         # revert: given changeset ID, return list of file/rev/action
432         #
433         # can it assume we're under CVS control? or must it check?
434         # given a patchset number, stage the revert for rcs_commit_staged()
435         # if commit succeeds, return undef
436         # else, warn and return error message (really? or just non-undef?)
437 }
438
439 sub main {
440         my $test_methods = defined $ENV{TEST_METHOD} 
441                          ? $ENV{TEST_METHOD}
442                          : $default_test_methods;
443
444         _startup($test_methods eq $default_test_methods);
445         _runtests(_get_matching_test_subs($test_methods));
446         _shutdown($test_methods eq $default_test_methods);
447 }
448
449 main();
450
451
452 # INTERNAL SUPPORT ROUTINES
453
454 sub _plan_for_test_more {
455         my $can_plan = shift;
456
457         foreach my $program (@required_programs) {
458                 my $program_path = `which $program`;
459                 chomp $program_path;
460                 return plan(skip_all => "$program not available")
461                         unless -x $program_path;
462         }
463
464         foreach my $module (@required_modules) {
465                 eval qq{use $module};
466                 return plan(skip_all => "$module not available")
467                         if $@;
468         }
469
470         return plan(skip_all => "can't create $dir: $!")
471                 unless mkdir($dir);
472         return plan(skip_all => "can't remove $dir: $!")
473                 unless rmdir($dir);
474
475         return unless $can_plan;
476
477         return plan(tests => $total_tests);
478 }
479
480 # http://stackoverflow.com/questions/607282/whats-the-best-way-to-discover-all-subroutines-a-perl-module-has
481
482 use B qw/svref_2object/;
483
484 sub in_package {
485         my ($coderef, $package) = @_;
486         my $cv = svref_2object($coderef);
487         return if not $cv->isa('B::CV') or $cv->GV->isa('B::SPECIAL');
488         return $cv->GV->STASH->NAME eq $package;
489 }
490
491 sub list_module {
492         my $module = shift;
493         no strict 'refs';
494         return grep {
495                 defined &{"$module\::$_"} and in_package(\&{*$_}, $module)
496         } keys %{"$module\::"};
497 }
498
499
500 # support for xUnit-style testing, a la Test::Class
501
502 sub _startup {
503         my $can_plan = shift;
504         _plan_for_test_more($can_plan);
505         _generate_test_config();
506 }
507
508 sub _shutdown {
509         my $had_plan = shift;
510         done_testing() unless $had_plan;
511 }
512
513 sub _setup {
514         _generate_test_repo();
515 }
516
517 sub _teardown {
518         # XXX does srcdir persist between test subs?
519         system "rm -rf $dir";
520 }
521
522 sub _runtests {
523         my @coderefs = (@_);
524         for (@coderefs) {
525                 _setup();
526                 $_->();
527                 _teardown();
528         }
529 }
530
531 sub _get_matching_test_subs {
532         my $re = shift;
533         no strict 'refs';
534         return map { \&{*$_} } grep { /$re/ } sort(list_module('main'));
535 }
536
537 sub _generate_test_config {
538         %config = IkiWiki::defaultconfig();
539         $config{rcs} = "cvs";
540         $config{srcdir} = "$dir/src";
541         $config{cvsrepo} = "$dir/repo";
542         $config{cvspath} = "ikiwiki";
543         IkiWiki::loadplugins();
544         IkiWiki::checkconfig();
545 }
546
547 sub _generate_test_repo {
548         die "can't create $dir: $!"
549                 unless mkdir($dir);
550
551         my $cvs = "cvs -d $config{cvsrepo}";
552         my $dn = ">/dev/null";
553         system "$cvs init $dn";
554         system "mkdir $dir/$config{cvspath} $dn";
555         system "cd $dir/$config{cvspath} && "
556                 . "$cvs import -m import $config{cvspath} VENDOR RELEASE $dn";
557         system "rm -rf $dir/$config{cvspath} $dn";
558         system "$cvs co -d $config{srcdir} $config{cvspath} $dn";
559 }
560
561 sub add_and_commit {
562         my ($file, $message, $contents) = @_;
563         writefile($file, $config{srcdir}, $contents);
564         IkiWiki::rcs_add($file);
565         IkiWiki::rcs_commit(
566                 file => $file,
567                 message => $message,
568                 token => "moo",
569         );
570 }
571
572 sub can_mkdir {
573         my $dir = shift;
574         ok(
575                 mkdir($config{srcdir} . "/$dir"),
576                 qq{can mkdir $dir},
577         );
578 }
579
580 sub is_newly_added {
581         my $file = shift;
582         is(
583                 IkiWiki::Plugin::cvs::cvs_info("Repository revision", $file),
584                 '1.1',
585                 qq{$file is newly added to CVS},
586         );
587 }
588
589 sub is_in_keyword_substitution_mode {
590         my ($file, $mode) = @_;
591         $mode = '(none)' unless defined $mode;
592         is(
593                 IkiWiki::Plugin::cvs::cvs_info("Sticky Options", $file),
594                 $mode,
595                 qq{$file is in CVS with expected keyword substitution mode},
596         );
597 }
598
599 sub is_total_number_of_changes {
600         my ($changes, $expected_total) = @_;
601         is(
602                 $#{$changes},
603                 $expected_total - 1,
604                 qq{total commits == $expected_total},
605         );
606 }
607
608 sub is_most_recent_change {
609         my ($changes, $page, $message) = @_;
610         is(
611                 $changes->[0]{message}[0]{"line"},
612                 $message,
613                 q{most recent commit's first message line matches},
614         );
615         is(
616                 $changes->[0]{pages}[0]{"page"},
617                 $page,
618                 q{most recent commit's first pagename matches},
619         );
620 }
621
622 sub stripext {
623         my ($file, $extension) = @_;
624         $extension = '\..+?' unless defined $extension;
625         $file =~ s|$extension$||g;
626         return $file;
627 }