diff options
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | Changes | 8 | ||||
| -rw-r--r-- | MANIFEST | 10 | ||||
| -rw-r--r-- | Makefile.PL | 19 | ||||
| -rw-r--r-- | README | 26 | ||||
| -rw-r--r-- | lib/DJabberd/RosterStorage/SQLite/Fixed.pm | 274 | ||||
| -rwxr-xr-x | scripts/rosteredit.pl | 62 | ||||
| -rw-r--r-- | t/00-load.t | 9 | ||||
| -rw-r--r-- | t/boilerplate.t | 48 | ||||
| -rw-r--r-- | t/pod-coverage.t | 6 | ||||
| -rw-r--r-- | t/pod.t | 6 |
11 files changed, 472 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10b5458 --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | META.yml | ||
| 2 | Makefile | ||
| 3 | /blib | ||
| 4 | /pm_to_blib | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | Revision history for DJabberd-RosterStorage-SQLite-Fixed | ||
| 2 | |||
| 3 | 0.02 2007-08-21 | ||
| 4 | Add rosteredit.pl script. Add in views | ||
| 5 | |||
| 6 | 0.01 2007-08-01 | ||
| 7 | First version, released on an unsuspecting world. | ||
| 8 | |||
diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..9afbfda --- /dev/null +++ b/MANIFEST | |||
| @@ -0,0 +1,10 @@ | |||
| 1 | Changes | ||
| 2 | MANIFEST | ||
| 3 | Makefile.PL | ||
| 4 | README | ||
| 5 | scripts/rosteredit.pl | ||
| 6 | lib/DJabberd/RosterStorage/SQLite/Fixed.pm | ||
| 7 | t/00-load.t | ||
| 8 | t/boilerplate.t | ||
| 9 | t/pod-coverage.t | ||
| 10 | t/pod.t | ||
diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..a7cff9f --- /dev/null +++ b/Makefile.PL | |||
| @@ -0,0 +1,19 @@ | |||
| 1 | use strict; | ||
| 2 | use warnings; | ||
| 3 | use ExtUtils::MakeMaker; | ||
| 4 | |||
| 5 | WriteMakefile( | ||
| 6 | NAME => 'DJabberd::RosterStorage::SQLite::Fixed', | ||
| 7 | AUTHOR => 'Edward Rudd <urkle@outoforder.cc>', | ||
| 8 | VERSION_FROM => 'lib/DJabberd/RosterStorage/SQLite/Fixed.pm', | ||
| 9 | EXE_FILES => ['scripts/rosteredit.pl'], | ||
| 10 | ABSTRACT_FROM => 'lib/DJabberd/RosterStorage/SQLite/Fixed.pm', | ||
| 11 | PL_FILES => {}, | ||
| 12 | PREREQ_PM => { | ||
| 13 | 'Test::More' => 0, | ||
| 14 | 'DJabberd' => 0.83, | ||
| 15 | 'DJabberd::RosterStorage::SQLite' => 1.00 | ||
| 16 | }, | ||
| 17 | dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, | ||
| 18 | clean => { FILES => 'DJabberd-RosterStorage-SQLite-Fixed-*' }, | ||
| 19 | ); | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | DJabberd-RosterStorage-SQLite-Fixed | ||
| 2 | |||
| 3 | Shared Roster for DJabberd using SQLite. | ||
| 4 | |||
| 5 | INSTALLATION | ||
| 6 | |||
| 7 | To install this module, run the following commands: | ||
| 8 | |||
| 9 | perl Makefile.PL | ||
| 10 | make | ||
| 11 | make test | ||
| 12 | make install | ||
| 13 | |||
| 14 | |||
| 15 | SUPPORT AND DOCUMENTATION | ||
| 16 | |||
| 17 | After installing, you can find documentation for this module with the perldoc command. | ||
| 18 | |||
| 19 | perldoc DJabberd::RosterStorage::SQLite::Fixed | ||
| 20 | |||
| 21 | COPYRIGHT AND LICENCE | ||
| 22 | |||
| 23 | Copyright (C) 2007 Edward Rudd | ||
| 24 | |||
| 25 | This program is free software; you can redistribute it and/or modify it | ||
| 26 | under the same terms as Perl itself. | ||
diff --git a/lib/DJabberd/RosterStorage/SQLite/Fixed.pm b/lib/DJabberd/RosterStorage/SQLite/Fixed.pm new file mode 100644 index 0000000..fe41363 --- /dev/null +++ b/lib/DJabberd/RosterStorage/SQLite/Fixed.pm | |||
| @@ -0,0 +1,274 @@ | |||
| 1 | package DJabberd::RosterStorage::SQLite::Fixed; | ||
| 2 | use strict; | ||
| 3 | use warnings; | ||
| 4 | use base 'DJabberd::RosterStorage::SQLite'; | ||
| 5 | use DJabberd::Log; | ||
| 6 | use DJabberd::Util; | ||
| 7 | our $logger = DJabberd::Log->get_logger(); | ||
| 8 | |||
| 9 | =head1 NAME | ||
| 10 | |||
| 11 | DJabberd::RosterStorage::SQLite::Fixed - a shared roster implementation for the SQLite roster storage | ||
| 12 | |||
| 13 | =head1 VERSION | ||
| 14 | |||
| 15 | Version 0.02 | ||
| 16 | =cut | ||
| 17 | |||
| 18 | our $VERSION = '0.02'; | ||
| 19 | |||
| 20 | =head1 SYNOPSIS | ||
| 21 | |||
| 22 | <VHost mydomain.com> | ||
| 23 | |||
| 24 | [...] | ||
| 25 | |||
| 26 | <Plugin DJabberd::RosterStorage::SQLite::Fixed> | ||
| 27 | Database jabberroster.sqlite | ||
| 28 | FixedGuestOK yes | ||
| 29 | </Plugin> | ||
| 30 | </VHost> | ||
| 31 | |||
| 32 | Valid command are all command valid in DJabberd::RosterStorage::SQLite Plus the following | ||
| 33 | |||
| 34 | FixedGuestOK - Populate accounts with the shared roster if they are not in the roster itself? | ||
| 35 | Setting this to yes will populate a user who is not in the shared roster with everyone in the shared roster | ||
| 36 | The default is to only populate rosters for users that are part of the shared roster | ||
| 37 | |||
| 38 | =head1 AUTHOR | ||
| 39 | |||
| 40 | Edward Rudd, C<< <urkle at outoforder.cc> >> | ||
| 41 | |||
| 42 | =cut | ||
| 43 | |||
| 44 | =head2 set_config_fixedguestok($self, $guest) | ||
| 45 | |||
| 46 | Called to specify if guests should have the shared roster added to their roster | ||
| 47 | |||
| 48 | =cut | ||
| 49 | |||
| 50 | sub set_config_fixedguestok { | ||
| 51 | my ($self, $guest) = @_; | ||
| 52 | $self->{fixed_guestok} = as_bool $guest; | ||
| 53 | } | ||
| 54 | |||
| 55 | =head2 finalize($self) | ||
| 56 | |||
| 57 | Set defaults for the configuration | ||
| 58 | |||
| 59 | =cut | ||
| 60 | |||
| 61 | sub finalize { | ||
| 62 | my $self = shift; | ||
| 63 | $self->{fixed_guestok} = 0 unless $self->{fixed_guestok}; | ||
| 64 | $self->SUPER::finalize; | ||
| 65 | } | ||
| 66 | |||
| 67 | =head2 get_roster($self, $cb, $jid) | ||
| 68 | |||
| 69 | Gets the Roster for the user | ||
| 70 | |||
| 71 | =cut | ||
| 72 | |||
| 73 | sub get_roster { | ||
| 74 | my ($self, $cb, $jid) = @_; | ||
| 75 | # cb can '->set_roster(Roster)' or decline | ||
| 76 | |||
| 77 | my $myself = lc $jid->as_bare_string; | ||
| 78 | $logger->info("Fixed loading roster for $myself ..."); | ||
| 79 | |||
| 80 | my $on_load_roster = sub { | ||
| 81 | my (undef, $roster) = @_; | ||
| 82 | |||
| 83 | my $pre_ct = $roster->items; | ||
| 84 | $logger->info(" $pre_ct roster items prior to population..."); | ||
| 85 | |||
| 86 | # see which shared contacts already in roster | ||
| 87 | my %has; | ||
| 88 | foreach my $it ($roster->items) { | ||
| 89 | my $jid = $it->jid; | ||
| 90 | $has{lc $jid->as_bare_string} = $it; | ||
| 91 | } | ||
| 92 | |||
| 93 | # add missing shared contacts to the roster | ||
| 94 | my $req_roster = $self->_roster(); | ||
| 95 | if ($self->{fixed_guestok}==0) { | ||
| 96 | my $guestok = 0; | ||
| 97 | foreach my $user ( @$req_roster) { | ||
| 98 | if ($user->{jid} eq $myself) { | ||
| 99 | $guestok = 1; | ||
| 100 | last; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | # Bail if guestOK == 0 && user it not in the roster | ||
| 104 | return if $guestok == 0; | ||
| 105 | } | ||
| 106 | |||
| 107 | foreach my $user ( @$req_roster) { | ||
| 108 | next if $user->{jid} eq $myself; | ||
| 109 | |||
| 110 | my $name = $user->{name}; | ||
| 111 | my $ri = $has{$user->{jid}} || DJabberd::RosterItem->new(jid => $user->{jid}, | ||
| 112 | name => ($user->{name} || $user->{jid}), | ||
| 113 | groups => [$user->{group}]); | ||
| 114 | |||
| 115 | |||
| 116 | $ri->subscription->set_from; | ||
| 117 | $ri->subscription->set_to; | ||
| 118 | $roster->add($ri); | ||
| 119 | } | ||
| 120 | |||
| 121 | my $post_ct = $roster->items; | ||
| 122 | $logger->info(" $post_ct roster items post population..."); | ||
| 123 | |||
| 124 | $cb->set_roster($roster); | ||
| 125 | }; | ||
| 126 | |||
| 127 | my $cb2 = DJabberd::Callback->new({set_roster => $on_load_roster, | ||
| 128 | decline => sub { $cb->decline }}); | ||
| 129 | $self->SUPER::get_roster($cb2, $jid); | ||
| 130 | } | ||
| 131 | |||
| 132 | =head2 check_install_schema($self) | ||
| 133 | |||
| 134 | Checks the SQL ite Schema | ||
| 135 | |||
| 136 | =cut | ||
| 137 | |||
| 138 | sub check_install_schema { | ||
| 139 | my $self = shift; | ||
| 140 | |||
| 141 | $self->SUPER::check_install_schema(); | ||
| 142 | |||
| 143 | my $dbh = $self->{dbh}; | ||
| 144 | |||
| 145 | eval { | ||
| 146 | $dbh->do(qq{ | ||
| 147 | CREATE TABLE requiredusers ( | ||
| 148 | jid VARCHAR(255) NOT NULL, | ||
| 149 | fullname VARCHAR(255) NOT NULL, | ||
| 150 | groupname VARCHAR(255) NOT NULL, | ||
| 151 | UNIQUE (jid) | ||
| 152 | )}); | ||
| 153 | }; | ||
| 154 | if ($@ && $@ !~ /table \w+ already exists/) { | ||
| 155 | $logger->logdie("SQL error $@"); | ||
| 156 | die "SQL error: $@\n"; | ||
| 157 | } | ||
| 158 | eval { | ||
| 159 | $dbh->do(qq{ | ||
| 160 | CREATE VIEW RosterPreview AS | ||
| 161 | SELECT ju.jid AS UserID, g.name AS [Group], | ||
| 162 | jr.jid AS ContactID, r.name AS Contact, r.subscription AS Subscription | ||
| 163 | FROM roster r | ||
| 164 | JOIN jidmap ju ON r.userid=ju.jidid | ||
| 165 | JOIN jidmap jr ON r.contactid = jr.jidid | ||
| 166 | JOIN groupitem gi ON gi.contactid=r.contactid | ||
| 167 | JOIN rostergroup g ON g.userid=r.userid AND g.groupid=gi.groupid | ||
| 168 | UNION SELECT r1.jid, r2.groupname, r2.jid, r2.fullname, 3 | ||
| 169 | FROM requiredusers r1, requiredusers r2 | ||
| 170 | WHERE r1.jid != r2.jid}); | ||
| 171 | }; | ||
| 172 | if ($@ && $@ !~ /table \w+ already exists/) { | ||
| 173 | $logger->logdie("SQL error $@"); | ||
| 174 | die "SQL error: $@\n"; | ||
| 175 | } | ||
| 176 | eval { | ||
| 177 | $dbh->do(qq{ | ||
| 178 | CREATE VIEW RosterList AS | ||
| 179 | SELECT J.jidid as LID, J2.jidid as RID, | ||
| 180 | G.groupid as GID, | ||
| 181 | J.jid AS Local, J2.jid AS Remote, | ||
| 182 | G.name AS [Group] | ||
| 183 | FROM jidmap J | ||
| 184 | JOIN rostergroup G ON G.userid=J.jidid | ||
| 185 | JOIN groupitem M ON G.groupid = M.groupid | ||
| 186 | JOIN jidmap J2 ON J2.jidid = M.contactid | ||
| 187 | ORDER BY J.jid, J2.jid}); | ||
| 188 | }; | ||
| 189 | if ($@ && $@ !~ /table \w+ already exists/) { | ||
| 190 | $logger->logdie("SQL error $@"); | ||
| 191 | die "SQL error: $@\n"; | ||
| 192 | } | ||
| 193 | $logger->info("Created all roster tables"); | ||
| 194 | } | ||
| 195 | |||
| 196 | my $last_roster; | ||
| 197 | my $last_roster_time = 0; # unixtime of last SQL suck | ||
| 198 | sub _roster { | ||
| 199 | my $self = shift; | ||
| 200 | my $now = time(); | ||
| 201 | |||
| 202 | # Cache list for 1 minute(s) | ||
| 203 | if ($last_roster && $last_roster_time > $now - 60) { | ||
| 204 | return $last_roster; | ||
| 205 | } | ||
| 206 | |||
| 207 | my $dbh = $self->{dbh}; | ||
| 208 | |||
| 209 | my $sql = qq{ | ||
| 210 | SELECT jid, fullname, groupname FROM requiredusers | ||
| 211 | }; | ||
| 212 | |||
| 213 | my $roster = eval { | ||
| 214 | $dbh->selectall_arrayref($sql); | ||
| 215 | }; | ||
| 216 | $logger->logdie("Failed to load roster: $@") if $@; | ||
| 217 | |||
| 218 | $logger->info("Found ".($#{ @$roster}+1)." Roster users"); | ||
| 219 | |||
| 220 | my @info = (); | ||
| 221 | foreach my $item ( @$roster ) { | ||
| 222 | my $rec = {}; | ||
| 223 | $rec->{'jid'} = $item->[0]; | ||
| 224 | $rec->{'name'} = $item->[1]; | ||
| 225 | $rec->{'group'} = $item->[2]; | ||
| 226 | push @info, $rec; | ||
| 227 | } | ||
| 228 | $logger->info("Loaded ".($#info+1)." Roster users"); | ||
| 229 | $last_roster_time = $now; | ||
| 230 | return $last_roster = \@info; | ||
| 231 | } | ||
| 232 | |||
| 233 | =head2 load_roster_item($self, $jid, $contact_jid, $cb) | ||
| 234 | |||
| 235 | Called when a roster item is added | ||
| 236 | |||
| 237 | =cut | ||
| 238 | |||
| 239 | sub load_roster_item { | ||
| 240 | my ($self, $jid, $contact_jid, $cb) = @_; | ||
| 241 | |||
| 242 | my $is_shared = sub { | ||
| 243 | my $jid = shift; | ||
| 244 | my $roster = $self->_roster(); | ||
| 245 | foreach my $user (@$roster) { | ||
| 246 | if (lc $user->{jid} eq lc $jid->as_bare_string) { return 1; } | ||
| 247 | } | ||
| 248 | return 0; | ||
| 249 | }; | ||
| 250 | |||
| 251 | if ($is_shared->($jid) && $is_shared->($contact_jid)) { | ||
| 252 | my $both = DJabberd::Subscription->new; | ||
| 253 | $both->set_from; | ||
| 254 | $both->set_to; | ||
| 255 | my $rit = DJabberd::RosterItem->new(jid => $contact_jid, | ||
| 256 | subscription => $both); | ||
| 257 | $cb->set($rit); | ||
| 258 | return; | ||
| 259 | } | ||
| 260 | |||
| 261 | $self->SUPER::load_roster_item($jid, $contact_jid, $cb); | ||
| 262 | } | ||
| 263 | |||
| 264 | =head1 COPYRIGHT & LICENSE | ||
| 265 | |||
| 266 | Original work Copyright 2006 Alexander Karelas, Martin Atkins, Brad Fitzpatrick and Aleksandar Milanov. All rights reserved. | ||
| 267 | Copyright 2007 Edward Rudd. All rights reserved. | ||
| 268 | |||
| 269 | This program is free software; you can redistribute it and/or modify it | ||
| 270 | under the same terms as Perl itself. | ||
| 271 | |||
| 272 | =cut | ||
| 273 | |||
| 274 | 1; | ||
diff --git a/scripts/rosteredit.pl b/scripts/rosteredit.pl new file mode 100755 index 0000000..4c7132d --- /dev/null +++ b/scripts/rosteredit.pl | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | #!/usr/bin/perl | ||
| 2 | |||
| 3 | use strict; | ||
| 4 | |||
| 5 | use DBI; | ||
| 6 | |||
| 7 | if ($#ARGV < 0) { | ||
| 8 | die ("must specify database then action (list, add, del, preview)\n"); | ||
| 9 | } | ||
| 10 | my $db = shift @ARGV; | ||
| 11 | my $dbh = DBI->connect_cached("dbi:SQLite:dbname=$db","","", { RaiseError => 1, PrintError => 0, AutoCommit => 1 }); | ||
| 12 | |||
| 13 | my $action; | ||
| 14 | my $param; | ||
| 15 | |||
| 16 | if ($#ARGV < 0) { | ||
| 17 | die ("must specify action (list, add, del, preview)\n"); | ||
| 18 | } else { | ||
| 19 | $action = shift @ARGV; | ||
| 20 | for ($action) { | ||
| 21 | if (/^preview$/) { | ||
| 22 | if ($#ARGV < 0) { | ||
| 23 | die ("Must specify jabber id"); | ||
| 24 | } | ||
| 25 | my $sql = "SELECT [Group], ContactID, Contact, Subscription FROM RosterPreview WHERE UserID = ?"; | ||
| 26 | my ($jid) = @ARGV; | ||
| 27 | my $roster = eval { | ||
| 28 | $dbh->selectall_arrayref($sql,{ Slite=> {} },$jid); | ||
| 29 | }; | ||
| 30 | foreach my $item ( @$roster ) { | ||
| 31 | print "Entry: $item->[3] $item->[2]<$item->[1]> in group $item->[0]\n"; | ||
| 32 | } | ||
| 33 | } elsif (/^list$/) { | ||
| 34 | my $sql = "SELECT jid, fullname, groupname FROM requiredusers ORDER BY groupname, fullname"; | ||
| 35 | my $roster = eval { | ||
| 36 | $dbh->selectall_arrayref($sql,{ Slite=> {} }); | ||
| 37 | }; | ||
| 38 | foreach my $item ( @$roster ) { | ||
| 39 | print "Entry: $item->[1]<$item->[0]> in group $item->[2]\n"; | ||
| 40 | } | ||
| 41 | } elsif (/^add$/) { | ||
| 42 | if ($#ARGV < 2) { | ||
| 43 | die ("Must specify jabber id, fullname and groupname"); | ||
| 44 | } | ||
| 45 | my $sql = "INSERT INTO requiredusers (jid, fullname, groupname) VALUES (?, ?, ?)"; | ||
| 46 | my ($jid, $fname, $gname) = @ARGV; | ||
| 47 | print "Adding: $fname<$jid> to group $gname\n"; | ||
| 48 | $dbh->do($sql,undef,$jid,$fname,$gname); | ||
| 49 | } elsif (/^del$/) { | ||
| 50 | if ($#ARGV < 0) { | ||
| 51 | die ("Must specify jabber id"); | ||
| 52 | } | ||
| 53 | my $sql = "DELETE FROM requiredusers WHERE jid = ?"; | ||
| 54 | my ($jid) = @ARGV; | ||
| 55 | print "Deleting: $jid\n"; | ||
| 56 | $dbh->do($sql,undef,$jid); | ||
| 57 | } else { | ||
| 58 | die ("Unknown action $action\n"); | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
diff --git a/t/00-load.t b/t/00-load.t new file mode 100644 index 0000000..786b9cd --- /dev/null +++ b/t/00-load.t | |||
| @@ -0,0 +1,9 @@ | |||
| 1 | #!perl -T | ||
| 2 | |||
| 3 | use Test::More tests => 1; | ||
| 4 | |||
| 5 | BEGIN { | ||
| 6 | use_ok( 'DJabberd::RosterStorage::SQLite::Fixed' ); | ||
| 7 | } | ||
| 8 | |||
| 9 | diag( "Testing DJabberd::RosterStorage::SQLite::Fixed $DJabberd::RosterStorage::SQLite::Fixed::VERSION, Perl $], $^X" ); | ||
diff --git a/t/boilerplate.t b/t/boilerplate.t new file mode 100644 index 0000000..824d393 --- /dev/null +++ b/t/boilerplate.t | |||
| @@ -0,0 +1,48 @@ | |||
| 1 | #!perl -T | ||
| 2 | |||
| 3 | use strict; | ||
| 4 | use warnings; | ||
| 5 | use Test::More tests => 3; | ||
| 6 | |||
| 7 | sub not_in_file_ok { | ||
| 8 | my ($filename, %regex) = @_; | ||
| 9 | open my $fh, "<", $filename | ||
| 10 | or die "couldn't open $filename for reading: $!"; | ||
| 11 | |||
| 12 | my %violated; | ||
| 13 | |||
| 14 | while (my $line = <$fh>) { | ||
| 15 | while (my ($desc, $regex) = each %regex) { | ||
| 16 | if ($line =~ $regex) { | ||
| 17 | push @{$violated{$desc}||=[]}, $.; | ||
| 18 | } | ||
| 19 | } | ||
| 20 | } | ||
| 21 | |||
| 22 | if (%violated) { | ||
| 23 | fail("$filename contains boilerplate text"); | ||
| 24 | diag "$_ appears on lines @{$violated{$_}}" for keys %violated; | ||
| 25 | } else { | ||
| 26 | pass("$filename contains no boilerplate text"); | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | not_in_file_ok(README => | ||
| 31 | "The README is used..." => qr/The README is used/, | ||
| 32 | "'version information here'" => qr/to provide version information/, | ||
| 33 | ); | ||
| 34 | |||
| 35 | not_in_file_ok(Changes => | ||
| 36 | "placeholder date/time" => qr(Date/time) | ||
| 37 | ); | ||
| 38 | |||
| 39 | sub module_boilerplate_ok { | ||
| 40 | my ($module) = @_; | ||
| 41 | not_in_file_ok($module => | ||
| 42 | 'the great new $MODULENAME' => qr/ - The great new /, | ||
| 43 | 'boilerplate description' => qr/Quick summary of what the module/, | ||
| 44 | 'stub function definition' => qr/function[12]/, | ||
| 45 | ); | ||
| 46 | } | ||
| 47 | |||
| 48 | module_boilerplate_ok('lib/DJabberd/RosterStorage/SQLite/Fixed.pm'); | ||
diff --git a/t/pod-coverage.t b/t/pod-coverage.t new file mode 100644 index 0000000..703f91d --- /dev/null +++ b/t/pod-coverage.t | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | #!perl -T | ||
| 2 | |||
| 3 | use Test::More; | ||
| 4 | eval "use Test::Pod::Coverage 1.04"; | ||
| 5 | plan skip_all => "Test::Pod::Coverage 1.04 required for testing POD coverage" if $@; | ||
| 6 | all_pod_coverage_ok(); | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | #!perl -T | ||
| 2 | |||
| 3 | use Test::More; | ||
| 4 | eval "use Test::Pod 1.14"; | ||
| 5 | plan skip_all => "Test::Pod 1.14 required for testing POD" if $@; | ||
| 6 | all_pod_files_ok(); | ||
