diff options
Diffstat (limited to 't')
57 files changed, 4141 insertions, 0 deletions
diff --git a/t/404.t b/t/404.t new file mode 100755 index 000000000..0bb3c6063 --- /dev/null +++ b/t/404.t @@ -0,0 +1,44 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 17; + +BEGIN { use_ok("IkiWiki::Plugin::404"); } + +sub cgi_page_from_404 { + return IkiWiki::Plugin::404::cgi_page_from_404(shift, shift, shift); +} + +$IkiWiki::config{htmlext} = 'html'; + +is(cgi_page_from_404('/', 'http://example.com', 1), 'index'); +is(cgi_page_from_404('/index.html', 'http://example.com', 0), 'index'); +is(cgi_page_from_404('/', 'http://example.com/', 1), 'index'); +is(cgi_page_from_404('/index.html', 'http://example.com/', 0), 'index'); + +is(cgi_page_from_404('/~user/foo/bar', 'http://example.com/~user', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar/index.html', 'http://example.com/~user', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar/', 'http://example.com/~user', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar.html', 'http://example.com/~user', 0), + 'foo/bar'); + +is(cgi_page_from_404('/~user/foo/bar', 'http://example.com/~user/', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar/index.html', 'http://example.com/~user/', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar/', 'http://example.com/~user/', 1), + 'foo/bar'); +is(cgi_page_from_404('/~user/foo/bar.html', 'http://example.com/~user/', 0), + 'foo/bar'); + +is(cgi_page_from_404('/~user/foo', 'https://example.com/~user', 1), + 'foo'); +is(cgi_page_from_404('/~user/foo/index.html', 'https://example.com/~user', 1), + 'foo'); +is(cgi_page_from_404('/~user/foo/', 'https://example.com/~user', 1), + 'foo'); +is(cgi_page_from_404('/~user/foo.html', 'https://example.com/~user', 0), + 'foo'); diff --git a/t/add_depends.t b/t/add_depends.t new file mode 100755 index 000000000..aa58fb0ff --- /dev/null +++ b/t/add_depends.t @@ -0,0 +1,70 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 40; + +BEGIN { use_ok("IkiWiki"); } +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::checkconfig(); + +$pagesources{"foo$_"}="foo$_.mdwn" for 0..9; + +# avoids adding an unparseable pagespec +ok(! add_depends("foo0", "foo and (bar")); +ok(! add_depends("foo0", "foo another")); + +# simple and not-so-simple dependencies split +ok(add_depends("foo0", "*")); +ok(add_depends("foo0", "bar")); +ok(add_depends("foo0", "BAZ")); +ok(exists $IkiWiki::depends_simple{foo0}{"bar"}); +ok(exists $IkiWiki::depends_simple{foo0}{"baz"}); # lowercase +ok(! exists $IkiWiki::depends_simple{foo0}{"*"}); +ok(! exists $IkiWiki::depends{foo0}{"bar"}); +ok(! exists $IkiWiki::depends{foo0}{"baz"}); + +# default dependencies are content dependencies +ok($IkiWiki::depends{foo0}{"*"} & $IkiWiki::DEPEND_CONTENT); +ok(! ($IkiWiki::depends{foo0}{"*"} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_LINKS))); +ok($IkiWiki::depends_simple{foo0}{"bar"} & $IkiWiki::DEPEND_CONTENT); +ok(! ($IkiWiki::depends_simple{foo0}{"bar"} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_LINKS))); + +# adding other dep types standalone +ok(add_depends("foo2", "*", deptype("presence"))); +ok(add_depends("foo2", "bar", deptype("links"))); +ok($IkiWiki::depends{foo2}{"*"} & $IkiWiki::DEPEND_PRESENCE); +ok(! ($IkiWiki::depends{foo2}{"*"} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))); +ok($IkiWiki::depends_simple{foo2}{"bar"} & $IkiWiki::DEPEND_LINKS); +ok(! ($IkiWiki::depends_simple{foo2}{"bar"} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_CONTENT))); + +# adding combined dep types +ok(add_depends("foo2", "baz", deptype("links", "presence"))); +ok($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_LINKS); +ok($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_PRESENCE); +ok(! ($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_CONTENT)); + +# adding dep types to existing dependencies should merge the flags +ok(add_depends("foo2", "baz")); +ok($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_LINKS); +ok($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_PRESENCE); +ok(($IkiWiki::depends_simple{foo2}{"baz"} & $IkiWiki::DEPEND_CONTENT)); +ok(add_depends("foo2", "bar", deptype("presence"))); # had only links before +ok($IkiWiki::depends_simple{foo2}{"bar"} & ($IkiWiki::DEPEND_LINKS | $IkiWiki::DEPEND_PRESENCE)); +ok(! ($IkiWiki::depends_simple{foo2}{"bar"} & $IkiWiki::DEPEND_CONTENT)); +ok(add_depends("foo0", "bar", deptype("links"))); # had only content before +ok($IkiWiki::depends{foo0}{"*"} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS)); +ok(! ($IkiWiki::depends{foo0}{"*"} & $IkiWiki::DEPEND_PRESENCE)); + +# content is the default if unknown types are entered +ok(add_depends("foo9", "*", deptype("monkey"))); +ok($IkiWiki::depends{foo9}{"*"} & $IkiWiki::DEPEND_CONTENT); +ok(! ($IkiWiki::depends{foo9}{"*"} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_LINKS))); + +# Influences are added for dependencies involving links. +$pagesources{"foo"}="foo.mdwn"; +$links{foo}=[qw{bar}]; +$pagesources{"bar"}="bar.mdwn"; +$links{bar}=[qw{}]; +ok(add_depends("foo", "link(bar) and backlink(meep)")); +ok($IkiWiki::depends_simple{foo}{foo} == $IkiWiki::DEPEND_LINKS); diff --git a/t/autoindex.t b/t/autoindex.t new file mode 100755 index 000000000..d16e44ca8 --- /dev/null +++ b/t/autoindex.t @@ -0,0 +1,134 @@ +#!/usr/bin/perl +package IkiWiki; + +use warnings; +use strict; +use Test::More tests => 38; + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } +BEGIN { use_ok("IkiWiki::Plugin::aggregate"); } +BEGIN { use_ok("IkiWiki::Plugin::autoindex"); } +BEGIN { use_ok("IkiWiki::Plugin::html"); } +BEGIN { use_ok("IkiWiki::Plugin::mdwn"); } + +ok(! system("rm -rf t/tmp; mkdir t/tmp")); + +$config{verbose} = 1; +$config{srcdir} = 't/tmp'; +$config{underlaydir} = 't/tmp'; +$config{underlaydirbase} = '.'; +$config{templatedir} = 'templates'; +$config{usedirs} = 1; +$config{htmlext} = 'html'; +$config{wiki_file_chars} = "-[:alnum:]+/.:_"; +$config{userdir} = "users"; +$config{tagbase} = "tags"; +$config{default_pageext} = "mdwn"; +$config{wiki_file_prune_regexps} = [qr/^\./]; +$config{autoindex_commit} = 0; + +is(checkconfig(), 1); + +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +# Pages that (we claim) were deleted in an earlier pass. We're using deleted, +# not autofile, to test backwards compat. +$wikistate{autoindex}{deleted}{deleted} = 1; +$wikistate{autoindex}{deleted}{expunged} = 1; +$wikistate{autoindex}{deleted}{reinstated} = 1; + +foreach my $page (qw(tags/numbers deleted/bar reinstated reinstated/foo gone/bar)) { + # we use a non-default extension for these, so they're distinguishable + # from programmatically-created pages + $pagesources{$page} = "$page.html"; + $pagemtime{$page} = $pagectime{$page} = 1000000; + writefile("$page.html", "t/tmp", "your ad here"); +} + +# a directory containing only an internal page shouldn't be indexed +$pagesources{"has_internal/internal"} = "has_internal/internal._aggregated"; +$pagemtime{"has_internal/internal"} = 123456789; +$pagectime{"has_internal/internal"} = 123456789; +writefile("has_internal/internal._aggregated", "t/tmp", "this page is internal"); + +# a directory containing only an attachment should be indexed +$pagesources{"attached/pie.jpg"} = "attached/pie.jpg"; +$pagemtime{"attached/pie.jpg"} = 123456789; +$pagectime{"attached/pie.jpg"} = 123456789; +writefile("attached/pie.jpg", "t/tmp", "I lied, this isn't a real JPEG"); + +# "gone" disappeared just before this refresh pass so it still has a mtime +$pagemtime{gone} = $pagectime{gone} = 1000000; + +my %pages; +my @del; + +IkiWiki::Plugin::autoindex::refresh(); + +# this page is still on record as having been deleted, because it has +# a reason to be re-created +is($wikistate{autoindex}{autofile}{"deleted.mdwn"}, 1); +is($autofiles{"deleted.mdwn"}{plugin}, "autoindex"); +%pages = (); +@del = (); +IkiWiki::gen_autofile("deleted.mdwn", \%pages, \@del); +is_deeply(\%pages, {}); +is_deeply(\@del, []); +ok(! -f "t/tmp/deleted.mdwn"); + +# this page is tried as an autofile, but because it'll be in @del, it's not +# actually created +ok(! exists $wikistate{autoindex}{autofile}{"gone.mdwn"}); +%pages = (); +@del = ("gone.mdwn"); +is($autofiles{"gone.mdwn"}{plugin}, "autoindex"); +IkiWiki::gen_autofile("gone.mdwn", \%pages, \@del); +is_deeply(\%pages, {}); +is_deeply(\@del, ["gone.mdwn"]); +ok(! -f "t/tmp/gone.mdwn"); + +# this page does not exist and has no reason to be re-created, but we no longer +# have a special case for that - see +# [[todo/autoindex_should_use_add__95__autofile]] - so it won't be created +# even if it gains subpages later +is($wikistate{autoindex}{autofile}{"expunged.mdwn"}, 1); +ok(! exists $autofiles{"expunged.mdwn"}); +ok(! -f "t/tmp/expunged.mdwn"); + +# a directory containing only an internal page shouldn't be indexed +ok(! exists $wikistate{autoindex}{autofile}{"has_internal.mdwn"}); +ok(! exists $autofiles{"has_internal.mdwn"}); +ok(! -f "t/tmp/has_internal.mdwn"); + +# this page was re-created, but that no longer gets a special case +# (see [[todo/autoindex_should_use_add__95__autofile]]) so it's the same as +# deleted +is($wikistate{autoindex}{autofile}{"reinstated.mdwn"}, 1); +ok(! exists $autofiles{"reinstated.mdwn"}); +ok(! -f "t/tmp/reinstated.mdwn"); + +# needs creating (deferred; part of the autofile mechanism now) +ok(! exists $wikistate{autoindex}{autofile}{"tags.mdwn"}); +%pages = (); +@del = (); +is($autofiles{"tags.mdwn"}{plugin}, "autoindex"); +IkiWiki::gen_autofile("tags.mdwn", \%pages, \@del); +is_deeply(\%pages, {"t/tmp/tags" => 1}); +is_deeply(\@del, []); +ok(! -s "t/tmp/tags.mdwn"); +ok(-s "t/tmp/.ikiwiki/transient/tags.mdwn"); + +# needs creating because of an attachment +ok(! exists $wikistate{autoindex}{autofile}{"attached.mdwn"}); +%pages = (); +@del = (); +is($autofiles{"attached.mdwn"}{plugin}, "autoindex"); +IkiWiki::gen_autofile("attached.mdwn", \%pages, \@del); +is_deeply(\%pages, {"t/tmp/attached" => 1}); +is_deeply(\@del, []); +ok(-s "t/tmp/.ikiwiki/transient/attached.mdwn"); + +1; diff --git a/t/basename.t b/t/basename.t new file mode 100755 index 000000000..87ae42cf3 --- /dev/null +++ b/t/basename.t @@ -0,0 +1,12 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 6; + +BEGIN { use_ok("IkiWiki"); } + +is(IkiWiki::basename("/home/joey/foo/bar"), "bar"); +is(IkiWiki::basename("./foo"), "foo"); +is(IkiWiki::basename("baz"), "baz"); +is(IkiWiki::basename("/tmp/"), ""); +is(IkiWiki::basename("/home/joey/foo/"), ""); diff --git a/t/basewiki_brokenlinks.t b/t/basewiki_brokenlinks.t new file mode 100755 index 000000000..74ddc61c5 --- /dev/null +++ b/t/basewiki_brokenlinks.t @@ -0,0 +1,29 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More 'no_plan'; + +ok(! system("rm -rf t/tmp; mkdir t/tmp")); +ok(! system("make -s ikiwiki.out")); +ok(! system("make underlay_install DESTDIR=`pwd`/t/tmp/install PREFIX=/usr >/dev/null")); + +foreach my $plugin ("", "listdirectives") { + ok(! system("LC_ALL=C perl -I. ./ikiwiki.out -rebuild -plugin brokenlinks ". + # always enabled because pages link to it conditionally, + # which brokenlinks cannot handle properly + "-plugin smiley ". + ($plugin ? "-plugin $plugin " : ""). + "-underlaydir=t/tmp/install/usr/share/ikiwiki/basewiki ". + "-set underlaydirbase=t/tmp/install/usr/share/ikiwiki ". + "-templatedir=templates t/basewiki_brokenlinks t/tmp/out")); + my $result=`grep 'no broken links' t/tmp/out/index.html`; + ok(length($result)); + if (! length $result) { + print STDERR "\n\nbroken links found".($plugin ? " (with $plugin)" : "")."\n"; + system("grep '<li>' t/tmp/out/index.html >&2"); + print STDERR "\n\n"; + } + ok(-e "t/tmp/out/style.css"); # linked to.. + ok(! system("rm -rf t/tmp/out t/basewiki_brokenlinks/.ikiwiki")); +} +ok(! system("rm -rf t/tmp")); diff --git a/t/basewiki_brokenlinks/index.mdwn b/t/basewiki_brokenlinks/index.mdwn new file mode 100644 index 000000000..41768f782 --- /dev/null +++ b/t/basewiki_brokenlinks/index.mdwn @@ -0,0 +1 @@ +[[!brokenlinks ]] diff --git a/t/bazaar.t b/t/bazaar.t new file mode 100755 index 000000000..6e58f48f1 --- /dev/null +++ b/t/bazaar.t @@ -0,0 +1,116 @@ +#!/usr/bin/perl +use warnings; +use strict; +my $dir; +BEGIN { + $dir = "/tmp/ikiwiki-test-bzr.$$"; + my $bzr=`which bzr`; + chomp $bzr; + if (! -x $bzr) { + eval q{ + use Test::More skip_all => "bzr not available" + } + } + if (! mkdir($dir)) { + die $@; + } +} +use Test::More tests => 17; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{rcs} = "bzr"; +$config{srcdir} = "$dir/repo"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +# XXX +# This is a workaround for bzr's new requirement that bzr whoami be run +# before committing. This makes the test suite work with an unconfigured +# bzr, but ignores the need to have a properly configured bzr before +# using ikiwiki with bzr. +$ENV{HOME}=$dir; +system 'bzr whoami test@example.com'; + +system "bzr init $config{srcdir}"; + +use CGI::Session; +my $session=CGI::Session->new; +$session->param("name", "Joe User"); + +# Web commit +my $test1 = readfile("t/test1.mdwn"); +writefile('test1.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test1.mdwn"); +IkiWiki::rcs_commit( + file => "test1.mdwn", + message => "Added the first page", + token => "moo", + session => $session); + +my @changes; +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 0); +is($changes[0]{message}[0]{"line"}, "Added the first page"); +is($changes[0]{pages}[0]{"page"}, "test1"); +is($changes[0]{user}, "Joe User"); + +# Manual commit +my $username = "Foo Bar"; +my $user = "$username <foo.bar\@example.com>"; +my $message = "Added the second page"; + +my $test2 = readfile("t/test2.mdwn"); +writefile('test2.mdwn', $config{srcdir}, $test2); +system "bzr add --quiet $config{srcdir}/test2.mdwn"; +system "bzr commit --quiet --author \"$user\" -m \"$message\" $config{srcdir}"; + +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 1); +is($changes[0]{message}[0]{"line"}, $message); +is($changes[0]{user}, $username); +is($changes[0]{pages}[0]{"page"}, "test2"); + +is($changes[1]{pages}[0]{"page"}, "test1"); + +my $ctime = IkiWiki::rcs_getctime("test2.mdwn"); +ok($ctime >= time() - 20); + +my $mtime = IkiWiki::rcs_getmtime("test2.mdwn"); +ok($mtime >= time() - 20); + +writefile('test3.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test3.mdwn"); +IkiWiki::rcs_rename("test3.mdwn", "test4.mdwn"); +IkiWiki::rcs_commit_staged( + message => "Added the 4th page", + session => $session, +); + +@changes = IkiWiki::rcs_recentchanges(4); + +is($#changes, 2); +is($changes[0]{pages}[0]{"page"}, "test4"); + +ok(mkdir($config{srcdir}."/newdir")); +IkiWiki::rcs_rename("test4.mdwn", "newdir/test5.mdwn"); +IkiWiki::rcs_commit_staged( + message => "Added the 5th page", + session => $session, +); + +@changes = IkiWiki::rcs_recentchanges(4); + +is($#changes, 3); +is($changes[0]{pages}[0]{"page"}, "newdir/test5"); + +IkiWiki::rcs_remove("newdir/test5.mdwn"); +IkiWiki::rcs_commit_staged( + message => "Remove the 5th page", + session => $session, +); + +system "rm -rf $dir"; diff --git a/t/beautify_urlpath.t b/t/beautify_urlpath.t new file mode 100755 index 000000000..94b923d3b --- /dev/null +++ b/t/beautify_urlpath.t @@ -0,0 +1,17 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 8; + +BEGIN { use_ok("IkiWiki"); } + +$IkiWiki::config{usedirs} = 1; +$IkiWiki::config{htmlext} = "HTML"; +is(IkiWiki::beautify_urlpath("foo/bar"), "./foo/bar"); +is(IkiWiki::beautify_urlpath("../badger"), "../badger"); +is(IkiWiki::beautify_urlpath("./bleh"), "./bleh"); +is(IkiWiki::beautify_urlpath("foo/index.HTML"), "./foo/"); +is(IkiWiki::beautify_urlpath("index.HTML"), "./"); +is(IkiWiki::beautify_urlpath("../index.HTML"), "../"); +$IkiWiki::config{usedirs} = 0; +is(IkiWiki::beautify_urlpath("foo/index.HTML"), "./foo/index.HTML"); diff --git a/t/bestlink.t b/t/bestlink.t new file mode 100755 index 000000000..0020a05e2 --- /dev/null +++ b/t/bestlink.t @@ -0,0 +1,33 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 11; + +BEGIN { use_ok("IkiWiki"); } + +sub test ($$$) { + my $page=shift; + my $link=shift; + my @existing_pages=@{shift()}; + + %IkiWiki::pagecase=(); + %pagesources=(); + $IkiWiki::config{userdir}="foouserdir"; + foreach my $page (@existing_pages) { + $IkiWiki::pagecase{lc $page}=$page; + $pagesources{$page}="$page.mdwn"; + } + + return bestlink($page, $link); +} + +is(test("bar", "foo", ["bar"]), "", "broken link"); +is(test("bar", "foo", ["bar", "foo"]), "foo", "simple link"); +is(test("bar", "FoO", ["bar", "foo"]), "foo", "simple link with different input case"); +is(test("bar", "foo", ["bar", "fOo"]), "fOo", "simple link with different page case"); +is(test("bar", "FoO", ["bar", "fOo"]), "fOo", "simple link with different page and input case"); +is(test("bar", "Foo", ["bar", "fOo", "foo", "fOO", "Foo", "fOo"]), "Foo", "in case of ambiguity, like case wins"); +is(test("bar", "foo", ["bar", "foo", "bar/foo"]), "bar/foo", "simple subpage link"); +is(test("bar", "foo/subpage", ["bar", "foo", "bar/subpage", "foo/subpage"]), "foo/subpage", "cross subpage link"); +is(test("bar", "bob", ["bar", "foo", "foouserdir/bob"]), "foouserdir/bob", "user link"); +is(test("bar", "bob", ["bar", "foo", "bob", "foouserdir/bob"]), "bob", "non-user link"); diff --git a/t/calculate_changed_links.t b/t/calculate_changed_links.t new file mode 100755 index 000000000..bf6e2af45 --- /dev/null +++ b/t/calculate_changed_links.t @@ -0,0 +1,58 @@ +#!/usr/bin/perl +package IkiWiki; + +use warnings; +use strict; +use Test::More tests => 5; + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; + +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +IkiWiki::checkconfig(); + +foreach my $page (qw(tags/a tags/b Reorder Add Remove TypeAdd TypeRemove)) { + $pagesources{$page} = "$page.mdwn"; + $pagemtime{$page} = $pagectime{$page} = 1000000; +} + +$oldlinks{Reorder} = [qw{tags/a tags/b}]; +$links{Reorder} = [qw{tags/b tags/a}]; + +$oldlinks{Add} = [qw{tags/b}]; +$links{Add} = [qw{tags/a tags/b}]; + +$oldlinks{Remove} = [qw{tags/a}]; +$links{Remove} = []; + +$oldlinks{TypeAdd} = [qw{tags/a tags/b}]; +$links{TypeAdd} = [qw{tags/a tags/b}]; +# This causes TypeAdd to be rebuilt, but isn't a backlink change, so it doesn't +# cause tags/b to be rebuilt. +$oldtypedlinks{TypeAdd}{tag} = { "tags/a" => 1 }; +$typedlinks{TypeAdd}{tag} = { "tags/a" => 1, "tags/b" => 1 }; + +$oldlinks{TypeRemove} = [qw{tags/a tags/b}]; +$links{TypeRemove} = [qw{tags/a tags/b}]; +# This causes TypeRemove to be rebuilt, but isn't a backlink change, so it +# doesn't cause tags/b to be rebuilt. +$oldtypedlinks{TypeRemove}{tag} = { "tags/a" => 1 }; +$typedlinks{TypeRemove}{tag} = { "tags/a" => 1, "tags/b" => 1 }; + +my $oldlink_targets = calculate_old_links([keys %pagesources], []); +is_deeply($oldlink_targets, { + Reorder => { "tags/a" => "tags/a", "tags/b" => "tags/b" }, + Add => { "tags/b" => "tags/b" }, + Remove => { "tags/a" => "tags/a" }, + TypeAdd => { "tags/a" => "tags/a", "tags/b" => "tags/b" }, + TypeRemove => { "tags/a" => "tags/a", "tags/b" => "tags/b" }, + }); +my ($backlinkchanged, $linkchangers) = calculate_changed_links([keys %pagesources], [], $oldlink_targets); + +is_deeply($backlinkchanged, { "tags/a" => 1 }); +is_deeply($linkchangers, { add => 1, remove => 1, typeadd => 1, typeremove => 1 }); diff --git a/t/cmp_path.t b/t/cmp_path.t new file mode 100755 index 000000000..9de79f49b --- /dev/null +++ b/t/cmp_path.t @@ -0,0 +1,48 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 25; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::checkconfig(); + +sub test { + my ($before, $after) = @_; + + $IkiWiki::SortSpec::a = $before; + $IkiWiki::SortSpec::b = $after; + my $r = IkiWiki::SortSpec::cmp_path(); + + if ($before eq $after) { + is($r, 0); + } + else { + is($r, -1); + } + + $IkiWiki::SortSpec::a = $after; + $IkiWiki::SortSpec::b = $before; + $r = IkiWiki::SortSpec::cmp_path(); + + if ($before eq $after) { + is($r, 0); + } + else { + is($r, 1); + } + + is_deeply([IkiWiki::SortSpec::sort_pages(\&IkiWiki::SortSpec::cmp_path, $before, $after)], + [$before, $after]); + is_deeply([IkiWiki::SortSpec::sort_pages(\&IkiWiki::SortSpec::cmp_path, $after, $before)], + [$before, $after]); +} + +test("a/b/c", "a/b/c"); +test("a/b", "a/c"); +test("a/z", "z/a"); +test("a", "a/b"); +test("a", "a/b"); +test("a/z", "ab"); diff --git a/t/comments.t b/t/comments.t new file mode 100755 index 000000000..da2148b6b --- /dev/null +++ b/t/comments.t @@ -0,0 +1,57 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More 'no_plan'; +use IkiWiki; + +ok(! system("rm -rf t/tmp")); +ok(mkdir "t/tmp"); +ok(! system("cp -R t/tinyblog t/tmp/in")); +ok(mkdir "t/tmp/in/post" or -d "t/tmp/in/post"); + +my $comment; + +$comment = <<EOF; +[[!comment username="neil" + date="1969-07-20T20:17:40Z" + content="I landed"]] +EOF +#ok(eval { writefile("post/comment_3._comment", "t/tmp/in", $comment); 1 }); +writefile("post/comment_3._comment", "t/tmp/in", $comment); + +$comment = <<EOF; +[[!comment username="christopher" + date="1969-02-12T07:00:00Z" + content="I explored"]] +EOF +writefile("post/comment_2._comment", "t/tmp/in", $comment); + +$comment = <<EOF; +[[!comment username="william" + date="1969-01-14T12:00:00Z" + content="I conquered"]] +EOF +writefile("post/comment_1._comment", "t/tmp/in", $comment); + +# Give the files mtimes in the wrong order +ok(utime(111111111, 111111111, "t/tmp/in/post/comment_3._comment")); +ok(utime(222222222, 222222222, "t/tmp/in/post/comment_2._comment")); +ok(utime(333333333, 333333333, "t/tmp/in/post/comment_1._comment")); + +# Build the wiki +ok(! system("make -s ikiwiki.out")); +ok(! system("perl -I. ./ikiwiki.out -verbose -plugin comments -url=http://example.com -cgiurl=http://example.com/ikiwiki.cgi -rss -atom -underlaydir=underlays/basewiki -set underlaydirbase=underlays -set comments_pagespec='*' -templatedir=templates t/tmp/in t/tmp/out")); + +# Check that the comments are in the right order + +sub slurp { + open my $fh, "<", shift or return undef; + local $/; + my $content = <$fh>; + close $fh or return undef; + return $content; +} + +my $content = slurp("t/tmp/out/post/index.html"); +ok(defined $content); +ok($content =~ m/I conquered.*I explored.*I landed/s); diff --git a/t/conflicts.t b/t/conflicts.t new file mode 100755 index 000000000..d7e04d3ae --- /dev/null +++ b/t/conflicts.t @@ -0,0 +1,131 @@ +#!/usr/bin/perl +# Tests for bugs relating to conflicting files in the srcdir +use warnings; +use strict; +use Test::More tests => 106; + +# setup +my $srcdir="t/tmp/src"; +my $destdir="t/tmp/dest"; +ok(! system("make -s ikiwiki.out")); + +# runs ikiwiki to build test site +sub runiki { + my $testdesc=shift; + ok((! system("perl -I. ./ikiwiki.out -plugin txt -plugin rawhtml -underlaydir=underlays/basewiki -set underlaydirbase=underlays -templatedir=templates $srcdir $destdir @_")), + $testdesc); +} +sub refreshiki { + runiki(shift); +} +sub setupiki { + ok(! system("rm -rf $srcdir/.ikiwiki $destdir")); + runiki(shift, "--rebuild"); +} +sub newsrcdir { + ok(! system("rm -rf $srcdir $destdir")); + ok(! system("mkdir -p $srcdir")); +} + +# At one point, changing the extension of the source file of a page caused +# ikiwiki to fail. +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +setupiki("initial setup"); +ok(! system("mv $srcdir/foo.mdwn $srcdir/foo.txt")); +refreshiki("changed extension of source file of page"); +ok(! system("mv $srcdir/foo.txt $srcdir/foo.mdwn")); +refreshiki("changed extension of source file of page 2"); + +# Conflicting page sources is sorta undefined behavior, +# but should not crash ikiwiki. +# Added when refreshing +ok(! system("touch $srcdir/foo.txt")); +refreshiki("conflicting page sources in refresh"); +# Present during setup +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +ok(! system("touch $srcdir/foo.txt")); +setupiki("conflicting page sources in setup"); + +# Page and non-page file with same pagename. +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +ok(! system("touch $srcdir/foo")); +setupiki("conflicting page and non-page in setup"); +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +setupiki("initial setup"); +ok(! system("touch $srcdir/foo")); +refreshiki("conflicting page added (non-page already existing) in refresh"); +newsrcdir(); +ok(! system("touch $srcdir/foo")); +setupiki("initial setup"); +ok(! system("touch $srcdir/foo.mdwn")); +refreshiki("conflicting non-page added (page already existing) in refresh"); + +# Page that renders to a file that is also a subdirectory holding another +# file. +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +ok(! system("mkdir -p $srcdir/foo/index.html")); +ok(! system("touch $srcdir/foo/index.html/bar.mdwn")); +setupiki("conflicting page file and subdirectory"); +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +ok(! system("mkdir -p $srcdir/foo/index.html")); +ok(! system("touch $srcdir/foo/index.html/bar")); +setupiki("conflicting page file and subdirectory 2"); + +# Changing a page file into a non-page could also cause ikiwiki to fail. +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +setupiki("initial setup"); +ok(! system("mv $srcdir/foo.mdwn $srcdir/foo")); +refreshiki("page file turned into non-page"); + +# Changing a non-page file into a page could also cause ikiwiki to fail. +newsrcdir(); +ok(! system("touch $srcdir/foo")); +setupiki("initial setup"); +ok(! system("mv $srcdir/foo $srcdir/foo.mdwn")); +refreshiki("non-page file turned into page"); + +# What if a page renders to the same html file that a rawhtml file provides? +# Added when refreshing +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +setupiki("initial setup"); +ok(! system("mkdir -p $srcdir/foo")); +ok(! system("touch $srcdir/foo/index.html")); +refreshiki("rawhtml file rendered same as existing page in refresh"); +# Moved when refreshing +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +setupiki("initial setup"); +ok(! system("mkdir -p $srcdir/foo")); +ok(! system("mv $srcdir/foo.mdwn $srcdir/foo/index.html")); +refreshiki("existing page moved to rawhtml file in refresh"); +# Inverse added when refreshing +newsrcdir(); +ok(! system("mkdir -p $srcdir/foo")); +ok(! system("touch $srcdir/foo/index.html")); +setupiki("initial setup"); +ok(! system("touch $srcdir/foo.mdwn")); +refreshiki("page rendered same as existing rawhtml file in refresh"); +# Inverse moved when refreshing +newsrcdir(); +ok(! system("mkdir -p $srcdir/foo")); +ok(! system("touch $srcdir/foo/index.html")); +setupiki("initial setup"); +ok(! system("mv $srcdir/foo/index.html $srcdir/foo.mdwn")); +refreshiki("rawhtml file moved to page in refresh"); +# Present during setup +newsrcdir(); +ok(! system("touch $srcdir/foo.mdwn")); +ok(! system("mkdir -p $srcdir/foo")); +ok(! system("touch $srcdir/foo/index.html")); +setupiki("rawhtml file rendered same as existing page in setup"); + +# cleanup +ok(! system("rm -rf t/tmp")); diff --git a/t/crazy-badass-perl-bug.t b/t/crazy-badass-perl-bug.t new file mode 100755 index 000000000..328a979c2 --- /dev/null +++ b/t/crazy-badass-perl-bug.t @@ -0,0 +1,20 @@ +#!/usr/bin/perl +# DO NOT CHANGE ANYTHING IN THIS FILE. +# THe crazy bug reproduced here will go away if any of the calls +# to htmlize are changed. +# Note: This was http://bugs.debian.org/376329 , and was fixed in +# perl 5.14. +use warnings; +use strict; +use Test::More tests => 102; +use Encode; + +BEGIN { use_ok("IkiWiki"); } + +# Initialize htmlscrubber plugin +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::loadplugins(); IkiWiki::checkconfig(); +ok(IkiWiki::htmlize("foo", "foo", "mdwn", readfile("t/test1.mdwn"))); +ok(IkiWiki::htmlize("foo", "foo", "mdwn", readfile("t/test3.mdwn")), + "wtf?") for 1..100; diff --git a/t/cvs.t b/t/cvs.t new file mode 100755 index 000000000..cbac43252 --- /dev/null +++ b/t/cvs.t @@ -0,0 +1,708 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More; my $total_tests = 72; +use IkiWiki; + +my $default_test_methods = '^test_*'; +my @required_programs = qw( + cvs + cvsps +); +my @required_modules = qw( + File::chdir + File::MimeInfo + Date::Parse + File::Temp + File::ReadBackwards +); +my $dir = "/tmp/ikiwiki-test-cvs.$$"; + +# TESTS FOR GENERAL META-BEHAVIOR + +sub test_web_comments { + # how much of the web-edit workflow are we actually testing? + # because we want to test comments: + # - when the first comment for page.mdwn is added, and page/ is + # created to hold the comment, page/ isn't added to CVS control, + # so the comment isn't either + # - can't reproduce after chmod g+s ikiwiki.cgi (20120204) + # - side effect for moderated comments: after approval they + # show up normally AND are still pending, too + # - comments.pm treats rcs_commit_staged() as returning conflicts? +} + +sub test_chdir_magic { + # when are we bothering with "local $CWD" and when aren't we? +} + +sub test_cvs_info { + # inspect "Repository revision" (used in code) + # inspect "Sticky Options" (used in tests to verify existence of "-kb") +} + +sub test_cvs_run_cvs { + # extract the stdout-redirect thing + # - prove that it silences stdout + # - prove that stderr comes through just fine + # prove that when cvs exits nonzero (fail), function exits false + # prove that when cvs exits zero (success), function exits true + # always pass -f, just in case + # steal from git.pm: safe_git(), run_or_{die,cry,non} + # - open() instead of system() + # always call cvs_run_cvs(), don't ever run 'cvs' directly + # - for cvs_info(), make it respect wantarray +} + +sub test_cvs_run_cvsps { + # parameterize command like run_cvs() + # expose config vars for e.g. "--cvs-direct -z 30" + # always pass -x (unless proven otherwise) + # - but diff doesn't! optimization alert + # always pass -b HEAD (configurable like gitmaster_branch?) +} + +sub test_cvs_parse_cvsps { + # extract method from rcs_recentchanges + # document expected changeset format + # document expected changeset delimiter + # try: cvsps -q -x -p && ls | sort -rn | head -100 + # - benchmark against current impl (that uses File::ReadBackwards) +} + +sub test_cvs_parse_log_accum { + # add new, preferred method for rcs_recentchanges to use + # teach log_accum to record commits (into transient?) + # script cvsps to bootstrap (or replace?) commit history + # teach ikiwiki-makerepo to set up log_accum and commit_prep + # why are NetBSD commit mails unreliable? + # - is it working for CVS commits and failing for web commits? +} + +sub test_cvs_is_controlling { + # with no args: + # - if srcdir is in CVS, return true + # - else, return false + # with a dir arg: + # - if dir is in CVS, return true + # - else, return false + # with a file arg: + # - is there anything that wants the answer? if so, answer + # - else, die +} + + +# TESTS FOR GENERAL PLUGIN API CALLS + +sub test_checkconfig { + my $default_cvspath = 'ikiwiki'; + + undef $config{cvspath}; IkiWiki::checkconfig(); + is( + $config{cvspath}, $default_cvspath, + q{can provide default cvspath}, + ); + + $config{cvspath} = "/$default_cvspath/"; IkiWiki::checkconfig(); + is( + $config{cvspath}, $default_cvspath, + q{can set typical cvspath and strip well-meaning slashes}, + ); + + $config{cvspath} = "/$default_cvspath//subdir"; IkiWiki::checkconfig(); + is( + $config{cvspath}, "$default_cvspath/subdir", + q{can really sanitize cvspath as assumed by rcs_recentchanges}, + ); + + my $default_num_wrappers = @{$config{wrappers}}; + undef $config{cvs_wrapper}; IkiWiki::checkconfig(); + is( + @{$config{wrappers}}, $default_num_wrappers, + q{can start with no wrappers configured}, + ); + + $config{cvs_wrapper} = $config{cvsrepo} . "/CVSROOT/post-commit"; + IkiWiki::checkconfig(); + is( + @{$config{wrappers}}, ++$default_num_wrappers, + q{can add cvs_wrapper}, + ); + + undef $config{cvs_wrapper}; + $config{cvspath} = $default_cvspath; + IkiWiki::checkconfig(); +} + +sub test_getsetup { + # anything worth testing? +} + +sub test_genwrapper { + # testable directly? affects rcs_add, but are we exercising this? +} + + +# TESTS FOR VCS PLUGIN API CALLS + +sub test_rcs_update { + # can it assume we're under CVS control? or must it check? + # anything else worth testing? +} + +sub test_rcs_prepedit { + # can it assume we're under CVS control? or must it check? + # for existing file, returns latest revision in repo + # - what's this used for? should it return latest revision in checkout? + # for new file, returns empty string + + # netbsd web log says "could not open lock file" + # XXX does this work right? + # how about with un-added dirs in the srcdir? + # how about with cvsps.core lying around? +} + +sub test_rcs_commit { + # can it assume we're under CVS control? or must it check? + # if someone else changed the page since rcs_prepedit was called: + # - try to merge into our working copy + # - if merge succeeds, proceed to commit + # - else, return page content with the conflict markers in it + # commit: + # - if success, return undef + # - else, revert + return content with the conflict markers in it + # git.pm receives "session" param -- useful here? + # web commits start with "web commit {by,from} " + + # XXX commit can fail due to "could not open lock file" +} + +sub test_rcs_commit_staged { + # if commit succeeds, return undef + # else, warn and return error message (really? or just non-undef?) +} + +sub test_rcs_add { + my @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 0); + + my $message = "add a top-level ASCII (non-UTF-8) page via VCS API"; + my $file = q{test0.mdwn}; + add_and_commit($file, $message, qq{# \$Id\$\n* some plain ASCII text}); + is_newly_added($file); + is_in_keyword_substitution_mode($file, q{-kkv}); + like( + readfile($config{srcdir} . "/$file"), + qr/^# \$Id: $file,v 1\.1 .+\$$/m, + q{can expand RCS Id keyword}, + ); + my $generated_file = $config{destdir} . q{/test0/index.html}; + ok(-e $generated_file, q{post-commit hook generates content}); + like( + readfile($generated_file), + qr/^<h1>\$Id: $file,v 1\.1 .+\$<\/h1>$/m, + q{can htmlize mdwn, including RCS Id}, + ); + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 1); + is_most_recent_change(\@changes, stripext($file), $message); + + $message = "add a top-level dir via VCS API"; + my $dir1 = q{test3}; + can_mkdir($dir1); + IkiWiki::rcs_add($dir1); + # XXX test that the wrapper hangs here without our genwrapper() + # XXX test that the wrapper doesn't hang here with it + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 1); # despite the dir add + IkiWiki::rcs_commit( + file => $dir1, + message => $message, + token => "oom", + ); + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 1); # dirs aren't tracked + + $message = "add a non-ASCII (UTF-8) text file in an un-added dir"; + can_mkdir($_) for (qw(test4 test4/test5)); + $file = q{test4/test5/test1.mdwn}; + add_and_commit($file, $message, readfile("t/test1.mdwn")); + is_newly_added($file); + is_in_keyword_substitution_mode($file, q{-kkv}); + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 2); + is_most_recent_change(\@changes, stripext($file), $message); + + $message = "add a binary file in an un-added dir, and commit_staged"; + can_mkdir(q{test6}); + $file = q{test6/test7.ico}; + my $bindata_in = readfile("doc/favicon.ico", 1); + my $bindata_out = sub { readfile($config{srcdir} . "/$file", 1) }; + writefile($file, $config{srcdir}, $bindata_in, 1); + is(&$bindata_out(), $bindata_in, q{binary files match before commit}); + IkiWiki::rcs_add($file); + IkiWiki::rcs_commit_staged(message => $message); + is_newly_added($file); + is_in_keyword_substitution_mode($file, q{-kb}); + is(&$bindata_out(), $bindata_in, q{binary files match after commit}); + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 3); + is_most_recent_change(\@changes, $file, $message); + ok( + unlink($config{srcdir} . "/$file"), + q{can remove file in order to re-fetch it from repo}, + ); + ok(! -e $config{srcdir} . "/$file", q{really removed file}); + IkiWiki::rcs_update(); + is(&$bindata_out(), $bindata_in, q{binary files match after re-fetch}); + + $message = "add a UTF-8 and a binary file in different dirs"; + my $file1 = "test8/test9.mdwn"; + my $file2 = "test10/test11.ico"; + can_mkdir($_) for (qw(test8 test10)); + writefile($file1, $config{srcdir}, readfile('t/test2.mdwn')); + writefile($file2, $config{srcdir}, $bindata_in, 1); + IkiWiki::rcs_add($_) for ($file1, $file2); + IkiWiki::rcs_commit_staged(message => $message); + is_newly_added($_) for ($file1, $file2); + is_in_keyword_substitution_mode($file1, q{-kkv}); + is_in_keyword_substitution_mode($file2, q{-kb}); + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 3); + @changes = IkiWiki::rcs_recentchanges(4); + is_total_number_of_changes(\@changes, 4); + # XXX test for both files in the commit, and no other files + is_most_recent_change(\@changes, $file2, $message); + + $message = "remove the UTF-8 and binary files we just added"; + IkiWiki::rcs_remove($_) for ($file1, $file2); + IkiWiki::rcs_commit_staged(message => $message); + ok(! -d "$config{srcdir}/test8", q{empty dir pruned by post-commit}); + ok(! -d "$config{srcdir}/test10", q{empty dir pruned by post-commit}); + @changes = IkiWiki::rcs_recentchanges(11); + is_total_number_of_changes(\@changes, 5); + # XXX test for both files in the commit, and no other files + is_most_recent_change(\@changes, $file2, $message); + + $message = "re-add UTF-8 and binary files with names swapped"; + writefile($file2, $config{srcdir}, readfile('t/test2.mdwn')); + writefile($file1, $config{srcdir}, $bindata_in, 1); + IkiWiki::rcs_add($_) for ($file1, $file2); + IkiWiki::rcs_commit_staged(message => $message); + isnt_newly_added($_) for ($file1, $file2); + is_in_keyword_substitution_mode($file2, q{-kkv}); + is_in_keyword_substitution_mode($file1, q{-kb}); + @changes = IkiWiki::rcs_recentchanges(11); + is_total_number_of_changes(\@changes, 6); + # XXX test for both files in the commit, and no other files + is_most_recent_change(\@changes, $file2, $message); + + # prevent web edits from attempting to create .../CVS/foo.mdwn + # on case-insensitive filesystems, also prevent .../cvs/foo.mdwn + # unless your "CVS" is something else and we've made it configurable + # also want a pre-commit hook for this? + + # pre-commit hook: + # - lcase filenames + # - ? + + # can it assume we're under CVS control? or must it check? +} + +sub test_rcs_remove { + # can it assume we're under CVS control? or must it check? + # remove a top-level file + # - rcs_commit + # - inspect recentchanges: one new change, file removed + # remove two files (in different dirs) + # - rcs_commit_staged + # - inspect recentchanges: one new change, both files removed +} + +sub test_rcs_rename { + # can it assume we're under CVS control? or must it check? + # rename a file in the same dir + # - rcs_commit_staged + # - inspect recentchanges: one new change, one file removed, one added + # rename a file into a different dir + # - rcs_commit_staged + # - inspect recentchanges: one new change, one file removed, one added + # rename a file into a not-yet-existing dir + # - rcs_commit_staged + # - inspect recentchanges: one new change, one file removed, one added + # is it safe to use "mv"? what if $dest is somehow outside the wiki? +} + +sub test_rcs_recentchanges { + my @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 0); + + my $message = "Add a page via CVS directly"; + my $file = q{test2.mdwn}; + writefile($file, $config{srcdir}, readfile(q{t/test2.mdwn})); + system "cd $config{srcdir}" + . " && cvs add $file >/dev/null 2>&1"; + system "cd $config{srcdir}" + . " && cvs commit -m \"$message\" $file >/dev/null"; + + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 1); + is_most_recent_change(\@changes, stripext($file), $message); + + # CVS commits run ikiwiki once for every committed file (!) + # - commit_prep alone should fix this + # CVS multi-dir commits show only the first dir in recentchanges + # - commit_prep might also fix this? + # CVS post-commit hook is amped off to avoid locking against itself + # - commit_prep probably doesn't fix this... but maybe? + # can it assume we're under CVS control? or must it check? + # don't worry whether we're called with a number (we always are) + # other rcs tests already inspect much of the returned structure + # CVS commits say "cvs" and get the right committer + # web commits say "web" and get the right committer + # - and don't start with "web commit {by,from} " + # "nickname" -- can we ever meaningfully set this? + + # prefer log_accum, then cvsps, else die + # run the high-level recentchanges tests 2x (once for each method) + # - including in other test subs that check recentchanges? +} + +sub test_rcs_diff { + my @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 0); + + my $message = "add a UTF-8 and an ASCII file in different dirs"; + my $file1 = "rcsdiff1/utf8.mdwn"; + my $file2 = "rcsdiff2/ascii.mdwn"; + my $contents2 = ''; $contents2 .= "$_. foo\n" for (1..11); + can_mkdir($_) for (qw(rcsdiff1 rcsdiff2)); + writefile($file1, $config{srcdir}, readfile('t/test2.mdwn')); + writefile($file2, $config{srcdir}, $contents2); + IkiWiki::rcs_add($_) for ($file1, $file2); + IkiWiki::rcs_commit_staged(message => $message); + + # XXX we rely on rcs_recentchanges() to be called first! + # XXX or else for no cvsps cache to exist yet... + # XXX because rcs_diff() doesn't pass -x (as an optimization) + @changes = IkiWiki::rcs_recentchanges(3); + is_total_number_of_changes(\@changes, 1); + + unlike( + $changes[0]->{pages}->[0]->{diffurl}, + qr/%2F/m, + q{path separators are preserved when UTF-8scaping filename}, + ); + + my $changeset = 1; + + my $maxlines = undef; + my $scalar_diffs = IkiWiki::rcs_diff($changeset, $maxlines); + like( + $scalar_diffs, + qr/^\+11\. foo$/m, + q{unbounded scalar diffs go all the way to 11}, + ); + my @array_diffs = IkiWiki::rcs_diff($changeset, $maxlines); + is( + $array_diffs[$#array_diffs], + "+11. foo\n", + q{unbounded array diffs go all the way to 11}, + ); + + $maxlines = 8; + $scalar_diffs = IkiWiki::rcs_diff($changeset, $maxlines); + unlike( + $scalar_diffs, + qr/^\+11\. foo$/m, + q{bounded scalar diffs don't go all the way to 11}, + ); + @array_diffs = IkiWiki::rcs_diff($changeset, $maxlines); + isnt( + $array_diffs[$#array_diffs], + "+11. foo\n", + q{bounded array diffs don't go all the way to 11}, + ); + is( + scalar @array_diffs, + $maxlines, + q{bounded array diffs contain expected maximum number of lines}, + ); + + # can it assume we're under CVS control? or must it check? +} + +sub test_rcs_getctime { + # can it assume we're under CVS control? or must it check? + # given a file, find its creation time, else return 0 + # first implement in the obvious way + # then cache +} + +sub test_rcs_getmtime { + # can it assume we're under CVS control? or must it check? + # given a file, find its modification time, else return 0 + # first implement in the obvious way + # then cache +} + +sub test_rcs_receive { + my $description = q{rcs_receive doesn't make sense for CVS}; + exists $IkiWiki::hooks{rcs}{rcs_receive} + ? fail($description) + : pass($description); +} + +sub test_rcs_preprevert { + # can it assume we're under CVS control? or must it check? + # given a patchset number, return structure describing what'd happen: + # - see doc/plugins/write.mdwn:rcs_receive() + # don't forget about attachments +} + +sub test_rcs_revert { + # test rcs_recentchanges() real darn well + # extract read-backwards patchset parser from rcs_recentchanges() + # recentchanges: given max, return list of changeset/files/etc. + # revert: given changeset ID, return list of file/rev/action + # + # can it assume we're under CVS control? or must it check? + # given a patchset number, stage the revert for rcs_commit_staged() + # if commit succeeds, return undef + # else, warn and return error message (really? or just non-undef?) +} + +sub main { + my $test_methods = defined $ENV{TEST_METHOD} + ? $ENV{TEST_METHOD} + : $default_test_methods; + + _startup($test_methods eq $default_test_methods); + _runtests(_get_matching_test_subs($test_methods)); + _shutdown($test_methods eq $default_test_methods); +} + +main(); + + +# INTERNAL SUPPORT ROUTINES + +sub _plan_for_test_more { + my $can_plan = shift; + + foreach my $program (@required_programs) { + my $program_path = `which $program`; + chomp $program_path; + return plan(skip_all => "$program not available") + unless -x $program_path; + } + + foreach my $module (@required_modules) { + eval qq{use $module}; + return plan(skip_all => "$module not available") + if $@; + } + + return plan(skip_all => "can't create $dir: $!") + unless mkdir($dir); + return plan(skip_all => "can't remove $dir: $!") + unless rmdir($dir); + + return unless $can_plan; + + return plan(tests => $total_tests); +} + +# http://stackoverflow.com/questions/607282/whats-the-best-way-to-discover-all-subroutines-a-perl-module-has + +use B qw/svref_2object/; + +sub in_package { + my ($coderef, $package) = @_; + my $cv = svref_2object($coderef); + return if not $cv->isa('B::CV') or $cv->GV->isa('B::SPECIAL'); + return $cv->GV->STASH->NAME eq $package; +} + +sub list_module { + my $module = shift; + no strict 'refs'; + return grep { + defined &{"$module\::$_"} and in_package(\&{*$_}, $module) + } keys %{"$module\::"}; +} + + +# support for xUnit-style testing, a la Test::Class + +sub _startup { + my $can_plan = shift; + _plan_for_test_more($can_plan); + hook(type => "genwrapper", id => "cvstest", call => \&_wrapper_paths); + _generate_test_config(); +} + +sub _shutdown { + my $had_plan = shift; + done_testing() unless $had_plan; +} + +sub _setup { + _generate_test_repo(); +} + +sub _teardown { + system "rm -rf $dir"; +} + +sub _runtests { + my @coderefs = (@_); + for (@coderefs) { + _setup(); + $_->(); + _teardown(); + } +} + +sub _get_matching_test_subs { + my $re = shift; + no strict 'refs'; + return map { \&{*$_} } grep { /$re/ } sort(list_module('main')); +} + +sub _generate_test_config { + %config = IkiWiki::defaultconfig(); + $config{rcs} = "cvs"; + $config{srcdir} = "$dir/src"; + $config{allow_symlinks_before_srcdir} = 1; + $config{destdir} = "$dir/dest"; + $config{cvsrepo} = "$dir/repo"; + $config{cvspath} = "ikiwiki"; + use Cwd; $config{templatedir} = getcwd() . '/templates'; + $config{diffurl} = "/nonexistent/cvsweb/[[file]]"; + IkiWiki::loadplugins(); + IkiWiki::checkconfig(); +} + +sub _generate_test_repo { + die "can't create $dir: $!" + unless mkdir($dir); + + my $cvs = "cvs -d $config{cvsrepo}"; + my $dn = ">/dev/null"; + + system "$cvs init $dn"; + system "mkdir $dir/$config{cvspath} $dn"; + system "cd $dir/$config{cvspath} && " + . "$cvs import -m import $config{cvspath} VENDOR RELEASE $dn"; + system "rm -rf $dir/$config{cvspath} $dn"; + system "$cvs co -d $config{srcdir} $config{cvspath} $dn"; + + _generate_and_configure_post_commit_hook(); +} + +sub _generate_and_configure_post_commit_hook { + $config{cvs_wrapper} = $config{cvsrepo} . "/CVSROOT/test-post"; + $config{wrapper} = $config{cvs_wrapper}; + + require IkiWiki::Wrapper; + { + no warnings 'once'; + $IkiWiki::program_to_wrap = 'ikiwiki.out'; + # XXX substitute its interpreter to Makefile's $(PERL) + # XXX best solution: do this to all scripts during build + } + IkiWiki::gen_wrapper(); + + my $cvs = "cvs -d $config{cvsrepo}"; + my $dn = ">/dev/null"; + + system "mkdir $config{destdir} $dn"; + system "cd $dir && $cvs co CVSROOT $dn && cd CVSROOT && " . + "echo 'DEFAULT $config{cvsrepo}/CVSROOT/test-post %{sVv} &' " + . " >> loginfo && " + . "$cvs commit -m 'test repo setup' $dn && " + . "cd .. && rm -rf CVSROOT"; +} + +sub add_and_commit { + my ($file, $message, $contents) = @_; + writefile($file, $config{srcdir}, $contents); + IkiWiki::rcs_add($file); + IkiWiki::rcs_commit( + file => $file, + message => $message, + token => "moo", + ); +} + +sub can_mkdir { + my $dir = shift; + ok( + mkdir($config{srcdir} . "/$dir"), + qq{can mkdir $dir}, + ); +} + +sub is_newly_added { _newly_added_or_not(shift, 1) } +sub isnt_newly_added { _newly_added_or_not(shift, 0) } +sub _newly_added_or_not { + my ($file, $expected_new) = @_; + my ($func, $word); + if ($expected_new) { + $func = \&Test::More::is; + $word = q{is}; + } + else { + $func = \&Test::More::isnt; + $word = q{isn't}; + } + $func->( + IkiWiki::Plugin::cvs::cvs_info("Repository revision", $file), + '1.1', + qq{$file $word newly added to CVS}, + ); +} + +sub is_in_keyword_substitution_mode { + my ($file, $mode) = @_; + is( + IkiWiki::Plugin::cvs::cvs_info("Sticky Options", $file), + $mode, + qq{$file is in CVS with expected keyword substitution mode}, + ); +} + +sub is_total_number_of_changes { + my ($changes, $expected_total) = @_; + is( + $#{$changes}, + $expected_total - 1, + qq{total commits == $expected_total}, + ); +} + +sub is_most_recent_change { + my ($changes, $page, $message) = @_; + is( + $changes->[0]{message}[0]{"line"}, + $message, + q{most recent commit's first message line matches}, + ); + is( + $changes->[0]{pages}[0]{"page"}, + $page, + q{most recent commit's first pagename matches}, + ); +} + +sub stripext { + my ($file, $extension) = @_; + $extension = '\..+?' unless defined $extension; + $file =~ s|$extension$||g; + return $file; +} + +sub _wrapper_paths { + return qq{newenviron[i++]="PERL5LIB=$ENV{PERL5LIB}";}; +} diff --git a/t/dirname.t b/t/dirname.t new file mode 100755 index 000000000..197d00d63 --- /dev/null +++ b/t/dirname.t @@ -0,0 +1,12 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 6; + +BEGIN { use_ok("IkiWiki"); } + +is(IkiWiki::dirname("/home/joey/foo/bar"), "/home/joey/foo"); +is(IkiWiki::dirname("./foo"), "."); +is(IkiWiki::dirname("baz"), ""); +is(IkiWiki::dirname("/tmp/"), "/tmp/"); +is(IkiWiki::dirname("/home/joey/foo/"), "/home/joey/foo/"); diff --git a/t/file_pruned.t b/t/file_pruned.t new file mode 100755 index 000000000..34f366610 --- /dev/null +++ b/t/file_pruned.t @@ -0,0 +1,40 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 27; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); + +ok(IkiWiki::file_pruned(".htaccess")); +ok(IkiWiki::file_pruned(".ikiwiki/")); +ok(IkiWiki::file_pruned(".ikiwiki/index")); +ok(IkiWiki::file_pruned("CVS/foo")); +ok(IkiWiki::file_pruned("subdir/CVS/foo")); +ok(IkiWiki::file_pruned(".svn")); +ok(IkiWiki::file_pruned("subdir/.svn")); +ok(IkiWiki::file_pruned("subdir/.svn/foo")); +ok(IkiWiki::file_pruned(".git")); +ok(IkiWiki::file_pruned("subdir/.git")); +ok(IkiWiki::file_pruned("subdir/.git/foo")); +ok(! IkiWiki::file_pruned("svn/fo")); +ok(! IkiWiki::file_pruned("git")); +ok(! IkiWiki::file_pruned("index.mdwn")); +ok(! IkiWiki::file_pruned("index.")); +ok(IkiWiki::file_pruned(".")); +ok(IkiWiki::file_pruned("./")); + +# absolute filenames are not allowed. +ok(IkiWiki::file_pruned("/etc/passwd")); +ok(IkiWiki::file_pruned("//etc/passwd")); +ok(IkiWiki::file_pruned("/")); +ok(IkiWiki::file_pruned("//")); +ok(IkiWiki::file_pruned("///")); + + +ok(IkiWiki::file_pruned("..")); +ok(IkiWiki::file_pruned("../")); + +ok(IkiWiki::file_pruned("y/foo.dpkg-tmp")); +ok(IkiWiki::file_pruned("y/foo.ikiwiki-new")); diff --git a/t/find_src_files.t b/t/find_src_files.t new file mode 100755 index 000000000..a3742db75 --- /dev/null +++ b/t/find_src_files.t @@ -0,0 +1,97 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 20; + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } + +%config=IkiWiki::defaultconfig(); +$config{srcdir}="t/tmp/srcdir"; +$config{underlaydir}="t/tmp/underlaydir"; +IkiWiki::checkconfig(); + +sub cleanup { + ok(! system("rm -rf t/tmp")); +} + +sub setup_underlay { + foreach my $file (@_) { + writefile($file, $config{underlaydir}, "test content"); + } + return @_; +} + +sub setup_srcdir { + foreach my $file (@_) { + writefile($file, $config{srcdir}, "test content"); + } + return @_; +} + +sub test_src_files { + my %expected=map { $_ => 1 } @{shift()}; # the input list may have dups + my $desc=shift; + + close STDERR; # find_src_files prints warnings about bad files + + my ($files, $pages)=IkiWiki::find_src_files(); + is_deeply([sort @$files], [sort keys %expected], $desc); +} + +cleanup(); + +my @list=setup_underlay(qw{index.mdwn sandbox.mdwn smiley.png ikiwiki.mdwn ikiwiki/directive.mdwn ikiwiki/directive/foo.mdwn}); +push @list, setup_srcdir(qw{index.mdwn foo.mwdn icon.jpeg blog/archive/1/2/3/foo.mdwn blog/archive/1/2/4/bar.mdwn blog/archive.mdwn}); +test_src_files(\@list, "simple test"); + +setup_srcdir(".badfile"); +test_src_files(\@list, "srcdir dotfile is skipped"); + +setup_underlay(".badfile"); +test_src_files(\@list, "underlay dotfile is skipped"); + +setup_srcdir(".ikiwiki/index"); +test_src_files(\@list, "srcdir dotdir is skipped"); + +setup_underlay(".ikiwiki/index"); +test_src_files(\@list, "underlay dotdir is skipped"); + +setup_srcdir("foo>.mdwn"); +test_src_files(\@list, "illegal srcdir filename skipped"); + +setup_underlay("foo>.mdwn"); +test_src_files(\@list, "illegal underlay filename skipped"); + +system("mkdir -p $config{srcdir}/empty"); +test_src_files(\@list, "empty srcdir directory ignored"); + +system("mkdir -p $config{underlaydir}/empty"); +test_src_files(\@list, "empty underlay directory ignored"); + +setup_underlay("bad.mdwn"); +system("ln -sf /etc/passwd $config{srcdir}/bad.mdwn"); +test_src_files(\@list, "underlaydir override attack foiled"); + +system("ln -sf /etc/passwd $config{srcdir}/symlink.mdwn"); +test_src_files(\@list, "file symlink in srcdir skipped"); + +system("ln -sf /etc/passwd $config{underlaydir}/symlink.mdwn"); +test_src_files(\@list, "file symlink in underlaydir skipped"); + +system("ln -sf /etc/ $config{srcdir}/symdir"); +test_src_files(\@list, "dir symlink in srcdir skipped"); + +system("ln -sf /etc/ $config{underlaydir}/symdir"); +test_src_files(\@list, "dir symlink in underlaydir skipped"); + +system("ln -sf /etc/ $config{srcdir}/blog/symdir"); +test_src_files(\@list, "deep dir symlink in srcdir skipped"); + +system("ln -sf /etc/ $config{underlaydir}/ikiwiki/symdir"); +test_src_files(\@list, "deep dir symlink in underlaydir skipped"); + + + + +cleanup(); diff --git a/t/git.t b/t/git.t new file mode 100755 index 000000000..6d847dfb0 --- /dev/null +++ b/t/git.t @@ -0,0 +1,97 @@ +#!/usr/bin/perl +use warnings; +use strict; + +my $dir; +BEGIN { + $dir="/tmp/ikiwiki-test-git.$$"; + my $git=`which git`; + chomp $git; + if (! -x $git) { + eval q{ + use Test::More skip_all => "git not available" + } + } + if (! mkdir($dir)) { + die $@; + } +} +use Test::More tests => 18; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{rcs} = "git"; +$config{srcdir} = "$dir/src"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +ok (mkdir($config{srcdir})); +is (system("./ikiwiki-makerepo git $config{srcdir} $dir/repo"), 0); + +my @changes; +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 0); # counts for dummy commit during repo creation +# ikiwiki-makerepo's first commit is setting up the .gitignore +is($changes[0]{message}[0]{"line"}, "initial commit"); +is($changes[0]{pages}[0]{"page"}, ".gitignore"); + +# Web commit +my $test1 = readfile("t/test1.mdwn"); +writefile('test1.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test1.mdwn"); +IkiWiki::rcs_commit( + file => "test1.mdwn", + message => "Added the first page", + token => "moo", +); + +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 1); +is($changes[0]{message}[0]{"line"}, "Added the first page"); +is($changes[0]{pages}[0]{"page"}, "test1"); + +# Manual commit +my $message = "Added the second page"; + +my $test2 = readfile("t/test2.mdwn"); +writefile('test2.mdwn', $config{srcdir}, $test2); +system "cd $config{srcdir}; git add test2.mdwn >/dev/null 2>&1"; +system "cd $config{srcdir}; git commit -m \"$message\" test2.mdwn >/dev/null 2>&1"; +system "cd $config{srcdir}; git push origin >/dev/null 2>&1"; + +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 2); +is($changes[0]{message}[0]{"line"}, $message); +is($changes[0]{pages}[0]{"page"}, "test2"); + +is($changes[1]{pages}[0]{"page"}, "test1"); + +# Renaming + +writefile('test3.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test3.mdwn"); +IkiWiki::rcs_rename("test3.mdwn", "test4.mdwn"); +IkiWiki::rcs_commit_staged(message => "Added the 4th page"); + +@changes = IkiWiki::rcs_recentchanges(4); + +is($#changes, 3); +is($changes[0]{pages}[0]{"page"}, "test4"); + +ok(mkdir($config{srcdir}."/newdir")); +IkiWiki::rcs_rename("test4.mdwn", "newdir/test5.mdwn"); +IkiWiki::rcs_commit_staged(message => "Added the 5th page"); + +@changes = IkiWiki::rcs_recentchanges(4); + +is($#changes, 3); +is($changes[0]{pages}[0]{"page"}, "newdir/test5"); + +IkiWiki::rcs_remove("newdir/test5.mdwn"); +IkiWiki::rcs_commit_staged(message => "Remove the 5th page"); + +system "rm -rf $dir"; diff --git a/t/html.t b/t/html.t new file mode 100755 index 000000000..3faf44154 --- /dev/null +++ b/t/html.t @@ -0,0 +1,30 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More; + +my @pages; + +BEGIN { + @pages=qw(index features news plugins/map security); + if (! -x "/usr/bin/validate") { + plan skip_all => "/usr/bin/validate html validator not present"; + } + else { + plan(tests => int @pages + 2); + } + use_ok("IkiWiki"); +} + +# Have to build the html pages first. +# Note that just building them like this doesn't exersise all the possible +# html that can be generated, in particular it misses some of the action +# links at the top, etc. +ok(system("make >/dev/null") == 0); + +foreach my $page (@pages) { + print "# Validating $page\n"; + ok(system("validate html/$page.html") == 0); +} + +# TODO: validate form output html diff --git a/t/htmlbalance.t b/t/htmlbalance.t new file mode 100755 index 000000000..e5a5db0ee --- /dev/null +++ b/t/htmlbalance.t @@ -0,0 +1,23 @@ +#!/usr/bin/perl +use warnings; +use strict; + +BEGIN { + eval q{ + use HTML::TreeBuilder; + }; + if ($@) { + eval q{use Test::More skip_all => "HTML::TreeBuilder not available"}; + } + else { + eval q{use Test::More tests => 7}; + } + use_ok("IkiWiki::Plugin::htmlbalance"); +} + +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "<br></br>"), "<br />"); +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "<div><p b=\"c\">hello world</div>"), "<div><p b=\"c\">hello world</p></div>"); +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "<a></a></a>"), "<a></a>"); +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "<b>foo <a</b>"), "<b>foo </b>"); +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "<b> foo <a</a></b>"), "<b> foo </b>"); +is(IkiWiki::Plugin::htmlbalance::sanitize(content => "a>"), "a>"); diff --git a/t/htmlize.t b/t/htmlize.t new file mode 100755 index 000000000..1569c8dcf --- /dev/null +++ b/t/htmlize.t @@ -0,0 +1,85 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 31; +use Encode; + +BEGIN { use_ok("IkiWiki"); } + +# Initialize htmlscrubber plugin +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +is(IkiWiki::htmlize("foo", "foo", "mdwn", "foo\n\nbar\n"), "<p>foo</p>\n\n<p>bar</p>\n", + "basic"); +my $val=Encode::encode_utf8(IkiWiki::htmlize("foo", "foo", "mdwn", readfile("t/test1.mdwn"))); +ok($val =~/ó/ && $val =~/óóóóó/, "utf8; bug #373203"); +ok(IkiWiki::htmlize("foo", "foo", "mdwn", readfile("t/test2.mdwn")), + "this file crashes markdown if it's fed in as decoded utf-8"); + +sub gotcha { + my $html=IkiWiki::htmlize("foo", "foo", "mdwn", shift); + return $html =~ /GOTCHA/; +} +ok(!gotcha(q{<a href="javascript:alert('GOTCHA')">click me</a>}), + "javascript url"); +ok(!gotcha(q{<a href="jscript:alert('GOTCHA')">click me</a>}), + "jscript url"); +ok(!gotcha(q{<a href="vbscript:alert('GOTCHA')">click me</a>}), + "vbscrpt url"); +ok(!gotcha(q{<a href="java script:alert('GOTCHA')">click me</a>}), + "java-tab-script url"); +ok(!gotcha(q{<span style="any: expressio(GOTCHA)n(window.location='http://example.org/')">foo</span>}), + "entity-encoded CSS script test"); +ok(!gotcha(q{<span style="any: expression(GOTCHA)(window.location='http://example.org/')">foo</span>}), + "another entity-encoded CSS script test"); +ok(!gotcha(q{<script>GOTCHA</script>}), + "script tag"); +ok(!gotcha(q{<form action="javascript:alert('GOTCHA')">foo</form>}), + "form action with javascript"); +ok(!gotcha(q{<video poster="javascript:alert('GOTCHA')" href="foo.avi">foo</video>}), + "video poster with javascript"); +ok(!gotcha(q{<span style="background: url(javascript:window.location=GOTCHA)">a</span>}), + "CSS script test"); +ok(! gotcha(q{<img src="data:text/javascript;GOTCHA">}), + "data:text/javascript (jeez!)"); +ok(gotcha(q{<img src="data:image/png;base64,GOTCHA">}), "data:image/png"); +ok(gotcha(q{<img src="data:image/gif;base64,GOTCHA">}), "data:image/gif"); +ok(gotcha(q{<img src="data:image/jpeg;base64,GOTCHA">}), "data:image/jpeg"); +ok(gotcha(q{<p>javascript:alert('GOTCHA')</p>}), + "not javascript AFAIK (but perhaps some web browser would like to + be perverse and assume it is?)"); +ok(gotcha(q{<img src="javascript.png?GOTCHA">}), "not javascript"); +ok(gotcha(q{<a href="javascript.png?GOTCHA">foo</a>}), "not javascript"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<img alt="foo" src="foo.gif">}), + q{<img alt="foo" src="foo.gif">}, "img with alt tag allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="http://google.com/">}), + q{<a href="http://google.com/">}, "absolute url allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="foo.html">}), + q{<a href="foo.html">}, "relative url allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<span class="foo">bar</span>}), + q{<span class="foo">bar</span>}, "class attribute allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="aaa#foo">}), + q{<a href="aaa#foo">}, "simple anchor allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="aaa#foo:bar">}), + q{<a href="aaa#foo:bar">}, "colon allowed in anchor"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="aaa?foo:bar">}), + q{<a href="aaa?foo:bar">}, "colon allowed in query string"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="foo:bar">}), + q{<a>}, "unknown protocol blocked"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="#foo">}), + q{<a href="#foo">}, "simple relative anchor allowed"); +is(IkiWiki::htmlize("foo", "foo", "mdwn", + q{<a href="#foo:bar">}), + q{<a href="#foo:bar">}, "colon in simple relative anchor allowed"); diff --git a/t/index.t b/t/index.t new file mode 100755 index 000000000..392a167e9 --- /dev/null +++ b/t/index.t @@ -0,0 +1,161 @@ +#!/usr/bin/perl +use warnings; +use strict; +use IkiWiki; + +package IkiWiki; # use internal variables +use Test::More tests => 31; + +$config{wikistatedir}="/tmp/ikiwiki-test.$$"; +system "rm -rf $config{wikistatedir}"; + +ok(! loadindex(), "loading nonexistent index file"); + +# Load standard plugins. +ok(loadplugin("meta"), "meta plugin loaded"); +ok(loadplugin("mdwn"), "mdwn plugin loaded"); + +# Set up a default state. +$pagesources{"Foo"}="Foo.mdwn"; +$pagesources{"bar"}="bar.mdwn"; +$pagesources{"bar.png"}="bar.png"; +my $now=time(); +$pagemtime{"Foo"}=$now; +$pagemtime{"bar"}=$now-1000; +$pagemtime{"bar.png"}=$now; +$pagectime{"Foo"}=$now; +$pagectime{"bar"}=$now-100000; +$pagectime{"bar.png"}=$now-100000; +$renderedfiles{"Foo"}=["Foo.html"]; +$renderedfiles{"bar"}=["bar.html", "bar.rss", "sparkline-foo.gif"]; +$renderedfiles{"bar.png"}=["bar.png"]; +$links{"Foo"}=["bar.png"]; +$links{"bar"}=["Foo", "new-page"]; +$typedlinks{"bar"}={tag => {"Foo" => 1}}; +$links{"bar.png"}=[]; +$depends{"Foo"}={}; +$depends{"bar"}={"foo*" => 1}; +$depends{"bar.png"}={}; +$pagestate{"bar"}{meta}{title}="a page about bar"; +$pagestate{"bar"}{meta}{moo}="mooooo"; +# only loaded plugins save state, so this should not be saved out +$pagestate{"bar"}{nosuchplugin}{moo}="mooooo"; + +ok(saveindex(), "save index"); +ok(-s "$config{wikistatedir}/indexdb", "index file created"); + +# Clear state. +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +ok(loadindex(), "load index"); +is_deeply(\%pagesources, { + Foo => "Foo.mdwn", + bar => "bar.mdwn", + "bar.png" => "bar.png", +}, "%pagesources loaded correctly"); +is_deeply(\%pagemtime, { + Foo => $now, + bar => $now-1000, + "bar.png" => $now, +}, "%pagemtime loaded correctly"); +is_deeply(\%pagectime, { + Foo => $now, + bar => $now-100000, + "bar.png" => $now-100000, +}, "%pagemtime loaded correctly"); +is_deeply(\%renderedfiles, { + Foo => ["Foo.html"], + bar => ["bar.html", "bar.rss", "sparkline-foo.gif"], + "bar.png" => ["bar.png"], +}, "%renderedfiles loaded correctly"); +is_deeply(\%oldrenderedfiles, { + Foo => ["Foo.html"], + bar => ["bar.html", "bar.rss", "sparkline-foo.gif"], + "bar.png" => ["bar.png"], +}, "%oldrenderedfiles loaded correctly"); +is_deeply(\%links, { + Foo => ["bar.png"], + bar => ["Foo", "new-page"], + "bar.png" => [], +}, "%links loaded correctly"); +is_deeply(\%depends, { + Foo => {}, + bar => {"foo*" => 1}, + "bar.png" => {}, +}, "%depends loaded correctly"); +is_deeply(\%pagestate, { + bar => { + meta => { + title => "a page about bar", + moo => "mooooo", + }, + }, +}, "%pagestate loaded correctly"); +is_deeply(\%pagecase, { + foo => "Foo", + bar => "bar", + "bar.png" => "bar.png" +}, "%pagecase generated correctly"); +is_deeply(\%destsources, { + "Foo.html" => "Foo", + "bar.html" => "bar", + "bar.rss" => "bar", + "sparkline-foo.gif" => "bar", + "bar.png" => "bar.png", +}, "%destsources generated correctly"); +is_deeply(\%typedlinks, { + bar => {tag => {"Foo" => 1}}, +}, "%typedlinks loaded correctly"); +is_deeply(\%oldtypedlinks, { + bar => {tag => {"Foo" => 1}}, +}, "%oldtypedlinks loaded correctly"); + +# Clear state. +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +# When state is loaded for a wiki rebuild, only ctime, oldrenderedfiles, +# and pagesources are retained. +$config{rebuild}=1; +ok(loadindex(), "load index"); +is_deeply(\%pagesources, { + Foo => "Foo.mdwn", + bar => "bar.mdwn", + "bar.png" => "bar.png", +}, "%pagesources loaded correctly"); +is_deeply(\%pagemtime, { +}, "%pagemtime loaded correctly"); +is_deeply(\%pagectime, { + Foo => $now, + bar => $now-100000, + "bar.png" => $now-100000, +}, "%pagemtime loaded correctly"); +is_deeply(\%renderedfiles, { +}, "%renderedfiles loaded correctly"); +is_deeply(\%oldrenderedfiles, { + Foo => ["Foo.html"], + bar => ["bar.html", "bar.rss", "sparkline-foo.gif"], + "bar.png" => ["bar.png"], +}, "%oldrenderedfiles loaded correctly"); +is_deeply(\%links, { +}, "%links loaded correctly"); +is_deeply(\%depends, { +}, "%depends loaded correctly"); +is_deeply(\%pagestate, { +}, "%pagestate loaded correctly"); +is_deeply(\%pagecase, { # generated implicitly since pagesources is loaded + foo => "Foo", + bar => "bar", + "bar.png" => "bar.png" +}, "%pagecase generated correctly"); +is_deeply(\%destsources, { +}, "%destsources generated correctly"); +is_deeply(\%typedlinks, { +}, "%typedlinks cleared correctly"); +is_deeply(\%oldtypedlinks, { +}, "%oldtypedlinks cleared correctly"); + +system "rm -rf $config{wikistatedir}"; diff --git a/t/linkify.t b/t/linkify.t new file mode 100755 index 000000000..6dff0a029 --- /dev/null +++ b/t/linkify.t @@ -0,0 +1,112 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 32; + +BEGIN { use_ok("IkiWiki"); } + +# Initialize link plugin +%config=IkiWiki::defaultconfig(); +IkiWiki::loadplugins(); + +my $prefix_directives; + +sub linkify ($$$$) { + my $lpage=shift; + my $page=shift; + + my $content=shift; + my @existing_pages=@{shift()}; + + # This is what linkify and htmllink need set right now to work. + # This could change, if so, update it.. + %IkiWiki::pagecase=(); + %links=(); + foreach my $p (@existing_pages) { + $IkiWiki::pagecase{lc $p}=$p; + $links{$p}=[]; + $renderedfiles{"$p.mdwn"}=[$p]; + $destsources{$p}="$p.mdwn"; + } + + %config=IkiWiki::defaultconfig(); + $config{cgiurl}="http://somehost/ikiwiki.cgi"; + $config{srcdir}=$config{destdir}="/dev/null"; # placate checkconfig + # currently coded for non usedirs mode (TODO: check both) + $config{usedirs}=0; + $config{prefix_directives}=$prefix_directives; + + IkiWiki::checkconfig(); + + return IkiWiki::linkify($lpage, $page, $content); +} + +sub links_to ($$) { + my $link=shift; + my $content=shift; + + if ($content =~ m!<a href="[^"]*\Q$link\E[^"]*"\s*[^>]*>!) { + return 1; + } + else { + print STDERR "# expected link to $link in $content\n"; + return; + } +} + +sub not_links_to ($$) { + my $link=shift; + my $content=shift; + + if ($content !~ m!<a href="[^"]*\Q$link\E[^"]*">!) { + return 1; + } + else { + print STDERR "# expected no link to $link in $content\n"; + return; + } +} + +sub links_text ($$) { + my $text=shift; + my $content=shift; + + if ($content =~ m!>\Q$text\E</a>!) { + return 1; + } + else { + print STDERR "# expected link text $text in $content\n"; + return; + } +} + +# Tests that are the same for both styles of prefix directives. +foreach $prefix_directives (0,1) { + ok(links_to("bar", linkify("foo", "foo", "link to [[bar]] ok", ["foo", "bar"])), "ok link"); + ok(links_to("bar_baz", linkify("foo", "foo", "link to [[bar_baz]] ok", ["foo", "bar_baz"])), "ok link"); + ok(not_links_to("bar", linkify("foo", "foo", "link to \\[[bar]] ok", ["foo", "bar"])), "escaped link"); + ok(links_to("page=bar", linkify("foo", "foo", "link to [[bar]] ok", ["foo"])), "broken link"); + ok(links_to("bar", linkify("foo", "foo", "link to [[baz]] and [[bar]] ok", ["foo", "baz", "bar"])), "dual links"); + ok(links_to("baz", linkify("foo", "foo", "link to [[baz]] and [[bar]] ok", ["foo", "baz", "bar"])), "dual links"); + ok(links_to("bar", linkify("foo", "foo", "link to [[some_page|bar]] ok", ["foo", "bar"])), "named link"); + ok(links_text("some page", linkify("foo", "foo", "link to [[some_page|bar]] ok", ["foo", "bar"])), "named link text"); + ok(links_text("0", linkify("foo", "foo", "link to [[0|bar]] ok", ["foo", "bar"])), "named link to 0"); + ok(links_text("Some long, & complex page name.", linkify("foo", "foo", "link to [[Some_long,_&_complex_page_name.|bar]] ok, and this is not a link]] here", ["foo", "bar"])), "complex named link text"); + ok(links_to("foo/bar", linkify("foo/item", "foo", "link to [[bar]] ok", ["foo", "foo/item", "foo/bar"])), "inline page link"); + ok(links_to("bar", linkify("foo", "foo", "link to [[bar]] ok", ["foo", "foo/item", "foo/bar"])), "same except not inline"); + ok(links_to("bar#baz", linkify("foo", "foo", "link to [[bar#baz]] ok", ["foo", "bar"])), "anchor link"); +} + +$prefix_directives=0; +ok(not_links_to("some_page", linkify("foo", "foo", "link to [[some page]] ok", ["foo", "bar", "some_page"])), + "link with whitespace, without prefix_directives"); +ok(not_links_to("bar", linkify("foo", "foo", "link to [[some page|bar]] ok", ["foo", "bar"])), + "named link, with whitespace, without prefix_directives"); + +$prefix_directives=1; +ok(links_to("some_page", linkify("foo", "foo", "link to [[some page]] ok", ["foo", "bar", "some_page"])), + "link with whitespace"); +ok(links_to("bar", linkify("foo", "foo", "link to [[some page|bar]] ok", ["foo", "bar"])), + "named link, with whitespace"); +ok(links_text("some page", linkify("foo", "foo", "link to [[some page|bar]] ok", ["foo", "bar"])), + "named link text, with whitespace"); diff --git a/t/linkpage.t b/t/linkpage.t new file mode 100755 index 000000000..8085de153 --- /dev/null +++ b/t/linkpage.t @@ -0,0 +1,13 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 7; + +BEGIN { use_ok("IkiWiki"); } + +is(linkpage("foo bar"), "foo_bar"); +is(linkpage("foo bar baz"), "foo_bar_baz"); +is(linkpage("foo bar/baz"), "foo_bar/baz"); +is(linkpage("foo bar&baz"), "foo_bar__38__baz"); +is(linkpage("foo bar & baz"), "foo_bar___38___baz"); +is(linkpage("foo bar_baz"), "foo_bar_baz"); diff --git a/t/map.t b/t/map.t new file mode 100755 index 000000000..5d4713d6f --- /dev/null +++ b/t/map.t @@ -0,0 +1,242 @@ +#!/usr/bin/perl +package IkiWiki; + +use warnings; +use strict; +use Test::More; + +BEGIN { + unless (eval { require XML::Twig }) { + eval q{ + use Test::More skip_all => "XML::Twig is not available" + } + } +} + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } +BEGIN { use_ok("IkiWiki::Plugin::map"); } +BEGIN { use_ok("IkiWiki::Plugin::mdwn"); } + +ok(! system("rm -rf t/tmp; mkdir t/tmp")); + +$config{verbose} = 1; +$config{srcdir} = 't/tmp'; +$config{underlaydir} = 't/tmp'; +$config{underlaydirbase} = '.'; +$config{templatedir} = 'templates'; +$config{usedirs} = 1; +$config{htmlext} = 'html'; +$config{wiki_file_chars} = "-[:alnum:]+/.:_"; +$config{userdir} = "users"; +$config{tagbase} = "tags"; +$config{default_pageext} = "mdwn"; +$config{wiki_file_prune_regexps} = [qr/^\./]; +$config{autoindex_commit} = 0; + +is(checkconfig(), 1); + +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +my @pages = qw( +alpha +alpha/1 +alpha/1/i +alpha/1/ii +alpha/1/iii +alpha/1/iv +alpha/2 +alpha/2/a +alpha/2/b +alpha/3 +beta +); + +foreach my $page (@pages) { + # we use a non-default extension for these, so they're distinguishable + # from programmatically-created pages + $pagesources{$page} = "$page.mdwn"; + $destsources{$page} = "$page.mdwn"; + $pagemtime{$page} = $pagectime{$page} = 1000000; + writefile("$page.mdwn", "t/tmp", "your ad here"); +} + +sub comment { + my $str = shift; + $str =~ s/^/# /gm; + print $str; +} + +sub node { + my $name = shift; + my $kids = shift; + my %stuff = @_; + + return { %stuff, name => $name, kids => $kids }; +} + +sub check_nodes { + my $ul = shift; + my $expected = shift; + + is($ul->tag, 'ul'); + + # expected is a list of hashes + # ul is a list of li + foreach my $li ($ul->children) { + my @kids = $li->children; + + is($li->tag, 'li'); + + my $expectation = shift @$expected; + + is($kids[0]->tag, 'a'); + my $a = $kids[0]; + + if ($expectation->{parent}) { + is($a->att('class'), 'mapparent'); + } + else { + is($a->att('class'), 'mapitem'); + } + + is_deeply([$a->text], [$expectation->{name}]); + + if (@{$expectation->{kids}}) { + is(scalar @kids, 2); + + check_nodes($kids[1], $expectation->{kids}); + } + else { + is_deeply([@kids], [$a]); + } + } +} + +sub check { + my $pagespec = shift; + my $expected = shift; + comment("*** $pagespec ***\n"); + + my $html = IkiWiki::Plugin::map::preprocess(pages => $pagespec, + page => 'map', + destpage => 'map'); + comment($html); + my $tree = XML::Twig->new(pretty_print => 'indented'); + eval { + $tree->parse($html); + }; + if ($@) { + print "malformed XML: $@\n$html\n"; + ok(0); + } + my $fragment = $tree->root; + + is($fragment->tag, 'div'); + is($fragment->att('class'), 'map'); + + if (@$expected) { + check_nodes(($fragment->children)[0], $expected); + } + else { + ok(! $fragment->children); + } + + $tree->dispose; +} + +check('doesnotexist', []); + +check('alpha', [node('alpha', [])]); + +check('alpha/*', + [ + node('1', [ + node('i', []), + node('ii', []), + node('iii', []), + node('iv', []), + ]), + node('2', [ + node('a', []), + node('b', []), + ]), + node('3', []), + ]); + +check('alpha or alpha/*', + [ + node('alpha', [ + node('1', [ + node('i', []), + node('ii', []), + node('iii', []), + node('iv', []), + ]), + node('2', [ + node('a', []), + node('b', []), + ]), + node('3', []), + ]), + ]); + +check('alpha or alpha/1 or beta', + [ + node('alpha', [ + node('1', []), + ]), + node('beta', []), + ]); + +check('alpha/1 or beta', + [ + node('alpha', [ + node('1', []), + ], parent => 1), + node('beta', []), + ]); + +check('alpha/1/i* or alpha/2/a or beta', + [ + node('alpha', [ + node('1', [ + node('i', []), + node('ii', []), + node('iii', []), + node('iv', []), + ], parent => 1), + node('2', [ + node('a', []), + ], parent => 1), + ], parent => 1), + node('beta', []), + ]); + +check('alpha/1/i* or alpha/2/a', + [ + node('1', [ + node('i', []), + node('ii', []), + node('iii', []), + node('iv', []), + ], parent => 1), + node('2', [ + node('a', []), + ], parent => 1), + ]); + +check('alpha/1/i*', + [ + node('i', []), + node('ii', []), + node('iii', []), + node('iv', []), + ]); + +ok(! system("rm -rf t/tmp")); +done_testing; + +1; diff --git a/t/mercurial.t b/t/mercurial.t new file mode 100755 index 000000000..4918fc76e --- /dev/null +++ b/t/mercurial.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl +use warnings; +use strict; +my $dir; +BEGIN { + $dir = "/tmp/ikiwiki-test-hg.$$"; + my $hg=`which hg`; + chomp $hg; + if (! -x $hg) { + eval q{ + use Test::More skip_all => "hg not available" + } + } + if (! mkdir($dir)) { + die $@; + } +} +use Test::More tests => 11; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{rcs} = "mercurial"; +$config{srcdir} = "$dir/repo"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +use CGI::Session; +my $session=CGI::Session->new; +$session->param("name", "Joe User"); + +system "hg init $config{srcdir}"; + +# Web commit +my $test1 = readfile("t/test1.mdwn"); +writefile('test1.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test1.mdwn"); +IkiWiki::rcs_commit( + file => "test1.mdwn", + message => "Added the first page", + token => "moo", + session => $session, +); + +my @changes; +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 0); +is($changes[0]{message}[0]{"line"}, "Added the first page"); +is($changes[0]{pages}[0]{"page"}, "test1"); +is($changes[0]{user}, "Joe User"); + +# Manual commit +my $username = "Foo Bar"; +my $user = "$username <foo.bar\@example.com>"; +my $message = "Added the second page"; + +my $test2 = readfile("t/test2.mdwn"); +writefile('test2.mdwn', $config{srcdir}, $test2); +system "hg add -R $config{srcdir} $config{srcdir}/test2.mdwn"; +system "hg commit -R $config{srcdir} -u \"$user\" -m \"$message\" -d \"0 0\""; + +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 1); +is($changes[0]{message}[0]{"line"}, $message); +is($changes[0]{user}, $username); +is($changes[0]{pages}[0]{"page"}, "test2"); + +is($changes[1]{pages}[0]{"page"}, "test1"); + +my $ctime = IkiWiki::rcs_getctime("test2.mdwn"); +is($ctime, 0); + +system "rm -rf $dir"; diff --git a/t/openiduser.t b/t/openiduser.t new file mode 100755 index 000000000..746090103 --- /dev/null +++ b/t/openiduser.t @@ -0,0 +1,42 @@ +#!/usr/bin/perl +use warnings; +use strict; + +BEGIN { + eval q{ + use Net::OpenID::VerifiedIdentity; + }; + if ($@) { + eval q{use Test::More skip_all => "Net::OpenID::VerifiedIdentity not available"}; + } + else { + eval q{use Test::More tests => 11}; + } + use_ok("IkiWiki"); +} + +# Some typical examples: + +# This test, when run by Test::Harness using perl -w, exposes a warning in +# Net::OpenID::VerifiedIdentity. Normally that warning is not displayed, as +# that module does not use warnings. To avoid cluttering the test output, +# disable the -w switch temporarily. +$^W=0; +is(IkiWiki::openiduser('http://josephturian.blogspot.com'), 'josephturian [blogspot.com]'); +$^W=1; + +is(IkiWiki::openiduser('http://yam655.livejournal.com/'), 'yam655 [livejournal.com]'); +is(IkiWiki::openiduser('http://id.mayfirst.org/jamie/'), 'jamie [id.mayfirst.org]'); + +# yahoo has an anchor in the url +is(IkiWiki::openiduser('https://me.yahoo.com/joeyhess#35f22'), 'joeyhess [me.yahoo.com]'); +# google urls are horrendous, but the worst bit is after a ?, so can be dropped +is(IkiWiki::openiduser('https://www.google.com/accounts/o8/id?id=AItOawm-ebiIfxbKD3KNa-Cu9LvvD9edMLW7BAo'), 'id [www.google.com/accounts/o8]'); + +# and some less typical ones taken from the ikiwiki commit history + +is(IkiWiki::openiduser('http://thm.id.fedoraproject.org/'), 'thm [id.fedoraproject.org]'); +is(IkiWiki::openiduser('http://dtrt.org/'), 'dtrt.org'); +is(IkiWiki::openiduser('http://alcopop.org/me/openid/'), 'openid [alcopop.org/me]'); +is(IkiWiki::openiduser('http://id.launchpad.net/882/bielawski1'), 'bielawski1 [id.launchpad.net/882]'); +is(IkiWiki::openiduser('http://technorati.com/people/technorati/drajt'), 'drajt [technorati.com/people/technorati]'); diff --git a/t/pagename.t b/t/pagename.t new file mode 100755 index 000000000..540d10f4c --- /dev/null +++ b/t/pagename.t @@ -0,0 +1,35 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 19; + +BEGIN { use_ok("IkiWiki"); } + +# define mdwn as an extension +$IkiWiki::hooks{htmlize}{mdwn}={}; +is(pagetype("foo.mdwn"), "mdwn"); +is(pagename("foo.mdwn"), "foo"); +is(pagetype("foo/bar.mdwn"), "mdwn"); +is(pagename("foo/bar.mdwn"), "foo/bar"); + +# bare files get the full filename as page name, undef type +is(pagetype("foo.png"), undef); +is(pagename("foo.png"), "foo.png"); +is(pagetype("foo/bar.png"), undef); +is(pagename("foo/bar.png"), "foo/bar.png"); +is(pagetype("foo"), undef); +is(pagename("foo"), "foo"); + +# keepextension preserves the extension in the page name +$IkiWiki::hooks{htmlize}{txt}={keepextension => 1}; +is(pagename("foo.txt"), "foo.txt"); +is(pagetype("foo.txt"), "txt"); +is(pagename("foo/bar.txt"), "foo/bar.txt"); +is(pagetype("foo/bar.txt"), "txt"); + +# noextension makes extensionless files be treated as first-class pages +$IkiWiki::hooks{htmlize}{Makefile}={noextension =>1}; +is(pagetype("Makefile"), "Makefile"); +is(pagename("Makefile"), "Makefile"); +is(pagetype("foo/Makefile"), "Makefile"); +is(pagename("foo/Makefile"), "foo/Makefile"); diff --git a/t/pagespec_match.t b/t/pagespec_match.t new file mode 100755 index 000000000..a37b06e8e --- /dev/null +++ b/t/pagespec_match.t @@ -0,0 +1,147 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 87; + +BEGIN { use_ok("IkiWiki"); } + +ok(pagespec_match("foo", "*")); +ok(!pagespec_match("foo", "")); +ok(pagespec_match("foo", "!bar")); +ok(pagespec_match("page", "?ag?")); +ok(! pagespec_match("page", "?a?g?")); +ok(pagespec_match("foo.png", "*.*")); +ok(! pagespec_match("foo", "*.*")); +ok(pagespec_match("foo", "foo or bar"), "simple list"); +ok(pagespec_match("bar", "foo or bar"), "simple list 2"); +ok(pagespec_match("foo", "f?? and !foz")); +ok(! pagespec_match("foo", "f?? and !foo")); +ok(! pagespec_match("foo", "* and !foo")); +ok(! pagespec_match("foo", "foo and !foo")); +ok(! pagespec_match("foo.png", "* and !*.*")); +ok(pagespec_match("foo", "(bar or ((meep and foo) or (baz or foo) or beep))")); +ok(pagespec_match("foo", "( + bar + or ( + (meep and foo) + or + (baz or foo) + or beep + ) +)"), "multiline complex pagespec"); +ok(! pagespec_match("a/foo", "foo", location => "a/b"), "nonrelative fail"); +ok(! pagespec_match("foo", "./*", location => "a/b"), "relative fail"); +ok(pagespec_match("a/foo", "./*", location => "a/b"), "relative"); +ok(pagespec_match("a/b/foo", "./*", location => "a/b"), "relative 2"); +ok(pagespec_match("a/foo", "./*", "a/b"), "relative oldstyle call"); +ok(pagespec_match("foo", "./*", location => "a"), "relative toplevel"); +ok(pagespec_match("foo/bar", "*", location => "baz"), "absolute"); +ok(! pagespec_match("foo", "foo and bar"), "foo and bar"); +ok(pagespec_match("{f}oo", "{*}*"), "curly match"); +ok(! pagespec_match("foo", "{*}*"), "curly !match"); + +ok(pagespec_match("somepage", "user(frodo)", user => "frodo")); +ok(pagespec_match("somepage", "user(frodo)", user => "Frodo")); +ok(! pagespec_match("somepage", "user(frodo)", user => "Sam")); +ok(pagespec_match("somepage", "user(*o)", user => "Bilbo")); +ok(pagespec_match("somepage", "user(*o)", user => "frodo")); +ok(! pagespec_match("somepage", "user(*o)", user => "Sam")); +ok(pagespec_match("somepage", "user(http://*.myopenid.com/)", user => "http://foo.myopenid.com/")); +ok(pagespec_match("somepage", "user(*://*)", user => "http://foo.myopenid.com/")); + +# The link and backlink stuff needs this. +$config{userdir}=""; +$links{foo}=[qw{bar baz}]; +$links{bar}=[]; +$links{baz}=[]; +$links{meh}=[]; +$links{"bugs/foo"}=[qw{bugs/done}]; +$links{"bugs/done"}=[]; +$links{"bugs/bar"}=[qw{done}]; +$links{"done"}=[]; +$links{"done"}=[]; +$links{"examples/softwaresite/bugs/fails_to_frobnicate"}=[qw{done}]; +$links{"examples/softwaresite/bugs/done"}=[]; +$links{"ook"}=[qw{/blog/tags/foo}]; +foreach my $p (keys %links) { + $pagesources{$p}="$p.mdwn"; +} +$pagesources{"foo.png"}="foo.png"; +$pagesources{"foo"}="foo.mdwn"; +$IkiWiki::hooks{htmlize}{mdwn}={}; + +ok(pagespec_match("foo", "foo"), "simple"); +ok(! pagespec_match("foo", "bar"), "simple fail"); +ok(pagespec_match("foo", "foo"), "simple glob"); +ok(pagespec_match("foo", "f*"), "simple glob fail"); +ok(pagespec_match("foo", "page(foo)"), "page()"); +print pagespec_match("foo", "page(foo)")."\n"; +ok(! pagespec_match("foo", "page(bar)"), "page() fail"); +ok(! pagespec_match("foo.png", "page(foo.png)"), "page() fails on non-page"); +ok(! pagespec_match("foo.png", "page(foo*)"), "page() fails on non-page glob"); +ok(pagespec_match("foo", "page(foo)"), "page() glob"); +ok(pagespec_match("foo", "page(f*)"), "page() glob fail"); +ok(pagespec_match("foo", "link(bar)"), "link"); +ok(pagespec_match("foo", "link(.)", location => "bar"), "link with ."); +ok(! pagespec_match("foo", "link(.)"), "link with . but missing location"); +ok(pagespec_match("foo", "link(ba?)"), "glob link"); +ok(! pagespec_match("foo", "link(quux)"), "failed link"); +ok(! pagespec_match("foo", "link(qu*)"), "failed glob link"); +ok(pagespec_match("bugs/foo", "link(done)", location => "bugs/done"), "link match to bestlink"); +ok(! pagespec_match("examples/softwaresite/bugs/done", "link(done)", + location => "bugs/done"), "link match to bestlink"); +ok(pagespec_match("examples/softwaresite/bugs/fails_to_frobnicate", + "link(./done)", location => "examples/softwaresite/bugs/done"), "link relative"); +ok(! pagespec_match("foo", "link(./bar)", location => "foo/bar"), "link relative fail"); +ok(pagespec_match("bar", "backlink(foo)"), "backlink"); +ok(! pagespec_match("quux", "backlink(foo)"), "failed backlink"); +ok(! pagespec_match("bar", ""), "empty pagespec should match nothing"); +ok(! pagespec_match("bar", " "), "blank pagespec should match nothing"); +ok(pagespec_match("ook", "link(blog/tags/foo)"), "link internal absolute success"); +ok(pagespec_match("ook", "link(/blog/tags/foo)"), "link explicit absolute success"); +ok(pagespec_match("meh", "!link(done)"), "negated failing match is a success"); + +$ENV{TZ}="GMT"; +$IkiWiki::pagectime{foo}=1154532692; # Wed Aug 2 11:26 EDT 2006 +$IkiWiki::pagectime{bar}=1154532695; # after +ok(pagespec_match("foo", "created_before(bar)")); +ok(! pagespec_match("foo", "created_after(bar)")); +ok(! pagespec_match("bar", "created_before(foo)")); +ok(pagespec_match("bar", "created_after(foo)")); +ok(pagespec_match("foo", "creation_year(2006)"), "year"); +ok(! pagespec_match("foo", "creation_year(2005)"), "other year"); +ok(pagespec_match("foo", "creation_month(8)"), "month"); +ok(! pagespec_match("foo", "creation_month(9)"), "other month"); +ok(pagespec_match("foo", "creation_day(2)"), "day"); +ok(! pagespec_match("foo", "creation_day(3)"), "other day"); + +ok(! pagespec_match("foo", "no_such_function(foo)"), "foo"); + +my $ret=pagespec_match("foo", "(invalid"); +ok(! $ret, "syntax error"); +ok($ret =~ /syntax error/, "error message"); + +$ret=pagespec_match("foo", "bar or foo"); +ok($ret, "simple match"); +is($ret, "foo matches foo", "stringified return"); + +my $i=pagespec_match("foo", "link(bar)")->influences; +is(join(",", keys %$i), 'foo', "link is influenced by the page with the link"); +$i=pagespec_match("bar", "backlink(foo)")->influences; +is(join(",", keys %$i), 'foo', "backlink is influenced by the page with the link"); +$i=pagespec_match("bar", "backlink(foo)")->influences; +is(join(",", keys %$i), 'foo', "backlink is influenced by the page with the link"); +$i=pagespec_match("bar", "created_before(foo)")->influences; +is(join(",", keys %$i), 'foo', "created_before is influenced by the comparison page"); +$i=pagespec_match("bar", "created_after(foo)")->influences; +is(join(",", keys %$i), 'foo', "created_after is influenced by the comparison page"); +$i=pagespec_match("foo", "link(baz) and created_after(bar)")->influences; +is(join(",", sort keys %$i), 'bar,foo', "influences add up over AND"); +$i=pagespec_match("foo", "link(baz) and created_after(bar)")->influences; +is(join(",", sort keys %$i), 'bar,foo', "influences add up over OR"); +$i=pagespec_match("foo", "!link(baz) and !created_after(bar)")->influences; +is(join(",", sort keys %$i), 'bar,foo', "influences unaffected by negation"); +$i=pagespec_match("foo", "!link(baz) and !created_after(bar)")->influences; +is(join(",", sort keys %$i), 'bar,foo', "influences unaffected by negation"); +$i=pagespec_match("meh", "!link(done)")->influences; +is(join(",", sort keys %$i), 'meh', "a negated, failing link test is successful, so the page is a link influence"); diff --git a/t/pagespec_match_list.t b/t/pagespec_match_list.t new file mode 100755 index 000000000..7ff178aad --- /dev/null +++ b/t/pagespec_match_list.t @@ -0,0 +1,174 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 126; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::checkconfig(); + +{ + package IkiWiki::SortSpec; + + sub cmp_raw_path { $a cmp $b } +} + +%pagesources=( + foo => "foo.mdwn", + foo2 => "foo2.mdwn", + foo3 => "foo3.mdwn", + bar => "bar.mdwn", + "post/1" => "post/1.mdwn", + "post/2" => "post/2.mdwn", + "post/3" => "post/3.mdwn", +); +$IkiWiki::pagectime{foo} = 2; +$IkiWiki::pagectime{foo2} = 2; +$IkiWiki::pagectime{foo3} = 1; +$IkiWiki::pagectime{foo4} = 1; +$IkiWiki::pagectime{foo5} = 1; +$IkiWiki::pagectime{bar} = 3; +$IkiWiki::pagectime{"post/1"} = 6; +$IkiWiki::pagectime{"post/2"} = 6; +$IkiWiki::pagectime{"post/3"} = 6; +$links{foo}=[qw{post/1 post/2}]; +$links{foo2}=[qw{bar}]; +$links{foo3}=[qw{bar}]; + +is_deeply([pagespec_match_list("foo", "bar")], ["bar"]); +is_deeply([sort(pagespec_match_list("foo", "* and !post/*"))], ["bar", "foo", "foo2", "foo3"]); +is_deeply([sort(pagespec_match_list("foo", "post/*"))], ["post/1", "post/2", "post/3"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title")], + ["post/1", "post/2", "post/3"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title", reverse => 1)], + ["post/3", "post/2", "post/1"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title", num => 2)], + ["post/1", "post/2"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title", num => 50)], + ["post/1", "post/2", "post/3"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title", num => 50, reverse => 1)], + ["post/3", "post/2", "post/1"]); +is_deeply([pagespec_match_list("foo", "post/*", sort => "title", + filter => sub { $_[0] =~ /3/}) ], + ["post/1", "post/2"]); +is_deeply([pagespec_match_list("foo", "*", sort => "raw_path", num => 2)], + ["bar", "foo"]); +is_deeply([pagespec_match_list("foo", "foo* or bar*", + sort => "-age title")], # oldest first, break ties by title + ["foo3", "foo", "foo2", "bar"]); +my $r=eval { pagespec_match_list("foo", "beep") }; +ok(eval { pagespec_match_list("foo", "beep") } == 0); +ok(! $@, "does not fail with error when unable to match anything"); +eval { pagespec_match_list("foo", "this is not a legal pagespec!") }; +ok($@, "fails with error when pagespec bad"); + +# A pagespec that requires page metadata should add influences +# as an explicit dependency. In the case of a link, a links dependency. +foreach my $spec ("* and link(bar)", "* or link(bar)") { + pagespec_match_list("foo2", $spec, deptype => deptype("presence")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_PRESENCE); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))); + ok($IkiWiki::depends_simple{foo2}{foo2} == $IkiWiki::DEPEND_LINKS); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); + pagespec_match_list("foo3", $spec, deptype => deptype("links")); + ok($IkiWiki::depends{foo3}{$spec} & $IkiWiki::DEPEND_LINKS); + ok(! ($IkiWiki::depends{foo3}{$spec} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_PRESENCE))); + ok($IkiWiki::depends_simple{foo3}{foo3} == $IkiWiki::DEPEND_LINKS); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +# A link pagespec is influenced by the pages that currently contain the link. +# It is not influced by pages that do not currently contain the link, +# because if those pages were changed to contain it, regular dependency +# handling would be triggered. +foreach my $spec ("* and link(bar)", "link(bar)", "no_such_page or link(bar)") { + pagespec_match_list("foo2", $spec); + ok($IkiWiki::depends_simple{foo2}{foo2} == $IkiWiki::DEPEND_LINKS); + ok(! exists $IkiWiki::depends_simple{foo2}{foo}, $spec); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +# Oppositely, a pagespec that tests for pages that do not have a link +# is not influenced by pages that currently contain the link, but +# is instead influenced by pages that currently do not (but that +# could be changed to have it). +foreach my $spec ("* and !link(bar)", "* and !(!(!link(bar)))") { + pagespec_match_list("foo2", $spec); + ok(! exists $IkiWiki::depends_simple{foo2}{foo2}); + ok($IkiWiki::depends_simple{foo2}{foo} == $IkiWiki::DEPEND_LINKS, $spec); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +# a pagespec with backlinks() will add as an influence the page with the links +foreach my $spec ("bar or (backlink(foo) and !*.png)", "backlink(foo)", "!backlink(foo)") { + pagespec_match_list("foo2", $spec, deptype => deptype("presence")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_PRESENCE); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))); + ok($IkiWiki::depends_simple{foo2}{foo} == $IkiWiki::DEPEND_LINKS); + ok(! exists $IkiWiki::depends_simple{foo2}{foo2}); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); + pagespec_match_list("foo2", $spec, deptype => deptype("links")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_LINKS); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_CONTENT))); + ok($IkiWiki::depends_simple{foo2}{foo} == $IkiWiki::DEPEND_LINKS); + ok(! exists $IkiWiki::depends_simple{foo2}{foo2}); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); + pagespec_match_list("foo2", $spec, deptype => deptype("presence", "links")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_PRESENCE); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_LINKS); + ok(! ($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_CONTENT)); + ok($IkiWiki::depends_simple{foo2}{foo} == $IkiWiki::DEPEND_LINKS); + ok(! exists $IkiWiki::depends_simple{foo2}{foo2}); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); + pagespec_match_list("foo2", $spec); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_CONTENT); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_PRESENCE | $IkiWiki::DEPEND_LINKS))); + ok($IkiWiki::depends_simple{foo2}{foo} == $IkiWiki::DEPEND_LINKS); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +# Hard fails due to a glob, etc, will block influences of other anded terms. +foreach my $spec ("nosuchpage and link(bar)", "link(bar) and nosuchpage", + "link(bar) and */Discussion", "*/Discussion and link(bar)", + "!foo2 and link(bar)", "link(bar) and !foo2") { + pagespec_match_list("foo2", $spec, deptype => deptype("presence")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_PRESENCE); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))); + ok(! exists $IkiWiki::depends_simple{foo2}{foo2}, "no influence from $spec"); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +# A hard fail will not block influences of other ored terms. +foreach my $spec ("nosuchpage or link(bar)", "link(bar) or nosuchpage", + "link(bar) or */Discussion", "*/Discussion or link(bar)", + "!foo2 or link(bar)", "link(bar) or !foo2", + "link(bar) or (!foo2 and !foo1)") { + pagespec_match_list("foo2", $spec, deptype => deptype("presence")); + ok($IkiWiki::depends{foo2}{$spec} & $IkiWiki::DEPEND_PRESENCE); + ok(! ($IkiWiki::depends{foo2}{$spec} & ($IkiWiki::DEPEND_CONTENT | $IkiWiki::DEPEND_LINKS))); + ok($IkiWiki::depends_simple{foo2}{foo2} == $IkiWiki::DEPEND_LINKS); + %IkiWiki::depends_simple=(); + %IkiWiki::depends=(); +} + +my @ps; +foreach my $p (100..500) { + $IkiWiki::pagectime{"p/$p"} = $p; + $pagesources{"p/$p"} = "p/$p.mdwn"; + unshift @ps, "p/$p"; +} +is_deeply([pagespec_match_list("foo", "p/*", sort => "age")], + [@ps]); +is_deeply([pagespec_match_list("foo", "p/*", sort => "age", num => 20)], + [@ps[0..19]]); diff --git a/t/pagespec_match_result.t b/t/pagespec_match_result.t new file mode 100755 index 000000000..13fcdcad0 --- /dev/null +++ b/t/pagespec_match_result.t @@ -0,0 +1,84 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 138; + +BEGIN { use_ok("IkiWiki"); } + +# Note that new objects have to be constructed freshly for each test, since +# object states are mutated as they are combined. +sub S { IkiWiki::SuccessReason->new("match", @_) } +sub F { IkiWiki::FailReason->new("no match", @_) } +sub E { IkiWiki::ErrorReason->new("error in matching", @_) } + +ok(S() eq "match"); +ok(F() eq "no match"); +ok(E() eq "error in matching"); + +ok(S()); +ok(! F()); +ok(! E()); + +ok(!(! S())); +ok(!(!(! F))); +ok(!(!(! E))); + +ok(S() | F()); +ok(F() | S()); +ok(!(F() | E())); +ok(!(!S() | F() | E())); + +ok(S() & S() & S()); +ok(!(S() & E())); +ok(!(S() & F())); +ok(!(S() & F() & E())); +ok(S() & (F() | F() | S())); + +# influence merging tests +foreach my $test ( + ['$s | $f' => 1], # OR merges + ['! $s | ! $f' => 1], # OR merges with negated terms too + ['!(!(!$s)) | $f' => 1],# OR merges with multiple negation too + ['$s | $f | E()' => 1], # OR merges, even though E() has no influences + ['$s | E() | $f' => 1], # ditto + ['E() | $s | $f' => 1], # ditto + ['!$s | !$f | E()' => 1],# negated terms also do not block merges + ['!$s | E() | $f' => 1],# ditto + ['E() | $s | !$f' => 1],# ditto + ['$s & $f' => 1], # AND merges if both items have influences + ['!$s & $f' => 1], # AND merges negated terms too + ['$s & !$f' => 1], # AND merges negated terms too + ['$s & $f & E()' => 0], # AND fails to merge since E() has no influences + ['$s & E() & $f' => 0], # ditto + ['E() & $s & $f' => 0], # ditto + ) { + my $op=$test->[0]; + my $influence=$test->[1]; + + my $s=S(foo => 1, bar => 1); + is($s->influences->{foo}, 1); + is($s->influences->{bar}, 1); + my $f=F(bar => 2, baz => 1); + is($f->influences->{bar}, 2); + is($f->influences->{baz}, 1); + my $c = eval $op; + ok(ref $c); + if ($influence) { + is($c->influences->{foo}, 1, "foo ($op)"); + is($c->influences->{bar}, (1 | 2), "bar ($op)"); + is($c->influences->{baz}, 1, "baz ($op)"); + } + else { + ok(! %{$c->influences}, "no influence for ($op)"); + } +} + +my $s=S(foo => 0, bar => 1); +$s->influences(baz => 1); +ok(! $s->influences->{foo}, "removed 0 influence"); +ok(! $s->influences->{bar}, "removed 1 influence"); +ok($s->influences->{baz}, "set influence"); +ok($s->influences_static); +$s=S(foo => 0, bar => 1); +$s->influences(baz => 1, "" => 1); +ok(! $s->influences_static); diff --git a/t/pagetitle.t b/t/pagetitle.t new file mode 100755 index 000000000..d9aa62063 --- /dev/null +++ b/t/pagetitle.t @@ -0,0 +1,13 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 7; + +BEGIN { use_ok("IkiWiki"); } + +is(pagetitle("foo_bar"), "foo bar"); +is(pagetitle("foo_bar_baz"), "foo bar baz"); +is(pagetitle("foo_bar__33__baz"), "foo bar!baz"); +is(pagetitle("foo_bar__1234__baz"), "foo barӒbaz"); +is(pagetitle("foo_bar___33___baz"), "foo bar ! baz"); +is(pagetitle("foo_bar___95___baz"), "foo bar _ baz"); diff --git a/t/parentlinks.t b/t/parentlinks.t new file mode 100755 index 000000000..9b4654903 --- /dev/null +++ b/t/parentlinks.t @@ -0,0 +1,81 @@ +#!/usr/bin/perl +# -*- cperl-indent-level: 8; -*- +# Testcases for the Ikiwiki parentlinks plugin. + +use warnings; +use strict; +use Test::More 'no_plan'; + +my %expected; + +BEGIN { use_ok("IkiWiki"); } + +# Init +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +$config{underlaydir}="underlays/basewiki"; +$config{templatedir}="t/parentlinks/templates"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +# Test data +$expected{'parentlinks'} = + { + "ikiwiki" => [], + "ikiwiki/pagespec" => + [ {depth => 0, height => 2, }, + {depth => 1, height => 1, }, + ], + "ikiwiki/pagespec/attachment" => + [ {depth => 0, height => 3, depth_0 => 1, height_3 => 1}, + {depth => 1, height => 2, }, + {depth => 2, height => 1, }, + ], + }; + +# Test function +sub test_loop($$) { + my $loop=shift; + my $expected=shift; + my $template; + my %params; + + ok($template=template('parentlinks.tmpl'), "template created"); + ok($params{template}=$template, "params populated"); + + while ((my $page, my $exp) = each %{$expected}) { + my @path=(split("/", $page)); + my $pagedepth=@path; + my $msgprefix="$page $loop"; + + # manually run the plugin hook + $params{page}=$page; + $template->clear_params(); + IkiWiki::Plugin::parentlinks::pagetemplate(%params); + my $res=$template->param($loop); + + is(scalar(@$res), $pagedepth, "$msgprefix: path length"); + # logic & arithmetic validation tests + for (my $i=0; $i<$pagedepth; $i++) { + my $r=$res->[$i]; + is($r->{height}, $pagedepth - $r->{depth}, + "$msgprefix\[$i\]: height = pagedepth - depth"); + ok($r->{depth} ge 0, "$msgprefix\[$i\]: depth>=0"); + ok($r->{height} ge 0, "$msgprefix\[$i\]: height>=0"); + } + # comparison tests, iff the test-suite has been written + if (scalar(@$exp) eq $pagedepth) { + for (my $i=0; $i<$pagedepth; $i++) { + my $e=$exp->[$i]; + my $r=$res->[$i]; + map { is($r->{$_}, $e->{$_}, "$msgprefix\[$i\]: $_"); } keys %$e; + } + } + # else { + # diag("Testsuite is incomplete for ($page,$loop); cannot run comparison tests."); + # } + } +} + +# Main +test_loop('parentlinks', $expected{'parentlinks'}); diff --git a/t/parentlinks/templates/parentlinks.tmpl b/t/parentlinks/templates/parentlinks.tmpl new file mode 100644 index 000000000..3ca3b0030 --- /dev/null +++ b/t/parentlinks/templates/parentlinks.tmpl @@ -0,0 +1,4 @@ +<!-- This template file only has to "use" the loops tested by parentlinks.t --> + +<TMPL_LOOP NAME="PARENTLINKS"> +</TMPL_LOOP> diff --git a/t/permalink.t b/t/permalink.t new file mode 100755 index 000000000..36be984c5 --- /dev/null +++ b/t/permalink.t @@ -0,0 +1,14 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More 'no_plan'; + +ok(! system("rm -rf t/tmp")); +ok(! system("mkdir t/tmp")); +ok(! system("make -s ikiwiki.out")); +ok(! system("perl -I. ./ikiwiki.out -plugin inline -url=http://example.com -cgiurl=http://example.com/ikiwiki.cgi -rss -atom -underlaydir=underlays/basewiki -set underlaydirbase=underlays -templatedir=templates t/tinyblog t/tmp/out")); +# This guid should never, ever change, for any reason whatsoever! +my $guid="http://example.com/post/"; +ok(length `egrep '<guid.*>$guid</guid>' t/tmp/out/index.rss`); +ok(length `egrep '<id>$guid</id>' t/tmp/out/index.atom`); +ok(! system("rm -rf t/tmp t/tinyblog/.ikiwiki")); @@ -0,0 +1,250 @@ +#!/usr/bin/perl +# -*- cperl-indent-level: 8; -*- +use warnings; +use strict; +use File::Temp qw{tempdir}; + +BEGIN { + unless (eval { require Locale::Po4a::Chooser }) { + eval q{ + use Test::More skip_all => "Locale::Po4a::Chooser::new is not available" + } + } + unless (eval { require Locale::Po4a::Po }) { + eval q{ + use Test::More skip_all => "Locale::Po4a::Po::new is not available" + } + } +} + +use Test::More tests => 114; + +BEGIN { use_ok("IkiWiki"); } + +my $msgprefix; + +my $dir = tempdir("ikiwiki-test-po.XXXXXXXXXX", + DIR => File::Spec->tmpdir, + CLEANUP => 1); + +### Init +%config=IkiWiki::defaultconfig(); +$config{srcdir} = "$dir/src"; +$config{destdir} = "$dir/dst"; +$config{destdir} = "$dir/dst"; +$config{underlaydirbase} = "/dev/null"; +$config{underlaydir} = "/dev/null"; +$config{url} = "http://example.com"; +$config{cgiurl} = "http://example.com/ikiwiki.cgi"; +$config{discussion} = 0; +$config{po_master_language} = { code => 'en', + name => 'English' + }; +$config{po_slave_languages} = { + es => 'Castellano', + fr => "Français" + }; +$config{po_translatable_pages}='index or test1 or test2 or translatable'; +$config{po_link_to}='negotiated'; +IkiWiki::loadplugins(); +ok(IkiWiki::loadplugin('meta'), "meta plugin loaded"); +ok(IkiWiki::loadplugin('po'), "po plugin loaded"); +IkiWiki::checkconfig(); + +### seed %pagesources and %pagecase +$pagesources{'index'}='index.mdwn'; +$pagesources{'index.fr'}='index.fr.po'; +$pagesources{'index.es'}='index.es.po'; +$pagesources{'test1'}='test1.mdwn'; +$pagesources{'test1.es'}='test1.es.po'; +$pagesources{'test1.fr'}='test1.fr.po'; +$pagesources{'test2'}='test2.mdwn'; +$pagesources{'test2.es'}='test2.es.po'; +$pagesources{'test2.fr'}='test2.fr.po'; +$pagesources{'test3'}='test3.mdwn'; +$pagesources{'test3.es'}='test3.es.mdwn'; +$pagesources{'translatable'}='translatable.mdwn'; +$pagesources{'translatable.fr'}='translatable.fr.po'; +$pagesources{'translatable.es'}='translatable.es.po'; +$pagesources{'nontranslatable'}='nontranslatable.mdwn'; +foreach my $page (keys %pagesources) { + $IkiWiki::pagecase{lc $page}=$page; +} + +### populate srcdir +writefile('index.mdwn', $config{srcdir}, + "[[!meta title=\"index title\"]]\n[[translatable]] [[nontranslatable]]"); +writefile('test1.mdwn', $config{srcdir}, + "[[!meta title=\"test1 title\"]]\ntest1 content"); +writefile('test2.mdwn', $config{srcdir}, 'test2 content'); +writefile('test3.mdwn', $config{srcdir}, 'test3 content'); +writefile('translatable.mdwn', $config{srcdir}, '[[nontranslatable]]'); +writefile('nontranslatable.mdwn', $config{srcdir}, '[[/]] [[translatable]]'); + +### istranslatable/istranslation +# we run these tests twice because memoization attempts made them +# succeed once every two tries... +foreach (1, 2) { +ok(IkiWiki::Plugin::po::istranslatable('index'), "index is translatable"); +ok(IkiWiki::Plugin::po::istranslatable('/index'), "/index is translatable"); +ok(! IkiWiki::Plugin::po::istranslatable('index.fr'), "index.fr is not translatable"); +ok(! IkiWiki::Plugin::po::istranslatable('index.es'), "index.es is not translatable"); +ok(! IkiWiki::Plugin::po::istranslatable('/index.fr'), "/index.fr is not translatable"); +ok(! IkiWiki::Plugin::po::istranslation('index'), "index is not a translation"); +ok(IkiWiki::Plugin::po::istranslation('index.fr'), "index.fr is a translation"); +ok(IkiWiki::Plugin::po::istranslation('index.es'), "index.es is a translation"); +ok(IkiWiki::Plugin::po::istranslation('/index.fr'), "/index.fr is a translation"); +ok(IkiWiki::Plugin::po::istranslatable('test1'), "test1 is translatable"); +ok(IkiWiki::Plugin::po::istranslation('test1.es'), "test1.es is a translation"); +ok(IkiWiki::Plugin::po::istranslation('test1.fr'), "test1.fr is a translation"); +ok(IkiWiki::Plugin::po::istranslatable('test2'), "test2 is translatable"); +ok(! IkiWiki::Plugin::po::istranslation('test2'), "test2 is not a translation"); +ok(! IkiWiki::Plugin::po::istranslatable('test3'), "test3 is not translatable"); +ok(! IkiWiki::Plugin::po::istranslation('test3'), "test3 is not a translation"); +} + +### pofiles + +my @pofiles = IkiWiki::Plugin::po::pofiles(srcfile("index.mdwn")); +ok( @pofiles, "pofiles is defined"); +ok( @pofiles == 2, "pofiles has correct size"); +is_deeply(\@pofiles, ["$config{srcdir}/index.es.po", "$config{srcdir}/index.fr.po"], "pofiles content is correct"); + +### links +require IkiWiki::Render; + +sub refresh_n_scan(@) { + my @masterfiles_rel=@_; + foreach my $masterfile_rel (@masterfiles_rel) { + my $masterfile=srcfile($masterfile_rel); + IkiWiki::scan($masterfile_rel); + next unless IkiWiki::Plugin::po::istranslatable(pagename($masterfile_rel)); + my @pofiles=IkiWiki::Plugin::po::pofiles($masterfile); + IkiWiki::Plugin::po::refreshpot($masterfile); + IkiWiki::Plugin::po::refreshpofiles($masterfile, @pofiles); + map IkiWiki::scan(IkiWiki::abs2rel($_, $config{srcdir})), @pofiles; + } +} + +$config{po_link_to}='negotiated'; +$msgprefix="links (po_link_to=negotiated)"; +refresh_n_scan('index.mdwn', 'translatable.mdwn', 'nontranslatable.mdwn'); +is_deeply(\@{$links{'index'}}, ['translatable', 'nontranslatable'], "$msgprefix index"); +is_deeply(\@{$links{'index.es'}}, ['translatable.es', 'nontranslatable'], "$msgprefix index.es"); +is_deeply(\@{$links{'index.fr'}}, ['translatable.fr', 'nontranslatable'], "$msgprefix index.fr"); +is_deeply(\@{$links{'translatable'}}, ['nontranslatable'], "$msgprefix translatable"); +is_deeply(\@{$links{'translatable.es'}}, ['nontranslatable'], "$msgprefix translatable.es"); +is_deeply(\@{$links{'translatable.fr'}}, ['nontranslatable'], "$msgprefix translatable.fr"); +is_deeply(\@{$links{'nontranslatable'}}, ['/', 'translatable', 'translatable.fr', 'translatable.es'], "$msgprefix nontranslatable"); + +$config{po_link_to}='current'; +$msgprefix="links (po_link_to=current)"; +refresh_n_scan('index.mdwn', 'translatable.mdwn', 'nontranslatable.mdwn'); +is_deeply(\@{$links{'index'}}, ['translatable', 'nontranslatable'], "$msgprefix index"); +is_deeply(\@{$links{'index.es'}}, [ (map bestlink('index.es', $_), ('translatable.es', 'nontranslatable'))], "$msgprefix index.es"); +is_deeply(\@{$links{'index.fr'}}, [ (map bestlink('index.fr', $_), ('translatable.fr', 'nontranslatable'))], "$msgprefix index.fr"); +is_deeply(\@{$links{'translatable'}}, [bestlink('translatable', 'nontranslatable')], "$msgprefix translatable"); +is_deeply(\@{$links{'translatable.es'}}, ['nontranslatable'], "$msgprefix translatable.es"); +is_deeply(\@{$links{'translatable.fr'}}, ['nontranslatable'], "$msgprefix translatable.fr"); +is_deeply(\@{$links{'nontranslatable'}}, ['/', 'translatable', 'translatable.fr', 'translatable.es'], "$msgprefix nontranslatable"); + +### targetpage +$config{usedirs}=0; +$msgprefix="targetpage (usedirs=0)"; +is(targetpage('test1', 'html'), 'test1.en.html', "$msgprefix test1"); +is(targetpage('test1.fr', 'html'), 'test1.fr.html', "$msgprefix test1.fr"); +$config{usedirs}=1; +$msgprefix="targetpage (usedirs=1)"; +is(targetpage('index', 'html'), 'index.en.html', "$msgprefix index"); +is(targetpage('index.fr', 'html'), 'index.fr.html', "$msgprefix index.fr"); +is(targetpage('test1', 'html'), 'test1/index.en.html', "$msgprefix test1"); +is(targetpage('test1.fr', 'html'), 'test1/index.fr.html', "$msgprefix test1.fr"); +is(targetpage('test3', 'html'), 'test3/index.html', "$msgprefix test3 (non-translatable page)"); +is(targetpage('test3.es', 'html'), 'test3.es/index.html', "$msgprefix test3.es (non-translatable page)"); + +### urlto -> index +$config{po_link_to}='current'; +$msgprefix="urlto (po_link_to=current)"; +is(urlto('', 'index'), './index.en.html', "$msgprefix index -> ''"); +is(urlto('', 'nontranslatable'), '../index.en.html', "$msgprefix nontranslatable -> ''"); +is(urlto('', 'translatable.fr'), '../index.fr.html', "$msgprefix translatable.fr -> ''"); +# when asking for a semi-absolute or absolute URL, we can't know what the +# current language is, so for translatable pages we use the master language +is(urlto('nontranslatable'), '/nontranslatable/', "$msgprefix 1-arg -> nontranslatable"); +is(urlto('translatable'), '/translatable/index.en.html', "$msgprefix 1-arg -> translatable"); +is(urlto('nontranslatable', undef, 1), 'http://example.com/nontranslatable/', "$msgprefix 1-arg -> nontranslatable"); +is(urlto('index', undef, 1), 'http://example.com/index.en.html', "$msgprefix 1-arg -> index"); +is(urlto('', undef, 1), 'http://example.com/index.en.html', "$msgprefix 1-arg -> ''"); +# FIXME: should these three produce the negotiatable URL instead of the master +# language? +is(urlto(''), '/index.en.html', "$msgprefix 1-arg -> ''"); +is(urlto('index'), '/index.en.html', "$msgprefix 1-arg -> index"); +is(urlto('translatable', undef, 1), 'http://example.com/translatable/index.en.html', "$msgprefix 1-arg -> translatable"); + +$config{po_link_to}='negotiated'; +$msgprefix="urlto (po_link_to=negotiated)"; +is(urlto('', 'index'), './', "$msgprefix index -> ''"); +is(urlto('', 'nontranslatable'), '../', "$msgprefix nontranslatable -> ''"); +is(urlto('', 'translatable.fr'), '../', "$msgprefix translatable.fr -> ''"); +is(urlto('nontranslatable'), '/nontranslatable/', "$msgprefix 1-arg -> nontranslatable"); +is(urlto('translatable'), '/translatable/', "$msgprefix 1-arg -> translatable"); +is(urlto(''), '/', "$msgprefix 1-arg -> ''"); +is(urlto('index'), '/', "$msgprefix 1-arg -> index"); +is(urlto('nontranslatable', undef, 1), 'http://example.com/nontranslatable/', "$msgprefix 1-arg -> nontranslatable"); +is(urlto('translatable', undef, 1), 'http://example.com/translatable/', "$msgprefix 1-arg -> translatable"); +is(urlto('index', undef, 1), 'http://example.com/', "$msgprefix 1-arg -> index"); +is(urlto('', undef, 1), 'http://example.com/', "$msgprefix 1-arg -> ''"); + +### bestlink +$config{po_link_to}='current'; +$msgprefix="bestlink (po_link_to=current)"; +is(bestlink('test1.fr', 'test2'), 'test2.fr', "$msgprefix test1.fr -> test2"); +is(bestlink('test1.fr', 'test2.es'), 'test2.es', "$msgprefix test1.fr -> test2.es"); +$config{po_link_to}='negotiated'; +$msgprefix="bestlink (po_link_to=negotiated)"; +is(bestlink('test1.fr', 'test2'), 'test2.fr', "$msgprefix test1.fr -> test2"); +is(bestlink('test1.fr', 'test2.es'), 'test2.es', "$msgprefix test1.fr -> test2.es"); + +### beautify_urlpath +$config{po_link_to}='default'; +$msgprefix="beautify_urlpath (po_link_to=default)"; +is(IkiWiki::beautify_urlpath('test1/index.en.html'), './test1/index.en.html', "$msgprefix test1/index.en.html"); +is(IkiWiki::beautify_urlpath('test1/index.fr.html'), './test1/index.fr.html', "$msgprefix test1/index.fr.html"); +$config{po_link_to}='negotiated'; +$msgprefix="beautify_urlpath (po_link_to=negotiated)"; +is(IkiWiki::beautify_urlpath('test1/index.html'), './test1/', "$msgprefix test1/index.html"); +is(IkiWiki::beautify_urlpath('test1/index.en.html'), './test1/', "$msgprefix test1/index.en.html"); +is(IkiWiki::beautify_urlpath('test1/index.fr.html'), './test1/', "$msgprefix test1/index.fr.html"); +$config{po_link_to}='current'; +$msgprefix="beautify_urlpath (po_link_to=current)"; +is(IkiWiki::beautify_urlpath('test1/index.en.html'), './test1/index.en.html', "$msgprefix test1/index.en.html"); +is(IkiWiki::beautify_urlpath('test1/index.fr.html'), './test1/index.fr.html', "$msgprefix test1/index.fr.html"); + +### re-scan +refresh_n_scan('index.mdwn'); +is($pagestate{'index'}{meta}{title}, 'index title'); +is($pagestate{'index.es'}{meta}{title}, 'index title'); +is($pagestate{'index.fr'}{meta}{title}, 'index title'); +refresh_n_scan('test1.mdwn'); +is($pagestate{'test1'}{meta}{title}, 'test1 title'); +is($pagestate{'test1.es'}{meta}{title}, 'test1 title'); +is($pagestate{'test1.fr'}{meta}{title}, 'test1 title'); + +### istranslatedto +ok(IkiWiki::Plugin::po::istranslatedto('index', 'es')); +ok(IkiWiki::Plugin::po::istranslatedto('index', 'fr')); +ok(! IkiWiki::Plugin::po::istranslatedto('index', 'cz')); +ok(IkiWiki::Plugin::po::istranslatedto('test1', 'es')); +ok(IkiWiki::Plugin::po::istranslatedto('test1', 'fr')); +ok(! IkiWiki::Plugin::po::istranslatedto('test1', 'cz')); +ok(! IkiWiki::Plugin::po::istranslatedto('nontranslatable', 'es')); +ok(! IkiWiki::Plugin::po::istranslatedto('nontranslatable', 'cz')); +ok(! IkiWiki::Plugin::po::istranslatedto('test1.es', 'fr')); +ok(! IkiWiki::Plugin::po::istranslatedto('test1.fr', 'es')); + +### islanguagecode +ok(IkiWiki::Plugin::po::islanguagecode('en')); +ok(IkiWiki::Plugin::po::islanguagecode('es')); +ok(IkiWiki::Plugin::po::islanguagecode('arn')); +ok(! IkiWiki::Plugin::po::islanguagecode('es_')); +ok(! IkiWiki::Plugin::po::islanguagecode('_en')); diff --git a/t/preprocess.t b/t/preprocess.t new file mode 100755 index 000000000..2211e8471 --- /dev/null +++ b/t/preprocess.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 34; + +BEGIN { use_ok("IkiWiki"); } + +$IkiWiki::hooks{preprocess}{foo}{call}=sub { + my @bits; + while (@_) { + my $key=shift; + my $value=shift; + next if $key eq 'page' || $key eq 'destpage' || $key eq 'preview'; + if (length $value) { + push @bits, "$key => $value"; + } + else { + push @bits, $key; + } + } + return "foo(".join(", ", @bits).")"; +}; + +is(IkiWiki::preprocess("foo", "foo", "[[foo]]", 0, 0), "[[foo]]", "not wikilink"); +is(IkiWiki::preprocess("foo", "foo", "[[foo ]]", 0, 0), "foo()", "simple"); +is(IkiWiki::preprocess("foo", "foo", "[[!foo ]]", 0, 0), "foo()", "prefixed"); +is(IkiWiki::preprocess("foo", "foo", "[[!foo]]", 0, 0), "[[!foo]]", "prefixed, no space"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=1]]", 0, 0), "foo(a => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="1"]]}, 0, 0), "foo(a => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="""1"""]]}, 0, 0), "foo(a => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a=""]]}, 0, 0), "foo(a)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="" b="1"]]}, 0, 0), "foo(a, b => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a=""""""]]}, 0, 0), "foo(a)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="""""" b="1"]]}, 0, 0), "foo(a, b => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="""""" b="""1"""]]}, 0, 0), "foo(a, b => 1)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="""""" b=""""""]]}, 0, 0), "foo(a, b)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="" b=""""""]]}, 0, 0), "foo(a, b)"); +is(IkiWiki::preprocess("foo", "foo", q{[[foo a="" b="""1"""]]}, 0, 0), "foo(a, b => 1)"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=\"1 2 3 4\"]]", 0, 0), "foo(a => 1 2 3 4)"); +is(IkiWiki::preprocess("foo", "foo", "[[foo ]] then [[foo a=2]]", 0, 0), + "foo() then foo(a => 2)"); +is(IkiWiki::preprocess("foo", "foo", "[[foo b c \"d and e=f\"]]", 0, 0), "foo(b, c, d and e=f)"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=1 b c=1]]", 0, 0), + "foo(a => 1, b, c => 1)"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=1 b c=1 \t\t]]", 0, 0), + "foo(a => 1, b, c => 1)", "whitespace"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=1\nb \nc=1]]", 0, 0), + "foo(a => 1, b, c => 1)", "multiline directive"); +is(IkiWiki::preprocess("foo", "foo", "[[foo a=1 a=2 a=3]]", 0, 0), + "foo(a => 1, a => 2, a => 3)", "dup item"); +is(IkiWiki::preprocess("foo", "foo", '[[foo a="[[bracketed]]" b=1]]', 0, 0), + "foo(a => [[bracketed]], b => 1)"); +my $multiline="here is my \"first\" +!! [[multiline ]] !! +string!"; +is(IkiWiki::preprocess("foo", "foo", '[[foo a="""'.$multiline.'"""]]', 0, 0), + "foo(a => $multiline)"); +is(IkiWiki::preprocess("foo", "foo", '[[foo """'.$multiline.'"""]]', 0, 0), + "foo($multiline)"); +is(IkiWiki::preprocess("foo", "foo", '[[foo a="""'.$multiline.'""" b="foo"]]', 0, 0), + "foo(a => $multiline, b => foo)"); +is(IkiWiki::preprocess("foo", "foo", '[[foo a="""'."\n".$multiline."\n".'""" b="foo"]]', 0, 0), + "foo(a => $multiline, b => foo)", "leading/trailing newline stripped"); +my $long='[[foo a="""'.("a" x 100000).''; +is(IkiWiki::preprocess("foo", "foo", $long, 0, 0), $long, + "unterminated triple-quoted string inside unterminated directive(should not warn about over-recursion)"); +is(IkiWiki::preprocess("foo", "foo", $long."]]", 0, 0), $long."]]", + "unterminated triple-quoted string is not treated as a bare word"); + +is(IkiWiki::preprocess("foo", "foo", "[[!foo a=<<HEREDOC\n".$multiline."\nHEREDOC]]", 0, 0), + "foo(a => $multiline)", "nested strings via heredoc (for key)"); +is(IkiWiki::preprocess("foo", "foo", "[[!foo <<HEREDOC\n".$multiline."\nHEREDOC]]", 0, 0), + "foo($multiline)", "nested strings via heredoc (without keyless)"); +is(IkiWiki::preprocess("foo", "foo", "[[!foo '''".$multiline."''']]", 0, 0), + "foo($multiline)", "triple-single-quoted multiline string"); + +TODO: { + local $TODO = "nested strings not yet implemented"; + + $multiline='here is a string containing another [[foo val="""string""]]'; + is(IkiWiki::preprocess("foo", "foo", '[[foo a="""'.$multiline.'"""]]', 0, 0), + "foo(a => $multiline)", "nested multiline strings"); +} diff --git a/t/prune.t b/t/prune.t new file mode 100755 index 000000000..8c3925e9e --- /dev/null +++ b/t/prune.t @@ -0,0 +1,23 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 6; +use File::Path qw(make_path remove_tree); + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } + +%config=IkiWiki::defaultconfig(); + +remove_tree("t/tmp"); + +make_path("t/tmp/srcdir/a/b/c"); +make_path("t/tmp/srcdir/d/e/f"); +writefile("a/b/c/d.mdwn", "t/tmp/srcdir", "foo"); +writefile("d/e/f/g.mdwn", "t/tmp/srcdir", "foo"); +IkiWiki::prune("t/tmp/srcdir/d/e/f/g.mdwn"); +ok(-d "t/tmp/srcdir"); +ok(! -e "t/tmp/srcdir/d"); +IkiWiki::prune("t/tmp/srcdir/a/b/c/d.mdwn", "t/tmp/srcdir"); +ok(-d "t/tmp/srcdir"); +ok(! -e "t/tmp/srcdir/a"); diff --git a/t/readfile.t b/t/readfile.t new file mode 100755 index 000000000..bb1c6bae3 --- /dev/null +++ b/t/readfile.t @@ -0,0 +1,12 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 3; +use Encode; + +BEGIN { use_ok("IkiWiki"); } + +# should read files as utf8 +ok(Encode::is_utf8(readfile("t/test1.mdwn"), 1)); +is(readfile("t/test1.mdwn"), + Encode::decode_utf8('![o](../images/o.jpg "ó")'."\n".'óóóóó'."\n")); diff --git a/t/renamepage.t b/t/renamepage.t new file mode 100755 index 000000000..a706cbb46 --- /dev/null +++ b/t/renamepage.t @@ -0,0 +1,51 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 21; +use Encode; + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Plugin::link"); } + +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +IkiWiki::checkconfig(); + +# tests of the link plugin's renamepage function +sub try { + my ($page, $oldpage, $newpage, $content)=@_; + + %IkiWiki::pagecase=(); + %links=(); + $IkiWiki::config{userdir}="foouserdir"; + foreach my $page ($page, $oldpage, $newpage) { + $IkiWiki::pagecase{lc $page}=$page; + $links{$page}=[]; + } + + IkiWiki::Plugin::link::renamepage( + page => $page, + oldpage => $oldpage, + newpage => $newpage, + content => $content, + ); +} +is(try("z", "foo" => "bar", "[[xxx]]"), "[[xxx]]"); # unrelated link +is(try("z", "foo" => "bar", "[[bar]]"), "[[bar]]"); # link already to new page +is(try("z", "foo" => "bar", "[[foo]]"), "[[bar]]"); # basic conversion to new page name +is(try("z", "foo" => "bar", "[[/foo]]"), "[[/bar]]"); # absolute link +is(try("z", "foo" => "bar", "[[Foo]]"), "[[Bar]]"); # preserve case +is(try("z", "x/foo" => "x/bar", "[[x/Foo]]"), "[[x/Bar]]"); # preserve case of subpage +is(try("z", "foo" => "bar", "[[/Foo]]"), "[[/Bar]]"); # preserve case w/absolute +is(try("z", "foo" => "bar", "[[foo]] [[xxx]]"), "[[bar]] [[xxx]]"); # 2 links, 1 converted +is(try("z", "foo" => "bar", "[[xxx|foo]]"), "[[xxx|bar]]"); # conversion w/text +is(try("z", "foo" => "bar", "[[foo#anchor]]"), "[[bar#anchor]]"); # with anchor +is(try("z", "foo" => "bar", "[[xxx|foo#anchor]]"), "[[xxx|bar#anchor]]"); # with anchor +is(try("z", "foo" => "bar", "[[!moo ]]"), "[[!moo ]]"); # preprocessor directive unchanged +is(try("bugs", "bugs/foo" => "wishlist/bar", "[[foo]]"), "[[wishlist/bar]]"); # subpage link +is(try("z", "foo_bar" => "bar", "[[foo_bar]]"), "[[bar]]"); # old link with underscore +is(try("z", "foo" => "bar_foo", "[[foo]]"), "[[bar_foo]]"); # new link with underscore +is(try("z", "foo_bar" => "bar_foo", "[[foo_bar]]"), "[[bar_foo]]"); # both with underscore +is(try("z", "foo" => "bar__".ord("(")."__", "[[foo]]"), "[[bar(]]"); # new link with escaped chars +is(try("z", "foo__".ord("(")."__" => "bar(", "[[foo(]]"), "[[bar(]]"); # old link with escaped chars +is(try("z", "foo__".ord("(")."__" => "bar__".ord(")")."__", "[[foo(]]"), "[[bar)]]"); # both with escaped chars diff --git a/t/rssurls.t b/t/rssurls.t new file mode 100755 index 000000000..870770496 --- /dev/null +++ b/t/rssurls.t @@ -0,0 +1,37 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 13; + +BEGIN { use_ok("IkiWiki::Plugin::inline"); } + +# Test the absolute_urls function, used to fix up relative urls for rss +# feeds. +sub test { + my $input=shift; + my $baseurl=shift; + my $expected=shift; + $expected=~s/URL/$baseurl/g; + is(IkiWiki::absolute_urls($input, $baseurl), $expected); + # try it with single quoting -- it's ok if the result comes back + # double or single-quoted + $input=~s/"/'/g; + my $expected_alt=$expected; + $expected_alt=~s/"/'/g; + my $ret=IkiWiki::absolute_urls($input, $baseurl); + ok(($ret eq $expected) || ($ret eq $expected_alt), "$ret vs $expected"); +} + +sub unchanged { + test($_[0], $_[1], $_[0]); +} + +my $url="http://example.com/blog/foo/"; +unchanged("foo", $url); +unchanged('<a href="http://other.com/bar.html">', $url, ); +test('<a href="bar.html">', $url, '<a href="URLbar.html">'); +test('<a href="/bar.html">', $url, '<a href="http://example.com/bar.html">'); +test('<img src="bar.png" />', $url, '<img src="URLbar.png" />'); +test('<img src="/bar.png" />', $url, '<img src="http://example.com/bar.png" />'); +# off until bug #603736 is fixed +#test('<video controls src="bar.ogg">', $url, '<video controls src="URLbar.ogg">'); diff --git a/t/rst.t b/t/rst.t new file mode 100755 index 000000000..4e0c4b747 --- /dev/null +++ b/t/rst.t @@ -0,0 +1,22 @@ +#!/usr/bin/perl +use warnings; +use strict; + +BEGIN { + if (system("python -c 'import docutils.core'") != 0) { + eval 'use Test::More skip_all => "docutils not available"'; + } +} + +use Test::More tests => 2; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{srcdir}=$config{destdir}="/dev/null"; +$config{libdir}="."; +$config{add_plugins}=[qw(rst)]; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +ok(IkiWiki::htmlize("foo", "foo", "rst", "foo\n") =~ m{\s*<p>foo</p>\s*}); diff --git a/t/svn.t b/t/svn.t new file mode 100755 index 000000000..cce8452a6 --- /dev/null +++ b/t/svn.t @@ -0,0 +1,78 @@ +#!/usr/bin/perl +use warnings; +use strict; +my $dir; +BEGIN { + $dir="/tmp/ikiwiki-test-svn.$$"; + my $svn=`which svn`; + chomp $svn; + my $svnadmin=`which svnadmin`; + chomp $svnadmin; + if (! -x $svn || ! -x $svnadmin) { + eval q{ + use Test::More skip_all => "svn or svnadmin not available" + } + } + if (! mkdir($dir)) { + die $@; + } +} +use Test::More tests => 12; + +BEGIN { use_ok("IkiWiki"); } + +%config=IkiWiki::defaultconfig(); +$config{rcs} = "svn"; +$config{srcdir} = "$dir/src"; +$config{svnrepo} = "$dir/repo"; +$config{svnpath} = "trunk"; +IkiWiki::loadplugins(); +IkiWiki::checkconfig(); + +my $svnrepo = "$dir/repo"; + +system "svnadmin create $svnrepo >/dev/null"; +system "svn mkdir file://$svnrepo/trunk -m add >/dev/null"; +system "svn co file://$svnrepo/trunk $config{srcdir} >/dev/null"; + +# Web commit +my $test1 = readfile("t/test1.mdwn"); +writefile('test1.mdwn', $config{srcdir}, $test1); +IkiWiki::rcs_add("test1.mdwn"); +IkiWiki::rcs_commit( + file => "test1.mdwn", + message => "Added the first page", + token => "moo", +); + +my @changes; +@changes = IkiWiki::rcs_recentchanges(3); + +is($#changes, 0); +is($changes[0]{message}[0]{"line"}, "Added the first page"); +is($changes[0]{pages}[0]{"page"}, "test1"); + +# Manual commit +my $message = "Added the second page"; + +my $test2 = readfile("t/test2.mdwn"); +writefile('test2.mdwn', $config{srcdir}, $test2); +system "svn add $config{srcdir}/test2.mdwn >/dev/null"; +system "svn commit $config{srcdir}/test2.mdwn -m \"$message\" >/dev/null"; + +@changes = IkiWiki::rcs_recentchanges(3); +is($#changes, 1); +is($changes[0]{message}[0]{"line"}, $message); +is($changes[0]{pages}[0]{"page"}, "test2"); +is($changes[1]{pages}[0]{"page"}, "test1"); + +# extra slashes in the path shouldn't break things +$config{svnpath} = "/trunk//"; +IkiWiki::checkconfig(); +@changes = IkiWiki::rcs_recentchanges(3); +is($#changes, 1); +is($changes[0]{message}[0]{"line"}, $message); +is($changes[0]{pages}[0]{"page"}, "test2"); +is($changes[1]{pages}[0]{"page"}, "test1"); + +system "rm -rf $dir"; diff --git a/t/syntax.t b/t/syntax.t new file mode 100755 index 000000000..b7c6efd58 --- /dev/null +++ b/t/syntax.t @@ -0,0 +1,20 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More; + +my @progs="ikiwiki.in"; +my @libs="IkiWiki.pm"; +# monotone, external, amazon_s3, po, and cvs +# skipped since they need perl modules +push @libs, map { chomp; $_ } `find IkiWiki -type f -name \\*.pm | grep -v monotone.pm | grep -v external.pm | grep -v amazon_s3.pm | grep -v po.pm | grep -v cvs.pm`; +push @libs, 'IkiWiki/Plugin/skeleton.pm.example'; + +plan(tests => (@progs + @libs)); + +foreach my $file (@progs) { + ok(system("perl -c $file >/dev/null 2>&1") eq 0, $file); +} +foreach my $file (@libs) { + ok(system("perl -c $file >/dev/null 2>&1") eq 0, $file); +} diff --git a/t/tag.t b/t/tag.t new file mode 100755 index 000000000..cc0a30cad --- /dev/null +++ b/t/tag.t @@ -0,0 +1,88 @@ +#!/usr/bin/perl +package IkiWiki; + +use warnings; +use strict; +use Test::More tests => 24; + +BEGIN { use_ok("IkiWiki"); } +BEGIN { use_ok("IkiWiki::Render"); } +BEGIN { use_ok("IkiWiki::Plugin::mdwn"); } +BEGIN { use_ok("IkiWiki::Plugin::tag"); } + +ok(! system("rm -rf t/tmp; mkdir t/tmp")); + +$config{srcdir} = 't/tmp'; +$config{underlaydir} = 't/tmp'; +$config{templatedir} = 'templates'; +$config{usedirs} = 1; +$config{htmlext} = 'html'; +$config{wiki_file_chars} = "-[:alnum:]+/.:_"; +$config{userdir} = "users"; +$config{tagbase} = "tags"; +$config{tag_autocreate} = 1; +$config{tag_autocreate_commit} = 0; +$config{default_pageext} = "mdwn"; +$config{wiki_file_prune_regexps} = [qr/^\./]; +$config{underlaydirbase} = '.'; + +is(checkconfig(), 1); + +%oldrenderedfiles=%pagectime=(); +%pagesources=%pagemtime=%oldlinks=%links=%depends=%typedlinks=%oldtypedlinks= +%destsources=%renderedfiles=%pagecase=%pagestate=(); + +foreach my $page (qw(tags/numbers tags/letters one two alpha beta)) { + $pagesources{$page} = "$page.mdwn"; + $pagemtime{$page} = $pagectime{$page} = 1000000; + writefile("$page.mdwn", "t/tmp", "your ad here"); +} + +$links{one}=[qw(tags/numbers alpha tags/letters)]; +$links{two}=[qw(tags/numbers)]; +$links{alpha}=[qw(tags/letters one)]; +$links{beta}=[qw(tags/letters)]; +$typedlinks{one}={tag => {"tags/numbers" => 1 }}; +$typedlinks{two}={tag => {"tags/numbers" => 1 }}; +$typedlinks{alpha}={tag => {"tags/letters" => 1 }}; +$typedlinks{beta}={tag => {"tags/letters" => 1 }}; + +ok(pagespec_match("one", "tagged(numbers)")); +ok(!pagespec_match("two", "tagged(alpha)")); +ok(pagespec_match("one", "link(tags/numbers)")); +ok(pagespec_match("one", "link(alpha)")); + +# emulate preprocessing [[!tag numbers primes lucky]] on page "seven", causing +# the "numbers" and "primes" tag pages to be auto-created +IkiWiki::Plugin::tag::preprocess_tag(page => "seven", numbers => undef, primes => undef, lucky => undef); +is($autofiles{"tags/lucky.mdwn"}{plugin}, "tag"); +is($autofiles{"tags/numbers.mdwn"}{plugin}, "tag"); +is($autofiles{"tags/primes.mdwn"}{plugin}, "tag"); +is_deeply([sort keys %autofiles], [qw(tags/lucky.mdwn tags/numbers.mdwn tags/primes.mdwn)]); + +ok(!-e "t/tmp/tags/lucky.mdwn"); +my (%pages, @del); +IkiWiki::gen_autofile("tags/lucky.mdwn", \%pages, \@del); +ok(! -s "t/tmp/tags/lucky.mdwn"); +ok(-s "t/tmp/.ikiwiki/transient/tags/lucky.mdwn"); +is_deeply(\%pages, {"t/tmp/tags/lucky" => 1}); +is_deeply(\@del, []); + +# generating an autofile that already exists does nothing +%pages = @del = (); +IkiWiki::gen_autofile("tags/numbers.mdwn", \%pages, \@del); +is_deeply(\%pages, {}); +is_deeply(\@del, []); + +# generating an autofile that we just deleted does nothing +%pages = (); +@del = ('tags/primes.mdwn'); +IkiWiki::gen_autofile("tags/primes.mdwn", \%pages, \@del); +is_deeply(\%pages, {}); +is_deeply(\@del, ['tags/primes.mdwn']); + + +# cleanup +ok(! system("rm -rf t/tmp")); + +1; diff --git a/t/template_syntax.t b/t/template_syntax.t new file mode 100755 index 000000000..1e156eed8 --- /dev/null +++ b/t/template_syntax.t @@ -0,0 +1,15 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More; + +my @templates=glob("templates/*.tmpl"), glob("doc/templates/*.mdwn"); +plan(tests => 2*@templates); + +use HTML::Template; + +foreach my $template (@templates) { + my $obj=eval {HTML::Template->new(filename => $template)}; + ok(! $@, $template." $@"); + ok($obj, $template); +} diff --git a/t/templates_documented.t b/t/templates_documented.t new file mode 100755 index 000000000..826c51d36 --- /dev/null +++ b/t/templates_documented.t @@ -0,0 +1,14 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More 'no_plan'; + +$/=undef; +open(IN, "doc/templates.mdwn") || die "doc/templates.mdwn: $!"; +my $page=<IN>; +close IN; + +foreach my $file (glob("templates/*.tmpl")) { + $file=~s/templates\///; + ok($page =~ /\Q$file\E/, "$file documented on doc/templates.mdwn"); +} diff --git a/t/test1.mdwn b/t/test1.mdwn new file mode 100644 index 000000000..f4ebc2c08 --- /dev/null +++ b/t/test1.mdwn @@ -0,0 +1,2 @@ +![o](../images/o.jpg "ó") +óóóóó diff --git a/t/test2.mdwn b/t/test2.mdwn new file mode 100644 index 000000000..7e9b15f80 --- /dev/null +++ b/t/test2.mdwn @@ -0,0 +1,5 @@ +<form> +</form> +<ul> +<li>ş <-- +</ul> diff --git a/t/test3.mdwn b/t/test3.mdwn new file mode 100644 index 000000000..541628bb4 --- /dev/null +++ b/t/test3.mdwn @@ -0,0 +1 @@ +<h1>☺</h1> diff --git a/t/tinyblog/index.mdwn b/t/tinyblog/index.mdwn new file mode 100644 index 000000000..72ba7846a --- /dev/null +++ b/t/tinyblog/index.mdwn @@ -0,0 +1 @@ +[[!inline pages="post" rss=yes]] diff --git a/t/tinyblog/post.mdwn b/t/tinyblog/post.mdwn new file mode 100644 index 000000000..9eaeec7ef --- /dev/null +++ b/t/tinyblog/post.mdwn @@ -0,0 +1 @@ +only post diff --git a/t/titlepage.t b/t/titlepage.t new file mode 100755 index 000000000..5df33423e --- /dev/null +++ b/t/titlepage.t @@ -0,0 +1,13 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 7; + +BEGIN { use_ok("IkiWiki"); } + +is(titlepage("foo bar"), "foo_bar"); +is(titlepage("foo bar baz"), "foo_bar_baz"); +is(titlepage("foo bar/baz"), "foo_bar/baz"); +is(titlepage("foo bar&baz"), "foo_bar__38__baz"); +is(titlepage("foo bar & baz"), "foo_bar___38___baz"); +is(titlepage("foo bar_baz"), "foo_bar__95__baz"); diff --git a/t/trail.t b/t/trail.t new file mode 100755 index 000000000..dce3b3c7e --- /dev/null +++ b/t/trail.t @@ -0,0 +1,292 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More 'no_plan'; +use IkiWiki; + +sub check_trail { + my $file=shift; + my $expected=shift; + my $trailname=shift || qr/\w+/; + my $blob=readfile("t/tmp/out/$file"); + my ($trailline)=$blob=~/^trail=$trailname\s+(.*)$/m; + is($trailline, $expected, "expected $expected in $file"); +} + +sub check_no_trail { + my $file=shift; + my $trailname=shift || qr/\w+/; + my $blob=readfile("t/tmp/out/$file"); + my ($trailline)=$blob=~/^trail=$trailname\s+(.*)$/m; + $trailline="" unless defined $trailline; + ok($trailline !~ /^trail=$trailname\s+/, "no trail $trailname in $file"); +} + +my $blob; + +ok(! system("rm -rf t/tmp")); +ok(! system("mkdir t/tmp")); + +# Write files with a date in the past, so that when we refresh, +# the update is detected. +sub write_old_file { + my $name = shift; + my $content = shift; + + writefile($name, "t/tmp/in", $content); + ok(utime(333333333, 333333333, "t/tmp/in/$name")); +} + +# Use a rather stylized template to override the default rendering, to make +# it easy to search for the desired results +write_old_file("templates/trails.tmpl", <<EOF +<TMPL_LOOP TRAILLOOP> +<TMPL_IF __FIRST__><nav></TMPL_IF> +<div> +trail=<TMPL_VAR TRAILPAGE> n=<TMPL_VAR NEXTPAGE> p=<TMPL_VAR PREVPAGE> +</div> +<div> +<TMPL_IF PREVURL> +<a href="<TMPL_VAR PREVURL>">< <TMPL_VAR PREVTITLE></a> +</TMPL_IF> | +<a href="<TMPL_VAR TRAILURL>">^ <TMPL_VAR TRAILTITLE> ^</a> +| <TMPL_IF NEXTURL> +<a href="<TMPL_VAR NEXTURL>"><TMPL_VAR NEXTTITLE> ></a> +</TMPL_IF> +</div> +<TMPL_IF __LAST__></nav></TMPL_IF> +</TMPL_LOOP> +EOF +); +write_old_file("badger.mdwn", "[[!meta title=\"The Breezy Badger\"]]\ncontent of badger"); +write_old_file("mushroom.mdwn", "content of mushroom"); +write_old_file("snake.mdwn", "content of snake"); +write_old_file("ratty.mdwn", "content of ratty"); +write_old_file("mr_toad.mdwn", "content of mr toad"); +write_old_file("add.mdwn", '[[!trailitems pagenames="add/a add/b add/c add/d add/e"]]'); +write_old_file("add/b.mdwn", "b"); +write_old_file("add/d.mdwn", "d"); +write_old_file("del.mdwn", '[[!trailitems pages="del/*" sort=title]]'); +write_old_file("del/a.mdwn", "a"); +write_old_file("del/b.mdwn", "b"); +write_old_file("del/c.mdwn", "c"); +write_old_file("del/d.mdwn", "d"); +write_old_file("del/e.mdwn", "e"); +write_old_file("self_referential.mdwn", '[[!trailitems pagenames="self_referential" circular=yes]]'); +write_old_file("sorting/linked.mdwn", "linked"); +write_old_file("sorting/a/b.mdwn", "a/b"); +write_old_file("sorting/a/c.mdwn", "a/c"); +write_old_file("sorting/z/a.mdwn", "z/a"); +write_old_file("sorting/beginning.mdwn", "beginning"); +write_old_file("sorting/middle.mdwn", "middle"); +write_old_file("sorting/end.mdwn", "end"); +write_old_file("sorting/new.mdwn", "new"); +write_old_file("sorting/old.mdwn", "old"); +write_old_file("sorting/ancient.mdwn", "ancient"); +# These three need to be in the appropriate age order +ok(utime(333333333, 333333333, "t/tmp/in/sorting/new.mdwn")); +ok(utime(222222222, 222222222, "t/tmp/in/sorting/old.mdwn")); +ok(utime(111111111, 111111111, "t/tmp/in/sorting/ancient.mdwn")); +write_old_file("sorting/linked2.mdwn", "linked2"); +# This initially uses the default sort order: age for the inline, and path +# for trailitems. We change it later. +write_old_file("sorting.mdwn", + '[[!traillink linked]] ' . + '[[!trailitems pages="sorting/z/a or sorting/a/b or sorting/a/c"]] ' . + '[[!trailitems pagenames="sorting/beginning sorting/middle sorting/end"]] ' . + '[[!inline pages="sorting/old or sorting/ancient or sorting/new" trail="yes"]] ' . + '[[!traillink linked2]]'); +write_old_file("limited/a.mdwn", "a"); +write_old_file("limited/b.mdwn", "b"); +write_old_file("limited/c.mdwn", "c"); +write_old_file("limited/d.mdwn", "d"); +write_old_file("limited.mdwn", + '[[!inline pages="limited/*" trail="yes" show=2 sort=title]]'); +write_old_file("untrail/a.mdwn", "a"); +write_old_file("untrail/b.mdwn", "b"); +write_old_file("untrail.mdwn", "[[!traillink a]] [[!traillink b]]"); +write_old_file("retitled/a.mdwn", "a"); +write_old_file("retitled.mdwn", + '[[!meta title="the old title"]][[!traillink a]]'); + +write_old_file("meme.mdwn", <<EOF +[[!trail]] +* [[!traillink badger]] +* [[!traillink badger text="This is a link to badger, with a title"]] +* [[!traillink That_is_the_badger|badger]] +* [[!traillink badger]] +* [[!traillink mushroom]] +* [[!traillink mushroom]] +* [[!traillink snake]] +* [[!traillink snake]] +EOF +); + +write_old_file("wind_in_the_willows.mdwn", <<EOF +[[!trailoptions circular=yes sort=title]] +[[!trailitems pages="ratty or badger or mr_toad"]] +[[!trailitem moley]] +EOF +); + +ok(! system("make -s ikiwiki.out")); + +my $command = "perl -I. ./ikiwiki.out -set usedirs=0 -plugin trail -plugin inline -url=http://example.com -cgiurl=http://example.com/ikiwiki.cgi -rss -atom -underlaydir=underlays/basewiki -set underlaydirbase=underlays -templatedir=templates t/tmp/in t/tmp/out -verbose"; + +ok(! system($command)); + +ok(! system("$command -refresh")); + +$blob = readfile("t/tmp/out/meme.html"); +ok($blob =~ /<a href="(\.\/)?badger.html">badger<\/a>/m); +ok($blob =~ /<a href="(\.\/)?badger.html">This is a link to badger, with a title<\/a>/m); +ok($blob =~ /<a href="(\.\/)?badger.html">That is the badger<\/a>/m); + +check_trail("badger.html", "n=mushroom p=", "meme"); +check_trail("badger.html", "n=mr_toad p=ratty", "wind_in_the_willows"); + +ok(! -f "t/tmp/out/moley.html"); + +check_trail("mr_toad.html", "n=ratty p=badger", "wind_in_the_willows"); +check_no_trail("mr_toad.html", "meme"); +# meta title is respected for pages that have one +$blob = readfile("t/tmp/out/mr_toad.html"); +ok($blob =~ /">< The Breezy Badger<\/a>/m); +# pagetitle for pages that don't +ok($blob =~ /">ratty ><\/a>/m); + +check_no_trail("ratty.html", "meme"); +check_trail("ratty.html", "n=badger p=mr_toad", "wind_in_the_willows"); + +check_trail("mushroom.html", "n=snake p=badger", "meme"); +check_no_trail("mushroom.html", "wind_in_the_willows"); + +check_trail("snake.html", "n= p=mushroom", "meme"); +check_no_trail("snake.html", "wind_in_the_willows"); + +check_trail("self_referential.html", "n= p=", "self_referential"); + +check_trail("add/b.html", "n=add/d p=", "add"); +check_trail("add/d.html", "n= p=add/b", "add"); +ok(! -f "t/tmp/out/add/a.html"); +ok(! -f "t/tmp/out/add/c.html"); +ok(! -f "t/tmp/out/add/e.html"); + +check_trail("del/a.html", "n=del/b p="); +check_trail("del/b.html", "n=del/c p=del/a"); +check_trail("del/c.html", "n=del/d p=del/b"); +check_trail("del/d.html", "n=del/e p=del/c"); +check_trail("del/e.html", "n= p=del/d"); + +check_trail("sorting/linked.html", "n=sorting/a/b p="); +check_trail("sorting/a/b.html", "n=sorting/a/c p=sorting/linked"); +check_trail("sorting/a/c.html", "n=sorting/z/a p=sorting/a/b"); +check_trail("sorting/z/a.html", "n=sorting/beginning p=sorting/a/c"); +check_trail("sorting/beginning.html", "n=sorting/middle p=sorting/z/a"); +check_trail("sorting/middle.html", "n=sorting/end p=sorting/beginning"); +check_trail("sorting/end.html", "n=sorting/new p=sorting/middle"); +check_trail("sorting/new.html", "n=sorting/old p=sorting/end"); +check_trail("sorting/old.html", "n=sorting/ancient p=sorting/new"); +check_trail("sorting/ancient.html", "n=sorting/linked2 p=sorting/old"); +check_trail("sorting/linked2.html", "n= p=sorting/ancient"); + +# If the inline has a limited number of pages, the trail still contains +# everything. +$blob = readfile("t/tmp/out/limited.html"); +ok($blob =~ /<a href="(\.\/)?limited\/a.html">a<\/a>/m); +ok($blob =~ /<a href="(\.\/)?limited\/b.html">b<\/a>/m); +ok($blob !~ /<a href="(\.\/)?limited\/c.html">/m); +ok($blob !~ /<a href="(\.\/)?limited\/d.html">/m); +check_trail("limited/a.html", "n=limited/b p="); +check_trail("limited/b.html", "n=limited/c p=limited/a"); +check_trail("limited/c.html", "n=limited/d p=limited/b"); +check_trail("limited/d.html", "n= p=limited/c"); + +check_trail("untrail/a.html", "n=untrail/b p="); +check_trail("untrail/b.html", "n= p=untrail/a"); + +$blob = readfile("t/tmp/out/retitled/a.html"); +ok($blob =~ /\^ the old title \^/m); + +# Make some changes and refresh. These writefile calls don't set an +# old mtime, so they're strictly newer than the "old" files. + +writefile("add/a.mdwn", "t/tmp/in", "a"); +writefile("add/c.mdwn", "t/tmp/in", "c"); +writefile("add/e.mdwn", "t/tmp/in", "e"); +ok(unlink("t/tmp/in/del/a.mdwn")); +ok(unlink("t/tmp/in/del/c.mdwn")); +ok(unlink("t/tmp/in/del/e.mdwn")); + +writefile("sorting.mdwn", "t/tmp/in", + readfile("t/tmp/in/sorting.mdwn") . + '[[!trailoptions sort="title" reverse="yes"]]'); + +writefile("retitled.mdwn", "t/tmp/in", + '[[!meta title="the new title"]][[!traillink a]]'); + +# If the inline has a limited number of pages, the trail still depends on +# everything. +writefile("limited.html", "t/tmp/out", "[this gets rebuilt]"); +writefile("limited/c.mdwn", "t/tmp/in", '[[!meta title="New C page"]]c'); + +writefile("untrail.mdwn", "t/tmp/in", "no longer a trail"); + +ok(! system("$command -refresh")); + +check_trail("add/a.html", "n=add/b p="); +check_trail("add/b.html", "n=add/c p=add/a"); +check_trail("add/c.html", "n=add/d p=add/b"); +check_trail("add/d.html", "n=add/e p=add/c"); +check_trail("add/e.html", "n= p=add/d"); + +check_trail("del/b.html", "n=del/d p="); +check_trail("del/d.html", "n= p=del/b"); +ok(! -f "t/tmp/out/del/a.html"); +ok(! -f "t/tmp/out/del/c.html"); +ok(! -f "t/tmp/out/del/e.html"); + +check_trail("sorting/old.html", "n=sorting/new p="); +check_trail("sorting/new.html", "n=sorting/middle p=sorting/old"); +check_trail("sorting/middle.html", "n=sorting/linked2 p=sorting/new"); +check_trail("sorting/linked2.html", "n=sorting/linked p=sorting/middle"); +check_trail("sorting/linked.html", "n=sorting/end p=sorting/linked2"); +check_trail("sorting/end.html", "n=sorting/a/c p=sorting/linked"); +check_trail("sorting/a/c.html", "n=sorting/beginning p=sorting/end"); +check_trail("sorting/beginning.html", "n=sorting/a/b p=sorting/a/c"); +check_trail("sorting/a/b.html", "n=sorting/ancient p=sorting/beginning"); +check_trail("sorting/ancient.html", "n=sorting/z/a p=sorting/a/b"); +check_trail("sorting/z/a.html", "n= p=sorting/ancient"); + +# If the inline has a limited number of pages, the trail still depends on +# everything, so it gets rebuilt even though it doesn't strictly need it. +# This means we could use it as a way to recompute the order of members +# and the contents of their trail navbars, allowing us to fix the regression +# described in [[bugs/trail excess dependencies]] without a full content +# dependency. +$blob = readfile("t/tmp/out/limited.html"); +ok($blob =~ /<a href="(\.\/)?limited\/a.html">a<\/a>/m); +ok($blob =~ /<a href="(\.\/)?limited\/b.html">b<\/a>/m); +ok($blob !~ /<a href="(\.\/)?limited\/c.html">/m); +ok($blob !~ /<a href="(\.\/)?limited\/d.html">/m); +check_trail("limited/a.html", "n=limited/b p="); +check_trail("limited/b.html", "n=limited/c p=limited/a"); +check_trail("limited/c.html", "n=limited/d p=limited/b"); +check_trail("limited/d.html", "n= p=limited/c"); +# Also, b and d should pick up the change to c. This regressed with the +# change to using a presence dependency. +$blob = readfile("t/tmp/out/limited/b.html"); +ok($blob =~ /New C page >/m); +$blob = readfile("t/tmp/out/limited/d.html"); +ok($blob =~ /< New C page/m); + +# Members of a retitled trail should pick up that change. +# This regressed with the change to using a presence dependency. +$blob = readfile("t/tmp/out/retitled/a.html"); +ok($blob =~ /\^ the new title \^/m); + +# untrail is no longer a trail, so these are no longer in it. +check_no_trail("untrail/a.html"); +check_no_trail("untrail/b.html"); + +ok(! system("rm -rf t/tmp")); diff --git a/t/urlto.t b/t/urlto.t new file mode 100755 index 000000000..338632e94 --- /dev/null +++ b/t/urlto.t @@ -0,0 +1,51 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 26; + +BEGIN { use_ok("IkiWiki"); } + +$IkiWiki::config{srcdir} = '/does/not/exist/'; +$IkiWiki::config{usedirs} = 1; +$IkiWiki::config{htmlext} = "HTML"; +$IkiWiki::config{wiki_file_chars} = "A-Za-z0-9._"; + +$IkiWiki::config{url} = "http://smcv.example.co.uk"; +$IkiWiki::config{cgiurl} = "http://smcv.example.co.uk/cgi-bin/ikiwiki.cgi"; +is(IkiWiki::checkconfig(), 1); + +# absolute version +is(IkiWiki::cgiurl(cgiurl => $config{cgiurl}), "http://smcv.example.co.uk/cgi-bin/ikiwiki.cgi"); +is(IkiWiki::cgiurl(cgiurl => $config{cgiurl}, do => 'badger'), "http://smcv.example.co.uk/cgi-bin/ikiwiki.cgi?do=badger"); +is(IkiWiki::urlto('index', undef, 1), "http://smcv.example.co.uk/"); +is(IkiWiki::urlto('stoats', undef, 1), "http://smcv.example.co.uk/stoats/"); +is(IkiWiki::urlto('', undef, 1), "http://smcv.example.co.uk/"); + +# "local" (absolute path within site) version (default for cgiurl) +is(IkiWiki::cgiurl(), "/cgi-bin/ikiwiki.cgi"); +is(IkiWiki::cgiurl(do => 'badger'), "/cgi-bin/ikiwiki.cgi?do=badger"); +is(IkiWiki::baseurl(undef), "/"); +is(IkiWiki::urlto('index', undef), "/"); +is(IkiWiki::urlto('index'), "/"); +is(IkiWiki::urlto('stoats', undef), "/stoats/"); +is(IkiWiki::urlto('stoats'), "/stoats/"); +is(IkiWiki::urlto(''), "/"); + +# fully-relative version (default for urlto and baseurl) +is(IkiWiki::baseurl('badger/mushroom'), "../../"); +is(IkiWiki::urlto('badger/mushroom', 'snake'), "../badger/mushroom/"); +is(IkiWiki::urlto('', 'snake'), "../"); +is(IkiWiki::urlto('', 'penguin/herring'), "../../"); + +# explicit cgiurl override +is(IkiWiki::cgiurl(cgiurl => 'https://foo/ikiwiki'), "https://foo/ikiwiki"); +is(IkiWiki::cgiurl(do => 'badger', cgiurl => 'https://foo/ikiwiki'), "https://foo/ikiwiki?do=badger"); + +# with url and cgiurl on different sites, "local" degrades to absolute +$IkiWiki::config{url} = "http://example.co.uk/~smcv"; +$IkiWiki::config{cgiurl} = "http://dynamic.example.co.uk/~smcv/ikiwiki.cgi"; +is(IkiWiki::checkconfig(), 1); +is(IkiWiki::cgiurl(), "http://dynamic.example.co.uk/~smcv/ikiwiki.cgi"); +is(IkiWiki::baseurl(undef), "http://example.co.uk/~smcv/"); +is(IkiWiki::urlto('stoats', undef), "http://example.co.uk/~smcv/stoats/"); +is(IkiWiki::urlto('', undef), "http://example.co.uk/~smcv/"); diff --git a/t/yesno.t b/t/yesno.t new file mode 100755 index 000000000..8770390a1 --- /dev/null +++ b/t/yesno.t @@ -0,0 +1,23 @@ +#!/usr/bin/perl +use warnings; +use strict; +use Test::More tests => 11; + +BEGIN { use_ok("IkiWiki"); } + +# note: yesno always accepts English even if localized. +# So no need to bother setting locale to C. + +ok(IkiWiki::yesno("yes") == 1); +ok(IkiWiki::yesno("Yes") == 1); +ok(IkiWiki::yesno("YES") == 1); + +ok(IkiWiki::yesno("no") == 0); +ok(IkiWiki::yesno("No") == 0); +ok(IkiWiki::yesno("NO") == 0); + +ok(IkiWiki::yesno("1") == 1); +ok(IkiWiki::yesno("0") == 0); +ok(IkiWiki::yesno("mooooooooooo") == 0); + +ok(IkiWiki::yesno(undef) == 0); |