File Coverage

blib/lib/CPAN/Faker.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package CPAN::Faker;
2             {
3             $CPAN::Faker::VERSION = '0.010';
4             }
5 2     2   134653 use 5.008;
  2         8  
  2         87  
6 2     2   5576 use Moose;
  0            
  0            
7             # ABSTRACT: build a bogus CPAN instance for testing
8              
9             use CPAN::Checksums ();
10             use Compress::Zlib ();
11             use Cwd ();
12             use Data::Section -setup;
13             use File::Find ();
14             use File::Next ();
15             use File::Path ();
16             use File::Spec ();
17             use IO::Compress::Gzip qw(gzip $GzipError);
18             use Module::Faker::Dist 0.015; # ->packages
19             use Sort::Versions qw(versioncmp);
20             use Text::Template;
21              
22              
23             has dest => (is => 'ro', isa => 'Str', required => 1);
24             has source => (is => 'ro', isa => 'Str', required => 1);
25              
26             has dist_class => (
27             is => 'ro',
28             isa => 'Str',
29             required => 1,
30             default => sub { 'Module::Faker::Dist' },
31             );
32              
33             has url => (
34             is => 'ro',
35             isa => 'Str',
36             default => sub {
37             my ($self) = @_;
38             my $url = "file://" . File::Spec->rel2abs($self->dest);
39             $url =~ s{(?<!/)$}{/};
40             return $url;
41             },
42             );
43              
44             has dist_dest => (
45             is => 'ro',
46             lazy => 1,
47             init_arg => undef,
48             default => sub { File::Spec->catdir($_[0]->dest, qw(authors id)) },
49             );
50              
51             BEGIN {
52             # These attributes are used to keep track of the indexes we'll write when we
53             # finish adding content to the CPAN::Faker. -- rjbs, 2008-05-07
54             for (qw(pkg_index author_index author_dir)) {
55             has "_$_" => (
56             is => 'ro',
57             isa => 'HashRef',
58             default => sub { {} },
59             init_arg => undef,
60             );
61             }
62             }
63              
64             sub __dor { defined $_[0] ? $_[0] : $_[1] }
65              
66              
67             sub make_cpan {
68             my ($self) = @_;
69              
70             for ($self->source) {
71             Carp::croak "source directory does not exist" unless -e;
72             Carp::croak "source directory is not a directory" unless -d;
73             }
74              
75             for ($self->dest) {
76             if (-e) {
77             Carp::croak "destination directory is not a directory" unless -d;
78              
79             opendir my $dir, $self->dest;
80             my @files = grep { $_ ne '.' and $_ ne '..' } readdir $dir;
81             Carp::croak "destination directory is not empty" if @files;
82             } else {
83             my $error;
84             # actually *using* $error is annoying; will sort it out later..?
85             # -- rjbs, 2011-04-18
86             Carp::croak "couldn't create destination"
87             unless File::Path::make_path($self->dest, { error => \$error });
88             }
89             }
90              
91             my $iter = File::Next::files($self->source);
92              
93             while (my $file = $iter->()) {
94             my $dist = $self->dist_class->from_file($file);
95             $self->add_dist($dist);
96             }
97              
98             $self->_update_author_checksums;
99              
100             $self->write_package_index;
101             $self->write_author_index;
102             $self->write_modlist_index;
103             $self->write_perms_index;
104             $self->write_perms_gz_index;
105             }
106              
107              
108             sub add_dist {
109             my ($self, $dist) = @_;
110              
111             my $archive = $dist->make_archive({
112             dir => $self->dist_dest,
113             author_prefix => 1,
114             });
115              
116             $self->_learn_author_of($dist);
117             $self->_maybe_index($dist);
118              
119             my ($author_dir) =
120             $dist->archive_filename({ author_prefix => 1 }) =~ m{\A(.+)/};
121              
122             $self->_author_dir->{ $author_dir } = 1;
123             }
124              
125             sub _update_author_checksums {
126             my ($self) = @_;
127              
128             my $dist_dest = File::Spec->catdir($self->dest, qw(authors id));
129              
130             for my $dir (keys %{ $self->_author_dir }) {
131             $dir = File::Spec->catdir($dist_dest, $dir);
132             CPAN::Checksums::updatedir($dir);
133             }
134             }
135              
136              
137             sub add_author {
138             my ($self, $pauseid, $info) = @_;
139             $self->_author_index->{$pauseid} = $info->{mailbox};
140             }
141              
142             sub _learn_author_of {
143             my ($self, $dist) = @_;
144              
145             my ($author) = $dist->authors;
146             my $pauseid = $dist->cpan_author;
147              
148             return unless $author and $pauseid;
149              
150             $self->add_author($pauseid => { mailbox => $author });
151             }
152              
153              
154             sub index_package {
155             my ($self, $package_name, $info) = @_;
156              
157             unless ($info->{dist_filename}) {
158             Carp::croak "invalid package entry: missing dist_filename";
159             }
160              
161             $self->_pkg_index->{$package_name} = {
162             version => $info->{version},
163             dist_filename => $info->{dist_filename},
164             dist_version => $info->{dist_version},
165             dist_author => $info->{dist_author},
166             };
167             }
168              
169             sub _index_pkg_obj {
170             my ($self, $pkg, $dist) = @_;
171             $self->index_package(
172             $pkg->name => {
173             version => $pkg->version,
174             dist_filename => $dist->archive_filename({ author_prefix => 1 }),
175             dist_version => $dist->version,
176             dist_author => $dist->cpan_author,
177             },
178             );
179             }
180              
181             sub _maybe_index {
182             my ($self, $dist) = @_;
183              
184             my $index = $self->_pkg_index;
185              
186             PACKAGE: for my $package ($dist->packages) {
187             if (my $e = $index->{ $package->name }) {
188             if (defined $package->version and not defined $e->{version}) {
189             $self->_index_pkg_obj($package, $dist);
190             next PACKAGE;
191             } elsif (not defined $package->version and defined $e->{version}) {
192             next PACKAGE;
193             } else {
194             my $pkg_cmp = versioncmp($package->version, $e->{version});
195              
196             if ($pkg_cmp == 1) {
197             $self->_index_pkg_obj($package, $dist);
198             next PACKAGE;
199             } elsif ($pkg_cmp == 0) {
200             if (versioncmp($dist->version, $e->{dist_version}) == 1) {
201             $self->_index_pkg_obj($package, $dist);
202             next PACKAGE;
203             }
204             }
205              
206             next PACKAGE;
207             }
208             } else {
209             $self->_index_pkg_obj($package, $dist);
210             }
211             }
212             }
213              
214              
215             sub write_author_index {
216             my ($self) = @_;
217              
218             my $index = $self->_author_index;
219              
220             my $index_dir = File::Spec->catdir($self->dest, 'authors');
221             File::Path::mkpath($index_dir);
222              
223             my $index_filename = File::Spec->catfile(
224             $index_dir,
225             '01mailrc.txt.gz',
226             );
227              
228             my $gz = Compress::Zlib::gzopen($index_filename, 'wb');
229              
230             for my $pauseid (sort keys %$index) {
231             $gz->gzwrite(qq[alias $pauseid "$index->{$pauseid}"\n])
232             or die "error writing to $index_filename"
233             }
234              
235             $gz->gzclose and die "error closing $index_filename";
236             }
237              
238             sub write_package_index {
239             my ($self) = @_;
240              
241             my $index = $self->_pkg_index;
242              
243             my @lines;
244             for my $name (sort keys %$index) {
245             my $info = $index->{ $name };
246              
247             push @lines, sprintf "%-34s %5s %s\n",
248             $name,
249             __dor($info->{version}, 'undef'),
250             $info->{dist_filename};
251             }
252              
253             my $front = $self->_02pkg_front_matter({ lines => scalar @lines });
254              
255             my $index_dir = File::Spec->catdir($self->dest, 'modules');
256             File::Path::mkpath($index_dir);
257              
258             my $index_filename = File::Spec->catfile(
259             $index_dir,
260             '02packages.details.txt.gz',
261             );
262              
263             my $gz = Compress::Zlib::gzopen($index_filename, 'wb');
264             $gz->gzwrite("$front\n");
265             $gz->gzwrite($_) || die "error writing to $index_filename" for @lines;
266             $gz->gzclose and die "error closing $index_filename";
267             }
268              
269             sub write_modlist_index {
270             my ($self) = @_;
271              
272             my $index_dir = File::Spec->catdir($self->dest, 'modules');
273              
274             my $index_filename = File::Spec->catfile(
275             $index_dir,
276             '03modlist.data.gz',
277             );
278              
279             my $gz = Compress::Zlib::gzopen($index_filename, 'wb');
280             $gz->gzwrite(${ $self->section_data('modlist') });
281             $gz->gzclose and die "error closing $index_filename";
282             }
283              
284             sub _perms_index_filename {
285             my ($self) = @_;
286             my $index_dir = File::Spec->catdir($self->dest, 'modules');
287              
288             return File::Spec->catfile(
289             $index_dir,
290             '06perms.txt',
291             );
292             }
293              
294             sub write_perms_index {
295             my ($self) = @_;
296              
297             my $index_filename = $self->_perms_index_filename;
298              
299             my $template = $self->section_data('packages');
300              
301             my $index = $self->_pkg_index;
302             my $lines = keys %$index;
303              
304             my $text = Text::Template->fill_this_in(
305             $$template,
306             DELIMITERS => [ '{{', '}}' ],
307             HASH => {
308             lines => \$lines,
309             self => \$self,
310             },
311             );
312              
313             open my $fh, '>', $index_filename
314             or die "can't open $index_filename for writing: $!";
315              
316             print {$fh} $text, "\n";
317              
318             for my $pkg (sort keys %$index) {
319             my $author = $index->{$pkg}{dist_author};
320              
321             printf {$fh} "%s,%s,%s\n", $pkg, $author, 'f';
322             }
323              
324             close $fh or die "error closing $index_filename after writing: $!";
325             }
326              
327             sub write_perms_gz_index {
328             my ($self) = @_;
329              
330             my $index_filename = $self->_perms_index_filename;
331             my $index_gz_fname = "$index_filename.gz";
332             gzip($index_filename, $index_gz_fname)
333             or confess "gzip failed: $GzipError"
334             }
335              
336             sub _02pkg_front_matter {
337             my ($self, $arg) = @_;
338              
339             my $template = $self->section_data('packages');
340              
341             my $text = Text::Template->fill_this_in(
342             $$template,
343             DELIMITERS => [ '{{', '}}' ],
344             HASH => {
345             self => \$self,
346             (map {; $_ => \($arg->{$_}) } keys %$arg),
347             },
348             );
349              
350             return $text;
351             }
352              
353             no Moose;
354             1;
355              
356             =pod
357              
358             =encoding UTF-8
359              
360             =head1 NAME
361              
362             CPAN::Faker - build a bogus CPAN instance for testing
363              
364             =head1 VERSION
365              
366             version 0.010
367              
368             =head1 SYNOPSIS
369              
370             use CPAN::Faker;
371              
372             my $cpan = CPAN::Faker->new({
373             source => './eg',
374             dest => './will-contain-fakepan',
375             });
376              
377             $cpan->make_cpan;
378              
379             =head1 DESCRIPTION
380              
381             First things first: this is a pretty special-needs module. It's for people who
382             are writing tools that will operate against a copy of the CPAN (or something
383             just like it), and who need data to test those tools against.
384              
385             Because the real CPAN is constantly changing, and a mirror of the CPAN is a
386             pretty big chunk of data to deal with, CPAN::Faker lets you build a fake
387             CPAN-like directory tree out of simple descriptions of the distributions that
388             should be in your fake CPAN.
389              
390             =head1 METHODS
391              
392             =head2 new
393              
394             my $faker = CPAN::Faker->new(\%arg);
395              
396             This create the new CPAN::Faker. All arguments may be accessed later by
397             methods of the same name. Valid arguments are:
398              
399             source - the directory in which to find source files
400             dest - the directory in which to construct the CPAN instance; required
401             url - the base URL for the CPAN; a file:// URL is generated by default
402              
403             dist_class - the class used to fake dists; default: Module::Faker::Dist
404              
405             =head2 make_cpan
406              
407             $faker->make_cpan;
408              
409             This method makes the CPAN::Faker do its job. It iterates through all the
410             files in the source directory and builds a distribution object. Distribution
411             archives are written out into the author's directory, distribution contents are
412             (potentially) added to the index, CHECKSUMS files are created, and the indices
413             are then written out.
414              
415             =head2 add_author
416              
417             $faker->add_author($pause_id => \%info);
418              
419             Low-level method for populating C<01mailrc>. Only likely to be useful if you
420             are not calling C<make_cpan>. If the author is already known, the info on file
421             is replaced.
422              
423             The C<%info> hash is expected to contain the following data:
424              
425             mailbox - a string like: Ricardo Signes <rjbs@cpan.org>
426              
427             =head2 index_package
428              
429             $faker->index_package($package_name => \%info);
430              
431             This is a low-level method for populating the structure that will used to
432             produce the C<02packages> index.
433              
434             This method is only likely to be useful if you are not calling C<make_cpan>.
435              
436             C<%entry> is expected to contain the following entries:
437              
438             version - the version of the package (defaults to undef)
439             dist_version - the version of the dist (defaults to undef)
440             dist_filename - the file containing the package, like R/RJ/RJBS/...tar.gz
441             dist_author - the PAUSE id of the uploader of the dist
442              
443             =head2 write_author_index
444              
445             =head2 write_package_index
446              
447             =head2 write_modlist_index
448              
449             =head2 write_perms_index
450              
451             =head2 write_perms_gz_index
452              
453             All these are automatically called by C<make_cpan>; you probably do not need to
454             call them yourself.
455              
456             Write C<01mailrc.txt.gz>, C<02packages.details.txt.gz>, C<03modlist.data.gz>,
457             C<06perms.txt>, and C<06perms.txt.gz> respectively.
458              
459             =head1 THE CPAN INTERFACE
460              
461             A CPAN instance is just a set of files in known locations. At present,
462             CPAN::Faker will create the following files:
463              
464             ./authors/01mailrc.txt.gz - the list of authors (PAUSE ids)
465             ./modules/02packages.details.txt.gz - the master index of current modules
466             ./modules/03modlist.txt.gz - the "registered" list; has no data
467             ./authors/id/X/Y/XYZZY/Dist-1.tar.gz - each distribution in the archive
468             ./authors/id/X/Y/XYZZY/CHECKSUMS - a CPAN checksums file for the dir
469              
470             Note that while the 03modlist file is created, for the sake of the CPAN client,
471             the file contains no data about registered modules. This may be addressed in
472             future versions.
473              
474             Other files that are not currently created, but may be in the future are:
475              
476             ./indices/find-ls.gz
477             ./indices/ls-lR.gz
478             ./modules/by-category/...
479             ./modules/by-module/...
480              
481             If there are other files that you'd like to see created (or if you want to ask
482             to get the creation of one of the above implemented soon), please contact the
483             current maintainer (see below).
484              
485             =head2 add_dist
486              
487             $faker->add_dist($dist);
488              
489             This method expects a L<Module::Faker::Dist> object, for which it will
490             construct an archive, index the author and (maybe) the contents.
491              
492             =head1 AUTHOR
493              
494             Ricardo Signes <rjbs@cpan.org>
495              
496             =head1 COPYRIGHT AND LICENSE
497              
498             This software is copyright (c) 2008 by Ricardo Signes.
499              
500             This is free software; you can redistribute it and/or modify it under
501             the same terms as the Perl 5 programming language system itself.
502              
503             =cut
504              
505             __DATA__
506             __[packages]__
507             File: 02packages.details.txt
508             URL: {{ $self->url }}modules/02packages.details.txt.gz
509             Description: Package names found in directory $CPAN/authors/id/
510             Columns: package name, version, path
511             Intended-For: Automated fetch routines, namespace documentation.
512             Written-By: CPAN::Faker version {{ $CPAN::Faker::VERSION }}
513             Line-Count: {{ $lines }}
514             Last-Updated: {{ scalar gmtime }} GMT
515             __[perms]__
516             File: 06perms.txt
517             Description: CSV file of upload permission to the CPAN per namespace
518             best-permission is one of "m" for "modulelist", "f" for
519             "first-come", "c" for "co-maint"
520             Columns: package,userid,best-permission
521             Line-Count: {{ $lines }}
522             Written-By: Id
523             Date: {{ scalar gmtime }} GMT
524             __[modlist]__
525             File: 03modlist.data
526             Description: CPAN::Faker does not provide modlist data.
527             Modcount: 0
528             Written-By: CPAN::Faker version {{ $CPAN::Faker::VERSION }}
529             Date: {{ scalar localtime }}
530              
531             package CPAN::Modulelist;
532             # Usage: print Data::Dumper->new([CPAN::Modulelist->data])->Dump or similar
533             # cannot 'use strict', because we normally run under Safe
534             # use strict;
535             sub data {
536             my $result = {};
537             my $primary = "modid";
538             for (@$CPAN::Modulelist::data){
539             my %hash;
540             @hash{@$CPAN::Modulelist::cols} = @$_;
541             $result->{$hash{$primary}} = \%hash;
542             }
543             $result;
544             }
545             $CPAN::Modulelist::cols = [
546             'modid',
547             'statd',
548             'stats',
549             'statl',
550             'stati',
551             'statp',
552             'description',
553             'userid',
554             'chapterid'
555             ];
556             $CPAN::Modulelist::data = [];