From 90fbfc292ebd1a51272fa2e7f749f347f814ee76 Mon Sep 17 00:00:00 2001 From: Packit Date: Sep 24 2020 09:30:53 +0000 Subject: perl-CPAN-Changes-0.400002 base --- diff --git a/Changes b/Changes new file mode 100644 index 0000000..aeb263c --- /dev/null +++ b/Changes @@ -0,0 +1,224 @@ +Revision history for perl module CPAN::Changes + +0.400002 - 2015-06-21 + - revert whitespace changes that were inadvertantly included in previous + release. + - escape curly brackets in test to avoid warning in perl 5.22 + +0.400001 - 2015-05-23 + - accept either UTF-8 or ISO-8859-1 files and decode them + - only treat bracketed text if it is on its own line with no other brackets + - bump version.pm prereq to make sure it works properly + - Let long tokens (like URLs) overflow rather than splitting them into + multiple likes + - Don't wrap on non-breaking spaces + - Clean up packaging + - Return undef for dates or notes that don't exist + - Quote meta chars to fix a problem that clobbered dates + with timezones due to the '+' char (GH #20) + +0.30 2014-07-26 + + - Fix for subclassing CPAN::Changes::Group (GH #23) + +0.29 2014-07-23 + + - Groups are now objects (CPAN::Changes::Group). Backwards + compatibility from hashes should be preserved (GH #22) + +0.28 2014-06-10 + + - Add "SEE ALSO" links to similar modules (RT #94636) + + - Use perl 5.8-compatible regex + +0.27 2013-12-13 + + [ Spec Changes ] + + - Bump version to 0.04 + + - Allow non-"word" characters between a Version and a Date + +0.26 2013-11-21 + + - Fix reference issues when adding a release (RT #90605) + +0.25 2013-10-08 + + - Move Text::Wrap usage to proper module (Michal Spacek) + + - Typo fix (Karen Etheridge) + +0.24 2013-10-07 + + - Fix Dist::Zilla date parsing. Now puts timezone data in + note section (Github #17) + +0.23 2013-08-14 + + [ Spec Changes ] + + - Bump version to 0.03 + + - Allow the "T" marker in W3CDTF to be omitted + + [ Code Changes ] + + - Be more strict about what we consider to be a Dist::Zilla-style date + to avoid false positive matches + + - Update W3CDTF parsing to make the "T" marker optional (RT #87499) + + - Fix extra whitespace for empty values after version (RT #87524) + +0.22 2013-07-30 + + - Sync module versions (RT #87455) + +0.21 2013-07-30 + + [ Spec Changes ] + + - Bump version to 0.02 + + - Added "unknown/dev" release date options (RT #67705) + + - Added optional release note (RT #69321) + + - Added another preamble example + + - Added a note about line length + + [ Code Changes ] + + - Require Test::More 0.96 (RT #84994) + + - Added --check and --help flags to tidy_changelog script (Gabor Szabo) + + - Properly parse multi-line preamble + + - Test::CPAN::Changes now warns about parsed dates not in spec-compliant form + + - Handle unknown/dev release dates and release note from new spec + +0.20 2013-05-01 + + - 'delete_empty_groups' shouldn't erronously delete default group + (Yanick Champoux) + + - Add tidy_changelog utility script (Yanick Champoux) + + - Minor pod fix + +0.19 2012-04-30 + + - Test::CPAN::Changes now accepts version entries ending in '-TRIAL' (RT + #76882, Karen Etheridge) + + - releases() in CPAN::Changes also accepts entries ending in '-TRIAL' + +0.18 2011-10-18 + + - Expand changes_file_ok() to accept arguments so that a specific version + may be checked (Ricardo Signes) + + - Add $VERSION to Test::CPAN::Changes so it plays nice with the toolchain + e.g Module::Install::AuthorRequres (Dan Brook) + +0.17 2011-04-21 + + - Eliminate extra whitespace when release date is not defined (RT #67441) + + - Require version.pm 0.79, which introduced the $LAX regexp (RT #67613) + + - Add the option to sort groups (Yanick Champoux) + +0.16 2011-04-12 + + - Expose W3CDTF regex variable + + - Allow whitespace in some dates + + - Extract out valid W3CDTF portions from dates + + - Requires perl 5.10 + +0.15 2011-04-11 + + - Handle more date/time formats during parsing + +0.14 2011-04-11 + + - Add delete_empty_groups() to Changes.pm and Release.pm (Yanick Champoux) + +0.13 2011-04-04 + + - Use version.pm's LAX regex for finding versions. + +0.12 2011-04-04 + + - Sort releases() by version first for greater consistency + +0.11 2011-03-31 + + - Parse the default Dist-Zilla date format (e.g. 2010-12-28 00:15:12 + Europe/London) + +0.10 2011-03-29 + + - Be more strict about date validation in Test::CPAN::Changes + +0.09 2011-03-29 + + - Be more lenient when parsing dates. Timestamps (e.g. + Tue Mar 29 08:32:16 2011) are now parsed and converted to W3CDTF. (Fixes + RT #66862) + +0.08 2011-03-14 + + - Handle inconsistent indentation between releases + + - Be more strict about parsing rules for grouping lines + +0.07 2011-03-03 + + - Wrap version parsing in eval() + +0.06 2011-02-15 + + - Add proper version sorting via version.pm + + - Update dist manifest to include tests missing from previous releases + +0.05 2011-02-13 + + - Attempt to squash warnings for undefined dates and add rudimentary + version sorting when dates are equal + + - Add support for a "next version" token (Yanick Champoux) + + - Proper definition of whitespace between version and date + (Fixes RT #65678) + +0.04 2011-02-10 + + - Remove done_testing() from a test. + +0.03 2011-02-10 + + - Allow more than one space between the version and the date. The spec + allowed for this, but the parser did not. (Dave Rolsky) + + - Handle the case where there is no space before the change marker + correctly. (Dave Rolsky) + +0.02 2011-02-08 + + - Make tests compatible with Test::More that ships with perl 5.10.0 + (Fixes RT #65543) + +0.01 2011-02-02 + + - Initial release + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..dc32e8d --- /dev/null +++ b/MANIFEST @@ -0,0 +1,55 @@ +bin/tidy_changelog +Changes +lib/CPAN/Changes.pm +lib/CPAN/Changes/Group.pm +lib/CPAN/Changes/Release.pm +lib/CPAN/Changes/Spec.pod +lib/Test/CPAN/Changes.pm +maint/Makefile.PL.include +Makefile.PL +MANIFEST This list of files +t/corpus/basic.changes +t/corpus/different-indentation.changes +t/corpus/dist-zilla.changes +t/corpus/dist-zilla_format.changes +t/corpus/group-brackets.changes +t/corpus/group.changes +t/corpus/latin1.changes +t/corpus/line-continuation.changes +t/corpus/long_preamble.changes +t/corpus/multiple_releases.changes +t/corpus/no-leading-space-for-change.changes +t/corpus/preamble.changes +t/corpus/space-before-date.changes +t/corpus/timestamp.changes +t/corpus/unknown_date.changes +t/corpus/utf8.changes +t/corpus/version-date-separator.changes +t/delete_empty_groups.t +t/dist-zilla-changes.t +t/moo_lazy_subclass.t +t/read_basic.t +t/read_different-indentation.t +t/read_dist-zilla.t +t/read_encoded.t +t/read_group-brackets.t +t/read_group.t +t/read_line-continuation.t +t/read_multiple_releases.t +t/read_no-leading-space-for-change.t +t/read_no_date.t +t/read_preamble.t +t/read_space-before-date.t +t/read_timestamp.t +t/read_unknown_date.t +t/read_version-date-separator.t +t/rt90605.t +t/self.t +t/serialize.t +t/sort_groups.t +t/valid_dates.t +xt/release/pod.t +xt/release/pod_coverage.t +META.yml Module YAML meta-data (added by MakeMaker) +META.json Module JSON meta-data (added by MakeMaker) +README README file (added by Distar) diff --git a/META.json b/META.json new file mode 100644 index 0000000..f38ea1e --- /dev/null +++ b/META.json @@ -0,0 +1,65 @@ +{ + "abstract" : "Read and write Changes files", + "author" : [ + "Brian Cassidy " + ], + "dynamic_config" : 1, + "generated_by" : "ExtUtils::MakeMaker version 7.0401, CPAN::Meta::Converter version 2.150001", + "license" : [ + "perl_5" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : "2" + }, + "name" : "CPAN-Changes", + "no_index" : { + "directory" : [ + "t", + "xt" + ] + }, + "prereqs" : { + "build" : {}, + "configure" : { + "requires" : { + "ExtUtils::MakeMaker" : "0" + } + }, + "develop" : { + "requires" : { + "Test::Pod" : "1", + "Test::Pod::Coverage" : "1" + } + }, + "runtime" : { + "requires" : { + "Text::Wrap" : "0.003", + "version" : "0.9906" + } + }, + "test" : { + "requires" : { + "Test::More" : "0.96" + } + } + }, + "release_status" : "stable", + "resources" : { + "bugtracker" : { + "mailto" : "bug-CPAN-Changes@rt.cpan.org", + "web" : "https://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Changes" + }, + "license" : [ + "http://dev.perl.org/licenses/" + ], + "repository" : { + "type" : "git", + "url" : "https://github.com/CPAN-API/CPAN-Changes.git", + "web" : "https://github.com/CPAN-API/CPAN-Changes" + }, + "x_IRC" : "irc://irc.perl.org/#metacpan" + }, + "version" : "0.400002", + "x_authority" : "cpan:BRICAS" +} diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..fe08cd5 --- /dev/null +++ b/META.yml @@ -0,0 +1,29 @@ +--- +abstract: 'Read and write Changes files' +author: + - 'Brian Cassidy ' +build_requires: + Test::More: '0.96' +configure_requires: + ExtUtils::MakeMaker: '0' +dynamic_config: 1 +generated_by: 'ExtUtils::MakeMaker version 7.0401, CPAN::Meta::Converter version 2.150001' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: CPAN-Changes +no_index: + directory: + - t + - xt +requires: + Text::Wrap: '0.003' + version: '0.9906' +resources: + IRC: irc://irc.perl.org/#metacpan + bugtracker: https://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Changes + license: http://dev.perl.org/licenses/ + repository: https://github.com/CPAN-API/CPAN-Changes.git +version: '0.400002' +x_authority: cpan:BRICAS diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..960bb79 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,94 @@ +use strict; +use warnings FATAL => 'all'; +use 5.008; + +my %META = ( + name => 'CPAN-Changes', + license => 'perl_5', + prereqs => { + configure => { requires => { + 'ExtUtils::MakeMaker' => 0, + } }, + test => { + requires => { + 'Test::More' => 0.96, + }, + }, + runtime => { + requires => { + 'Text::Wrap' => 0.003, + 'version' => 0.9906, + }, + }, + develop => { + requires => { + 'Test::Pod' => 1.00, + 'Test::Pod::Coverage' => 1.00, + }, + }, + }, + resources => { + repository => { + url => 'https://github.com/CPAN-API/CPAN-Changes.git', + web => 'https://github.com/CPAN-API/CPAN-Changes', + type => 'git', + }, + x_IRC => 'irc://irc.perl.org/#metacpan', + bugtracker => { + web => 'https://rt.cpan.org/Public/Dist/Display.html?Name=CPAN-Changes', + mailto => 'bug-CPAN-Changes@rt.cpan.org', + }, + license => [ 'http://dev.perl.org/licenses/' ], + }, + no_index => { + directory => [ 't', 'xt' ] + }, + x_authority => 'cpan:BRICAS', +); + +my %MM_ARGS = ( + EXE_FILES => [ 'bin/tidy_changelog' ], +); + +## BOILERPLATE ############################################################### +require ExtUtils::MakeMaker; +(do 'maint/Makefile.PL.include' or die $@) unless -f 'META.yml'; + +# have to do this since old EUMM dev releases miss the eval $VERSION line +my $eumm_version = eval $ExtUtils::MakeMaker::VERSION; +my $mymeta = $eumm_version >= 6.57_02; +my $mymeta_broken = $mymeta && $eumm_version < 6.57_07; + +($MM_ARGS{NAME} = $META{name}) =~ s/-/::/g; +($MM_ARGS{VERSION_FROM} = "lib/$MM_ARGS{NAME}.pm") =~ s{::}{/}g; +$META{license} = [ $META{license} ] + if $META{license} && !ref $META{license}; +$MM_ARGS{LICENSE} = $META{license}[0] + if $META{license} && $eumm_version >= 6.30; +$MM_ARGS{NO_MYMETA} = 1 + if $mymeta_broken; +$MM_ARGS{META_ADD} = { 'meta-spec' => { version => 2 }, %META } + unless -f 'META.yml'; + +for (qw(configure build test runtime)) { + my $key = $_ eq 'runtime' ? 'PREREQ_PM' : uc $_.'_REQUIRES'; + my $r = $MM_ARGS{$key} = { + %{$META{prereqs}{$_}{requires} || {}}, + %{delete $MM_ARGS{$key} || {}}, + }; + defined $r->{$_} or delete $r->{$_} for keys %$r; +} + +$MM_ARGS{MIN_PERL_VERSION} = delete $MM_ARGS{PREREQ_PM}{perl} || 0; + +delete $MM_ARGS{MIN_PERL_VERSION} + if $eumm_version < 6.47_01; +$MM_ARGS{BUILD_REQUIRES} = {%{$MM_ARGS{BUILD_REQUIRES}}, %{delete $MM_ARGS{TEST_REQUIRES}}} + if $eumm_version < 6.63_03; +$MM_ARGS{PREREQ_PM} = {%{$MM_ARGS{PREREQ_PM}}, %{delete $MM_ARGS{BUILD_REQUIRES}}} + if $eumm_version < 6.55_01; +delete $MM_ARGS{CONFIGURE_REQUIRES} + if $eumm_version < 6.51_03; + +ExtUtils::MakeMaker::WriteMakefile(%MM_ARGS); +## END BOILERPLATE ########################################################### diff --git a/README b/README new file mode 100644 index 0000000..b5797b3 --- /dev/null +++ b/README @@ -0,0 +1,145 @@ +NAME + CPAN::Changes - Read and write Changes files + +SYNOPSIS + # Load from file + my $changes = CPAN::Changes->load( 'Changes' ); + + # Create a new Changes file + $changes = CPAN::Changes->new( + preamble => 'Revision history for perl module Foo::Bar' + ); + + $changes->add_release( { + version => '0.01', + date => '2009-07-06', + } ); + + $changes->serialize; + +DESCRIPTION + It is standard practice to include a Changes file in your distribution. + The purpose the Changes file is to help a user figure out what has + changed since the last release. + + People have devised many ways to write the Changes file. A preliminary + specification has been created (CPAN::Changes::Spec) to encourage module + authors to write clear and concise Changes. + + This module will help users programmatically read and write Changes + files that conform to the specification. + +METHODS + new( %args ) + Creates a new object using %args as the initial data. + + "next_token" + Used to passes a regular expression for a "next version" placeholder + token. See "DEALING WITH "NEXT VERSION" PLACEHOLDERS" for an example + of its usage. + + load( $filename, %args ) + Parses $filename as per CPAN::Changes::Spec. If present, the optional + %args are passed to the underlaying call to "new()". + + load_string( $string, %args ) + Parses $string as per CPAN::Changes::Spec. If present, the optional + %args are passed to the underlaying call to "new()". + + preamble( [ $preamble ] ) + Gets/sets the preamble section. + + releases( [ @releases ] ) + Without any arguments, a list of current release objects is returned + sorted by ascending release date. When arguments are specified, all + existing releases are removed and replaced with the supplied + information. Each release may be either a regular hashref, or a + CPAN::Changes::Release object. + + # Hashref argument + $changes->releases( { version => '0.01', date => '2009-07-06' } ); + + # Release object argument + my $rel = CPAN::Changes::Release->new( + version => '0.01', date => '2009-07-06' + ); + $changes->releases( $rel ); + + add_release( @releases ) + Adds the release to the changes file. If a release at the same version + exists, it will be overwritten with the supplied data. + + delete_release( @versions ) + Deletes all of the releases specified by the versions supplied to the + method. + + release( $version ) + Returns the release object for the specified version. Should there be no + matching release object, undef is returned. + + serialize( reverse => $boolean, group_sort => \&sorting_function ) + Returns all of the data as a string, suitable for saving as a Changes + file. + + If *reverse* is provided and true, the releases are printed in the + reverse order (oldest to latest). + + If *group_sort* is provided, change groups are sorted according to the + given function. If not, groups are sorted alphabetically. + + delete_empty_groups( ) + Deletes change groups without changes in all releases. + +DEALING WITH "NEXT VERSION" PLACEHOLDERS + In the working copy of a distribution, it's not uncommon to have a "next + release" placeholder section as the first entry of the "Changes" file. + + For example, the "Changes" file of a distribution using Dist::Zilla and + Dist::Zilla::Plugin::NextRelease would look like: + + Revision history for Foo-Bar + + {{$NEXT}} + - Add the 'frobuscate' method. + + 1.0.0 2010-11-30 + - Convert all comments to Esperanto. + + 0.0.1 2010-09-29 + - Original version unleashed on an unsuspecting world + + To have "CPAN::Changes" recognizes the "{{$NEXT}}" token as a valid + version, you can use the "next_token" argument with any of the class' + constructors. Note that the resulting release object will also be + considered the latest release, regardless of its timestamp. + + To continue with our example: + + # recognizes {{$NEXT}} as a version + my $changes = CPAN::Changes->load( + 'Changes', + next_token => qr/{{\$NEXT}}/, + ); + + my @releases = $changes->releases; + print $releases[-1]->version; # prints '{{$NEXT}}' + +SEE ALSO + * CPAN::Changes::Spec + + * Test::CPAN::Changes + + SIMILAR MODULES + * Module::Metadata::Changes + + * Module::Changes + +AUTHOR + Brian Cassidy + +COPYRIGHT AND LICENSE + Copyright 2011-2013 by Brian Cassidy + + This library is free software; you can redistribute it and/or modify it + under the same terms as Perl itself. + diff --git a/bin/tidy_changelog b/bin/tidy_changelog new file mode 100755 index 0000000..e32a7d9 --- /dev/null +++ b/bin/tidy_changelog @@ -0,0 +1,103 @@ +#!/usr/bin/perl + +use CPAN::Changes; + +use Getopt::Long qw(GetOptions); +use Pod::Usage qw(pod2usage); + +GetOptions( \my %opt, + 'next!', + 'token:s', + 'headers!', + 'reverse', + 'check', + 'help', +) or pod2usage( -verbose => 2 ); +pod2usage( -verbose => 2 ) if $opt{help}; + +$opt{token} ||= qr/\{\{\$NEXT\}\}/; + +my $changelog = shift; + +unless ( $changelog ) { + # try to guess it + opendir my $dir, '.'; + my @files = grep { -f $_ and /^change/i } readdir $dir; + die "changelog not provided and couldn't be guessed\n" + unless @files == 1; + + $changelog = shift @files; + warn "changelog not provided, guessing '$changelog'\n\n"; +} + +if ($opt{check}) { + require Test::CPAN::Changes; + require Test::More; + Test::CPAN::Changes::changes_file_ok(); + exit; +} + +my $changes = CPAN::Changes->load( + $changelog, + ( next_token => $opt{token} ) x $opt{next}, +); + +if( $opt{headers} ) { + $_->clear_changes for $changes->releases; +} + +print $changes->serialize( + reverse => $opt{reverse}, +); + +__END__ + +=head1 NAME + +tidy_changelog - command-line tool for CPAN::Changes + +=head1 SYNOPSIS + + $ tidy_changelog Changelog + +=head1 DESCRIPTION + +Takes a changelog file, parse it using L and prints out +the resulting output. If a file is not given, the program will see if +there is one file in the current directory beginning by 'change' +(case-insensitive) and, if so, assume it to be the changelog. + +=head1 ARGUMENTS + +=head2 --next + +If provided, assumes that there is a placeholder +header for an upcoming next release. The placeholder token +is given via I<--token>. + +=head2 --token + +Regular expression to use to detect the token for an upcoming +release if I<--next> is used. If not explicitly given, defaults +to C<\{\{\$NEXT\}\}>. + +=head2 --headers + +If given, only print out the release header lines, without any of the +changes. + +=head2 --reverse + +Prints the releases in reverse order (from the oldest to latest). + + +=head2 --check + +Only check if the changelog is formatted properly using the changes_file_ok +function of L. + + +=head2 --help + +This help + diff --git a/lib/CPAN/Changes.pm b/lib/CPAN/Changes.pm new file mode 100644 index 0000000..59f7721 --- /dev/null +++ b/lib/CPAN/Changes.pm @@ -0,0 +1,493 @@ +package CPAN::Changes; + +use strict; +use warnings; + +use CPAN::Changes::Release; +use Scalar::Util (); +use version (); +use Encode qw(decode FB_CROAK LEAVE_SRC); + +our $VERSION = '0.400002'; + +# From DateTime::Format::W3CDTF +our $W3CDTF_REGEX = qr{(\d\d\d\d) # Year + (?:-(\d\d) # -Month + (?:-(\d\d) # -Day + (?:[T\s] + (\d\d):(\d\d) # Hour:Minute + (?: + :(\d\d) # :Second + (\.\d+)? # .Fractional_Second + )? + ( Z # UTC + | [+-]\d\d:\d\d # Hour:Minute TZ offset + (?::\d\d)? # :Second TZ offset + )?)?)?)?}x; + +my @m = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ); +my %months = map { $m[ $_ ] => $_ + 1 } 0 .. 11; + +our $UNKNOWN_VALS = join( '|', ( + 'Unknown Release Date', 'Unknown', 'Not Released', 'Development Release', + 'Development', 'Developer Release', +) ); + +sub new { + my $class = shift; + return bless { + preamble => '', + releases => {}, + months => \%months, + @_, + }, $class; +} + +sub load { + my ( $class, $file, @args ) = @_; + + open( my $fh, '<:raw', $file ) or die $!; + + my $content = do { local $/; <$fh> }; + + close $fh; + + # if it's valid UTF-8, decode that. otherwise, assume latin 1 and leave it. + eval { $content = decode('UTF-8', $content, FB_CROAK | LEAVE_SRC) }; + + return $class->load_string( $content, @args ); +} + +sub load_string { + my ( $class, $string, @args ) = @_; + + my $changes = $class->new( @args ); + my $preamble = ''; + my ( @releases, $ingroup, $indent ); + + $string =~ s/(?:\015{1,2}\012|\015|\012)/\n/gs; + my @lines = split( "\n", $string ); + + my $version_line_re + = $changes->{ next_token } + ? qr/^(?:$version::LAX|$changes->{next_token})/ + : qr/^$version::LAX/; + + $preamble .= shift( @lines ) . "\n" while @lines && $lines[ 0 ] !~ $version_line_re; + + for my $l ( @lines ) { + + # Version & Date + if ( $l =~ $version_line_re ) { + my ( $v, $n ) = split m{\s[\W\s]*}, $l, 2; + my $match = ''; + my $d; + + # munge date formats, save the remainder as note + if ( $n ) { + # unknown dates + if ( $n =~ m{^($UNKNOWN_VALS)}i ) { + $d = $1; + $match = $d; + } + # handle localtime-like timestamps + elsif ( $n + =~ m{^(\D{3}\s+(\D{3})\s+(\d{1,2})\s+([\d:]+)?\D*(\d{4}))} ) + { + $match = $1; + if ( $4 ) { + + # unfortunately ignores TZ data + $d = sprintf( + '%d-%02d-%02dT%sZ', + $5, $changes->{ months }->{ $2 }, + $3, $4 + ); + } + else { + $d = sprintf( '%d-%02d-%02d', + $5, $changes->{ months }->{ $2 }, $3 ); + } + } + + # RFC 2822 + elsif ( $n + =~ m{^(\D{3}, (\d{1,2}) (\D{3}) (\d{4}) (\d\d:\d\d:\d\d) ([+-])(\d{2})(\d{2}))} + ) + { + $match = $1; + $d = sprintf( + '%d-%02d-%02dT%s%s%02d:%02d', + $4, $changes->{ months }->{ $3 }, + $2, $5, $6, $7, $8 + ); + } + + # handle dist-zilla style, puts TZ data in note + elsif ( $n + =~ m{^((\d{4}-\d\d-\d\d)\s+(\d\d:\d\d(?::\d\d)?))(?:\s+[A-Za-z]+/[A-Za-z_-]+)} ) + { + $match = $1; + $d = sprintf( '%sT%sZ', $2, $3 ); + } + + # start with W3CDTF, ignore rest + elsif ( $n =~ m{^($W3CDTF_REGEX)} ) { + $match = $1; + $d = $match; + $d =~ s{ }{T}; + # Add UTC TZ if date ends at H:M, H:M:S or H:M:S.FS + $d .= 'Z' if length( $d ) == 16 || length( $d ) == 19 || $d =~ m{\.\d+$}; + } + + # clean date from note + $n =~ s{^\Q$match\E\s*}{}; + } + + undef $d unless length $d; + undef $n unless length $n; + + push @releases, + CPAN::Changes::Release->new( + version => $v, + date => $d, + _parsed_date => $match, + note => $n, + ); + $ingroup = undef; + $indent = undef; + next; + } + + # Grouping + if ( $l =~ m{^\s+\[\s*([^\[\]]+?)\s*\]\s*$} ) { + $ingroup = $1; + $releases[ -1 ]->add_group( $1 ); + next; + } + + $ingroup = '' if !defined $ingroup; + + next if $l =~ m{^\s*$}; + + if ( !defined $indent ) { + $indent + = $l =~ m{^(\s+)} + ? '\s' x length $1 + : ''; + } + + $l =~ s{^$indent}{}; + + # Inconsistent indentation between releases + if ( $l =~ m{^\s} && !@{ $releases[ -1 ]->changes( $ingroup ) } ) { + $l =~ m{^(\s+)}; + $indent = $1; + $l =~ s{^\s+}{}; + } + + # Change line cont'd + if ( $l =~ m{^\s} ) { + $l =~ s{^\s+}{}; + my $changeset = $releases[ -1 ]->changes( $ingroup ); + $changeset->[ -1 ] .= " $l"; + } + + # Start of Change line + else { + $l =~ s{^[^[:alnum:]]+\s}{}; # remove leading marker + $releases[ -1 ]->add_changes( { group => $ingroup }, $l ); + } + + } + + $changes->preamble( $preamble ); + $changes->releases( @releases ); + + return $changes; +} + +sub preamble { + my $self = shift; + + if ( @_ ) { + my $preamble = shift; + $preamble =~ s{\s+$}{}s; + $self->{ preamble } = $preamble; + } + + return $self->{ preamble }; +} + +sub releases { + my $self = shift; + + if ( @_ ) { + $self->{ releases } = {}; + $self->add_release( @_ ); + } + + my $sort_function = sub { + ( eval { + ( my $v = $a->version ) =~ s/-TRIAL$//; + version->parse( $v ); + } + || 0 + ) <=> ( + eval { + ( my $v = $b->version ) =~ s/-TRIAL$//; + version->parse( $v ); + } + || 0 + ) or ( $a->date || '' ) cmp( $b->date || '' ); + }; + + my $next_token = $self->{ next_token }; + + my $token_sort_function = sub { + $a->version =~ $next_token - $b->version =~ $next_token + or $sort_function->(); + }; + + my $sort = $next_token ? $token_sort_function : $sort_function; + + return sort $sort values %{ $self->{ releases } }; +} + +sub add_release { + my $self = shift; + + for my $release ( @_ ) { + my $new = Scalar::Util::blessed $release ? $release + : CPAN::Changes::Release->new( %$release ); + $self->{ releases }->{ $new->version } = $new; + } +} + +sub delete_release { + my $self = shift; + + delete $self->{ releases }->{ $_ } for @_; +} + +sub release { + my ( $self, $version ) = @_; + + return unless exists $self->{ releases }->{ $version }; + return $self->{ releases }->{ $version }; +} + +sub delete_empty_groups { + my $self = shift; + + $_->delete_empty_groups for $self->releases; +} + +sub serialize { + my $self = shift; + my %args = @_; + + my %release_args; + $release_args{ group_sort } = $args{ group_sort } if $args{ group_sort }; + + my $output; + + $output = $self->preamble . "\n\n" if $self->preamble; + + my @r = $self->releases; + @r = reverse @r unless $args{reverse}; # not a typo! + + $output .= $_->serialize( %release_args ) for @r; + $output =~ s/\n\n+\z/\n/; + + return $output; +} + +1; + +__END__ + +=head1 NAME + +CPAN::Changes - Read and write Changes files + +=head1 SYNOPSIS + + # Load from file + my $changes = CPAN::Changes->load( 'Changes' ); + + # Create a new Changes file + $changes = CPAN::Changes->new( + preamble => 'Revision history for perl module Foo::Bar' + ); + + $changes->add_release( { + version => '0.01', + date => '2009-07-06', + } ); + + $changes->serialize; + +=head1 DESCRIPTION + +It is standard practice to include a Changes file in your distribution. The +purpose the Changes file is to help a user figure out what has changed since +the last release. + +People have devised many ways to write the Changes file. A preliminary +specification has been created (L) to encourage module +authors to write clear and concise Changes. + +This module will help users programmatically read and write Changes files that +conform to the specification. + +=head1 METHODS + +=head2 new( %args ) + +Creates a new object using C<%args> as the initial data. + +=over + +=item C + +Used to passes a regular expression for a "next version" placeholder token. +See L for an example of its usage. + +=back + +=head2 load( $filename, %args ) + +Parses C<$filename> as per L. +If present, +the optional C<%args> are passed to the underlaying call to +C. + +=head2 load_string( $string, %args ) + +Parses C<$string> as per L. +If present, +the optional C<%args> are passed to the underlaying call to +C. + +=head2 preamble( [ $preamble ] ) + +Gets/sets the preamble section. + +=head2 releases( [ @releases ] ) + +Without any arguments, a list of current release objects is returned sorted +by ascending release date. When arguments are specified, all existing +releases are removed and replaced with the supplied information. Each release +may be either a regular hashref, or a L object. + + # Hashref argument + $changes->releases( { version => '0.01', date => '2009-07-06' } ); + + # Release object argument + my $rel = CPAN::Changes::Release->new( + version => '0.01', date => '2009-07-06' + ); + $changes->releases( $rel ); + +=head2 add_release( @releases ) + +Adds the release to the changes file. If a release at the same version exists, +it will be overwritten with the supplied data. + +=head2 delete_release( @versions ) + +Deletes all of the releases specified by the versions supplied to the method. + +=head2 release( $version ) + +Returns the release object for the specified version. Should there be no +matching release object, undef is returned. + +=head2 serialize( reverse => $boolean, group_sort => \&sorting_function ) + +Returns all of the data as a string, suitable for saving as a Changes +file. + +If I is provided and true, the releases are +printed in the reverse order (oldest to latest). + +If I is provided, change groups are +sorted according to the given function. If not, +groups are sorted alphabetically. + +=head2 delete_empty_groups( ) + +Deletes change groups without changes in all releases. + +=head1 DEALING WITH "NEXT VERSION" PLACEHOLDERS + +In the working copy of a distribution, it's not uncommon +to have a "next release" placeholder section as the first entry +of the C file. + +For example, the C file of a distribution using +L and L +would look like: + + + Revision history for Foo-Bar + + {{$NEXT}} + - Add the 'frobuscate' method. + + 1.0.0 2010-11-30 + - Convert all comments to Esperanto. + + 0.0.1 2010-09-29 + - Original version unleashed on an unsuspecting world + + +To have C recognizes the C<{{$NEXT}}> token as a valid +version, you can use the C argument with any of the class' +constructors. Note that the resulting release object will also +be considered the latest release, regardless of its timestamp. + +To continue with our example: + + # recognizes {{$NEXT}} as a version + my $changes = CPAN::Changes->load( + 'Changes', + next_token => qr/{{\$NEXT}}/, + ); + + my @releases = $changes->releases; + print $releases[-1]->version; # prints '{{$NEXT}}' + +=head1 SEE ALSO + +=over 4 + +=item * L + +=item * L + +=back + +=head2 SIMILAR MODULES + +=over 4 + +=item * L + +=item * L + +=back + +=head1 AUTHOR + +Brian Cassidy Ebricas@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright 2011-2013 by Brian Cassidy + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/lib/CPAN/Changes/Group.pm b/lib/CPAN/Changes/Group.pm new file mode 100644 index 0000000..cec8545 --- /dev/null +++ b/lib/CPAN/Changes/Group.pm @@ -0,0 +1,180 @@ +package CPAN::Changes::Group; + +use strict; +use warnings; + +use Text::Wrap (); + +sub new { + my $class = shift; + return bless { + changes => [], + @_, + }, $class; +} + +# Intentionally read only +# to prevent hash key and name being out of sync. +sub name { + my $self = shift; + if ( not exists $self->{ name } ) { + $self->{ name } = q[]; + } + return $self->{ name }; +} + +sub changes { + my $self = shift; + return $self->{ changes }; +} + +sub add_changes { + my $self = shift; + push @{ $self->{ changes } }, @_; +} + +sub set_changes { + my $self = shift; + $self->{ changes } = \@_; +} + +sub clear_changes { + my $self = shift; + $self->{ changes } = []; +} + +sub is_empty { + my $self = shift; + return !@{ $self->changes }; +} + +sub serialize { + my $self = shift; + my %args = @_; + + my $output = ''; + my $name = $self->name; + $output .= sprintf " [%s]\n", $name if length $name; + # change logs commonly have long URLs we shouldn't break, and by default + # Text::Wrap wraps on NONBREAKING SPACE. + local $Text::Wrap::break = '[\t ]'; + local $Text::Wrap::huge = 'overflow'; + $output .= Text::Wrap::wrap( ' - ', ' ', $_ ) . "\n" for @{ $self->changes }; + + return $output; +} + +1; + +__END__ + +=head1 NAME + +CPAN::Changes::Group - A group of related change information within a release + +=head1 SYNOPSIS + + my $rel = CPAN::Changes::Release->new( + version => '0.01', + date => '2009-07-06', + ); + + my $grp = CPAN::Changes::Group->new( + name => 'BugFixes', + ); + + $grp->add_changes( + 'Return a Foo object instead of a Bar object in foobar()' + ); + + $rel->attach_group( $grp ); # clobbers existing group if present. + +=head1 DESCRIPTION + +A release is made up of several groups. This object provides access +to all of the key data that embodies a such a group. + +For instance: + + 0.27 2013-12-13 + + - Foo + + [ Spec Changes ] + + - Bar + +Here, there are two groups, the second one, C< Spec Changes > and the first with the empty label C. + +=head1 METHODS + +=head2 new( %args ) + +Creates a new group object, using C<%args> as the default data. + + Group->new( + name => 'Some Group Name', + changes => [ ], + ); + +=head2 name() + +Returns the name of the group itself. + +=head2 changes( [ $group ] ) + +Gets the list of changes for this group as an arrayref of changes. + +=head2 add_changes( @changes ) + +Appends a list of changes to the group. + + $group->add_changes( 'Added foo() function' ); + +=head2 set_changes( @changes ) + +Replaces the existing list of changes with the supplied values. + +=head2 clear_changes( ) + +Clears all changes from the group. + +=head2 groups( sort => \&sorting_function ) + +Returns a list of current groups in this release. + +=head2 is_empty() + +Returns whether or not the given group has changes. + +=head2 serialize() + +Returns the group data as a string, suitable for inclusion in a Changes +file. + +=head1 SEE ALSO + +=over 4 + +=item * L + +=item * L + +=item * L + +=item * L + +=back + +=head1 AUTHOR + +Brian Cassidy Ebricas@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright 2011-2013 by Brian Cassidy + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/lib/CPAN/Changes/Release.pm b/lib/CPAN/Changes/Release.pm new file mode 100644 index 0000000..bf5a1b6 --- /dev/null +++ b/lib/CPAN/Changes/Release.pm @@ -0,0 +1,308 @@ +package CPAN::Changes::Release; + +use strict; +use warnings; + +use Text::Wrap (); +use CPAN::Changes::Group; +use Scalar::Util qw(blessed); + +sub new { + my $class = shift; + return bless { + changes => {}, + @_, + }, $class; +} + +sub version { + my $self = shift; + + if ( @_ ) { + $self->{ version } = shift; + } + + return $self->{ version }; +} + +sub date { + my $self = shift; + + if ( @_ ) { + $self->{ date } = shift; + } + + return $self->{ date }; +} + +sub note { + my $self = shift; + + if ( @_ ) { + $self->{ note } = shift; + } + + return $self->{ note }; +} + +sub changes { + my $self = shift; + + if ( @_ ) { + my $group = shift; + return unless exists $self->{ changes }->{ $group }; + return $self->{ changes }->{ $group }->changes; + } + + return { map { $_ => $self->{ changes }->{$_}->changes } keys %{ $self->{ changes } } }; +} + +sub add_changes { + my $self = shift; + my $group = ''; + + if ( ref $_[ 0 ] ) { + $group = shift->{ group }; + } + + $self->get_group( $group )->add_changes( @_ ); +} + +sub set_changes { + my $self = shift; + my $group = ''; + + if ( ref $_[ 0 ] ) { + $group = shift->{ group }; + } + + $self->get_group( $group )->set_changes(@_); +} + +sub clear_changes { + my $self = shift; + $self->{ changes } = {}; +} + +sub groups { + my $self = shift; + my %args = @_; + + $args{ sort } ||= sub { sort @_ }; + + return $args{ sort }->( keys %{ $self->{ changes } } ); +} + +sub get_group { + my $self = shift; + my $group = ''; + + if ( $_[ 0 ] ) { + $group = shift; + } + if ( !exists $self->{ changes }->{ $group } ) { + $self->{ changes }->{ $group } = CPAN::Changes::Group->new( name => $group ); + } + if ( not blessed $self->{changes}->{$group} ) { + $self->{ changes }->{ $group } = CPAN::Changes::Group->new( name => $group , changes => $self->{changes}->{$group} ); + } + + return $self->{ changes }->{ $group }; +} + +sub attach_group { + my $self = shift; + my $group = shift; + + die "Not a group" unless blessed $group; + + my $name = $group->name; + + $self->{changes}->{$name} = $group; + +} + +sub group_values { + my $self = shift; + return map { $self->get_group( $_ ) } $self->groups( @_ ); +} + +sub add_group { + my $self = shift; + $self->{ changes }->{ $_ } = CPAN::Changes::Group->new( name => $_ ) for @_; +} + +sub delete_group { + my $self = shift; + my @groups = @_; + + @groups = ( '' ) unless @groups; + + delete $self->{ changes }->{ $_ } for @groups; +} + +sub delete_empty_groups { + my $self = shift; + + $self->delete_group($_->name) + for grep { $_->is_empty } $self->group_values; +} + +sub serialize { + my $self = shift; + my %args = @_; + + my $output = join( ' ', grep { defined && length } ( $self->version, $self->date, $self->note ) ) + . "\n"; + + $output .= join "\n", + map { $_->serialize } $self->group_values( sort => $args{ group_sort } ); + $output .= "\n"; + + return $output; +} + +1; + +__END__ + +=head1 NAME + +CPAN::Changes::Release - Information about a particular release + +=head1 SYNOPSIS + + my $rel = CPAN::Changes::Release->new( + version => '0.01', + date => '2009-07-06', + ); + + $rel->add_changes( + { group => 'THINGS THAT MAY BREAK YOUR CODE' }, + 'Return a Foo object instead of a Bar object in foobar()' + ); + +=head1 DESCRIPTION + +A changelog is made up of one or more releases. This object provides access +to all of the key data that embodies a release including the version number, +date of release, and all of the changelog information lines. Any number of +changelog lines can be grouped together under a heading. + +=head1 METHODS + +=head2 new( %args ) + +Creates a new release object, using C<%args> as the default data. + +=head2 version( [ $version ] ) + +Gets/sets the version number for this release. + +=head2 date( [ $date ] ) + +Gets/sets the date for this release. + +=head2 note( [ $note ] ) + +Gets/sets the note for this release. + +=head2 changes( [ $group ] ) + +Gets the list of changes for this release as a hashref of group/changes +pairs. If a group name is specified, an array ref of changes for that group +is returned. Should that group not exist, undef is returned. + +=head2 add_changes( [ \%options ], @changes ) + +Appends a list of changes to the release. Specifying a C option +appends them to that particular group. NB: the default group is represented +by and empty string. + + # Append to default group + $release->add_changes( 'Added foo() function' ); + + # Append to a particular group + $release->add_changes( { group => 'Fixes' }, 'Fixed foo() function' ); + +=head2 set_changes( [ \%options ], @changes ) + +Replaces the existing list of changes with the supplied values. Specifying +a C option will only replace change items in that group. + +=head2 clear_changes( ) + +Clears all changes from the release. + +=head2 groups( sort => \&sorting_function ) + +Returns a list of current groups in this release. + +If I is provided, groups are +sorted according to the given function. If not, +they are sorted alphabetically. + +=head2 add_group( @groups ) + +Creates an empty group under the names provided. + +=head2 delete_group( @groups ) + +Deletes the groups of changes specified. + +=head2 delete_empty_groups( ) + +Deletes all groups that don't contain any changes. + +=head2 serialize( group_sort => \&sorting_function ) + +Returns the release data as a string, suitable for inclusion in a Changes +file. + +If I is provided, change groups are +sorted according to the given function. If not, +groups are sorted alphabetically. + +=head2 get_group( [ $name ] ) + +Returns the internal L object for the group C<$name>. + +If C<$name> is not specified, the C group C<('')> will be returned. + +If C<$name> does not exist, a L object will be created, and returned. + +=head2 attach_group( $group_object ) + +Attach a L object to the C<::Release>. Note that the name is B specified, +as it is instead determined from C<< $group_object->name >> + +=head2 group_values( sort => \&sorting_function ) + +Works like L but instead returns C compatible objects. + + + +=head1 SEE ALSO + +=over 4 + +=item * L + +=item * L + +=item * L + +=back + +=head1 AUTHOR + +Brian Cassidy Ebricas@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright 2011-2013 by Brian Cassidy + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/lib/CPAN/Changes/Spec.pod b/lib/CPAN/Changes/Spec.pod new file mode 100644 index 0000000..89886f1 --- /dev/null +++ b/lib/CPAN/Changes/Spec.pod @@ -0,0 +1,196 @@ + +=pod + +=head1 NAME + +CPAN::Changes::Spec - Specification for CPAN Changes files + +=head1 VERSION + +version 0.04 + +=head1 SYNOPSIS + + Revision history for perl module Foo::Bar + + 0.02 2009-07-17 + + - Added more foo() tests + + 0.01 2009-07-16 + + - Initial release + +=head1 DESCRIPTION + +This document describes version 0.03 of the specification for Changes files +included in a CPAN distribution. + +It is intended as a guide for module authors to encourage them to write +meaningful changelogs as well as provide a programmatic interface to +reliably read and write Changes files. + +=head1 DATA TYPES + +=head2 Version + +Versions should be formatted as described in L. + +=head2 Date + +A date/time in the format specified by L +aka W3CDTF. B The "T" marker before the time portion is optional. + +In order to satisfy release events not made to the public, or dates +that are historically unknown, the following strings are also available: + +=over 4 + +=item * Unknown Release Date + +=item * Unknown + +=item * Not Released + +=item * Development Release + +=item * Development + +=item * Developer Release + +=back + +=head1 STRUCTURE + +=head2 Required Elements + +In its simplest form, the only required elements are a C, a C and +the noted changes. Blank lines between the C line and the first +C line are optional. Blank lines between C lines are also +optional. + + (whitespace/non-"word" characters) + (whitespace) + +B The characters between a C and a C must start with +whitespace, but may subsquently contain any combination of whitespace and +non-"word" characters. Example: + + 0.01 - 2013-12-11 + +C lines have no specific format. Commonly, authors will use a dash +C<-> followed by a space to start a new change, and indent subsequent lines +for multi-line changes. Example + + - Simple Change + - This is a very very very long + change line + +Although there is no limit on line length, authors generally wrap each line +at 78 columns. + +=head2 Optional Elements + +=head3 Release Note + +Any text following the C portion of the C line will be +considered the C. Example: + + 0.01 2013-04-01 Codename: April Fool + + - First Release + +=head2 Preamble + +Any amount of text before the first C line will be considered part +of the preamble. Most existing distributions include something along the +lines of: + + Revision history for perl module My::Module + +Or + + Revision history for perl distribution My-Distribution + +=head2 Groups + +Changelog entries may be grouped under headings. Heading lines begin with an +opening square bracket (C<[>), and end with a matching square bracket (C<]>). +When parsing group headings, leading and trailing whitespace inside the +brackets should be discarded. + + (whitespace)[Grouping Name] + (whitespace) + +Since empty lines hold no special meaning, all C lines will fall +under the current group until a new group heading is found. Example: + + [ First Group ] + - First Change + + - Second Change; in first group + + [ Second Group ] + - First Change; in second group + +=head1 EXAMPLES + +=head2 Basic Example + + 0.01 2009-07-16 + - Initial release + +=head2 Example with a preamble + + Revision history for perl module Foo::Bar + + 0.02 2009-07-17 + + - Added more foo() tests + + 0.01 2009-07-16 + + - Initial release + +=head2 Example with groups + + Revision history for perl module Foo::Bar + + 0.03 2009-07-18 + + [Important Security Information] + - This release fixes critical bug RT #1234 + + [Other Changes] + - Added some feature + + 0.02 2009-07-17 + + - Added more foo() tests + + 0.01 2009-07-16T19:20:30+01:00 + + - Initial release + +=head1 SEE ALSO + +=over 4 + +=item * L + +=item * L + +=back + +=head1 AUTHOR + +Brian Cassidy Ebricas@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright 2011-2013 by Brian Cassidy + +This is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/lib/Test/CPAN/Changes.pm b/lib/Test/CPAN/Changes.pm new file mode 100644 index 0000000..f148ac5 --- /dev/null +++ b/lib/Test/CPAN/Changes.pm @@ -0,0 +1,160 @@ +package Test::CPAN::Changes; + +use strict; +use warnings; + +use CPAN::Changes; +use Test::Builder; +use version (); + +our $VERSION = '0.400002'; + +my $Test = Test::Builder->new; + +sub import { + my $self = shift; + + my $caller = caller; + no strict 'refs'; + *{ $caller . '::changes_ok' } = \&changes_ok; + *{ $caller . '::changes_file_ok' } = \&changes_file_ok; + + $Test->exported_to( $caller ); + $Test->plan( @_ ); +} + +sub changes_ok { + $Test->plan( tests => 4 ); + return changes_file_ok( undef, @_ ); +} + +sub changes_file_ok { + my ( $file, $arg ) = @_; + $file ||= 'Changes'; + $arg ||= {}; + + my $changes = eval { CPAN::Changes->load( $file ) }; + + if ( $@ ) { + $Test->ok( 0, "Unable to parse $file" ); + $Test->diag( " ERR: $@" ); + return; + } + + $Test->ok( 1, "$file is loadable" ); + + my @releases = $changes->releases; + + if ( !@releases ) { + $Test->ok( 0, "$file does not contain any releases" ); + return; + } + + $Test->ok( 1, "$file contains at least one release" ); + + for ( @releases ) { + if ( !defined $_->date || $_->date eq '' ) { + $Test->ok( 0, "$file contains an invalid release date" ); + $Test->diag( ' ERR: No date at version ' . $_->version ); + return; + } + + my $d = $_->{ _parsed_date }; + if ( $d !~ m[^${CPAN::Changes::W3CDTF_REGEX}$] + && $d !~ m[^(${CPAN::Changes::UNKNOWN_VALS})$] ) { + $Test->carp( 'Date "' . $d . '" is not in the recommended format' ); + } + + # strip off -TRIAL before testing + (my $version = $_->version) =~ s/-TRIAL$//; + if ( not version::is_lax($version) ) { + $Test->ok( 0, "$file contains an invalid version number" ); + $Test->diag( ' ERR: ' . $_->version ); + return; + } + } + + $Test->ok( 1, "$file contains valid release dates" ); + $Test->ok( 1, "$file contains valid version numbers" ); + + if ( defined $arg->{version} ) { + my $v = $arg->{version}; + + if ( my $release = $changes->release( $v ) ) { + $Test->ok( 1, "$file has an entry for the current version, $v" ); + my $changes = $release->changes; + + if ( $changes and grep { @$_ > 0 } values %$changes ) { + $Test->ok( 1, "entry for the current version, $v, has content" ); + } else { + $Test->ok( 0, "entry for the current version, $v, no content" ); + } + } else { + # Twice so that we have a fixed number of tests to plan. + # -- rjbs, 2011-05-02 + $Test->ok( 0, "$file has no entry for the current version, $v" ); + $Test->ok( 0, "$file has no entry for the current version, $v" ); + } + } + + return $changes; +} + +1; + +__END__ + +=head1 NAME + +Test::CPAN::Changes - Validation of the Changes file in a CPAN distribution + +=head1 SYNOPSIS + + use Test::More; + eval 'use Test::CPAN::Changes'; + plan skip_all => 'Test::CPAN::Changes required for this test' if $@; + changes_ok(); + +=head1 DESCRIPTION + +This module allows CPAN authors to write automated tests to ensure their +changelogs match the specification. + +=head1 METHODS + +=head2 changes_ok( ) + +Simple wrapper around C. Declares a four test plan, and +uses the default filename of C. + +=head2 changes_file_ok( $filename, \%arg ) + +Checks the contents of the changes file against the specification. No plan +is declared and if the filename is undefined, C is used. + +C<%arg> may include a I entry, in which case the entry for that +version must exist and have content. This is useful to ensure that the version +currently being released has documented changes. + +=head1 SEE ALSO + +=over 4 + +=item * L + +=item * L + +=back + +=head1 AUTHOR + +Brian Cassidy Ebricas@cpan.orgE + +=head1 COPYRIGHT AND LICENSE + +Copyright 2011-2013 by Brian Cassidy + +This library is free software; you can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut diff --git a/maint/Makefile.PL.include b/maint/Makefile.PL.include new file mode 100644 index 0000000..9632bdd --- /dev/null +++ b/maint/Makefile.PL.include @@ -0,0 +1,12 @@ +BEGIN { -e 'Distar' or system("git clone git://git.shadowcat.co.uk/p5sagit/Distar.git") } +use lib 'Distar/lib'; +use Distar 0.001; + +use ExtUtils::MakeMaker 6.57_10 (); + +author 'Brian Cassidy '; + +manifest_include bin => qr/.*/; +manifest_include 't/corpus' => qr/.*/; + +1; diff --git a/t/corpus/basic.changes b/t/corpus/basic.changes new file mode 100644 index 0000000..f93ece1 --- /dev/null +++ b/t/corpus/basic.changes @@ -0,0 +1,2 @@ +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/different-indentation.changes b/t/corpus/different-indentation.changes new file mode 100644 index 0000000..8d37778 --- /dev/null +++ b/t/corpus/different-indentation.changes @@ -0,0 +1,5 @@ +0.02 2010-06-17 + - New version + +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/dist-zilla.changes b/t/corpus/dist-zilla.changes new file mode 100644 index 0000000..a02a337 --- /dev/null +++ b/t/corpus/dist-zilla.changes @@ -0,0 +1,11 @@ +Revision history for Catalyst-Plugin-Sitemap + +{{$NEXT}} + - Something + +1.0.0 2010-11-30 + - Change sitemap object under the hood to WWW::Sitemap::XML. + - Add Dancer::Plugin::SiteMap in the 'SEE ALSO' section. + +0.0.1 2010-09-29 + - original version unleashed on an unsuspecting world diff --git a/t/corpus/dist-zilla_format.changes b/t/corpus/dist-zilla_format.changes new file mode 100644 index 0000000..47eddf3 --- /dev/null +++ b/t/corpus/dist-zilla_format.changes @@ -0,0 +1,2 @@ +0.01 2010-12-28 00:15:12 Europe/London + - Initial release diff --git a/t/corpus/group-brackets.changes b/t/corpus/group-brackets.changes new file mode 100644 index 0000000..5fa4692 --- /dev/null +++ b/t/corpus/group-brackets.changes @@ -0,0 +1,6 @@ +0.01 2010-06-16 + [Group 1] + - Initial release + [not a group], seriously. + - change + [also] [not a group] diff --git a/t/corpus/group.changes b/t/corpus/group.changes new file mode 100644 index 0000000..b91b608 --- /dev/null +++ b/t/corpus/group.changes @@ -0,0 +1,3 @@ +0.01 2010-06-16 + [Group 1] + - Initial release diff --git a/t/corpus/latin1.changes b/t/corpus/latin1.changes new file mode 100644 index 0000000..1656197 --- /dev/null +++ b/t/corpus/latin1.changes @@ -0,0 +1,2 @@ +0.01 2010-06-16 + - change made by k�the diff --git a/t/corpus/line-continuation.changes b/t/corpus/line-continuation.changes new file mode 100644 index 0000000..8dda6db --- /dev/null +++ b/t/corpus/line-continuation.changes @@ -0,0 +1,3 @@ +0.01 2010-06-16 + - Initial release + This line is part of the first diff --git a/t/corpus/long_preamble.changes b/t/corpus/long_preamble.changes new file mode 100644 index 0000000..e962ab6 --- /dev/null +++ b/t/corpus/long_preamble.changes @@ -0,0 +1,6 @@ +Revision history for perl module Foo::Bar + +Yep. + +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/multiple_releases.changes b/t/corpus/multiple_releases.changes new file mode 100644 index 0000000..232ec3d --- /dev/null +++ b/t/corpus/multiple_releases.changes @@ -0,0 +1,5 @@ +0.02 2010-06-17 + - New version + +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/no-leading-space-for-change.changes b/t/corpus/no-leading-space-for-change.changes new file mode 100644 index 0000000..22175ec --- /dev/null +++ b/t/corpus/no-leading-space-for-change.changes @@ -0,0 +1,3 @@ +0.01 2010-06-16 +- Initial release + This line is part of the first diff --git a/t/corpus/preamble.changes b/t/corpus/preamble.changes new file mode 100644 index 0000000..63345ed --- /dev/null +++ b/t/corpus/preamble.changes @@ -0,0 +1,4 @@ +Revision history for perl module Foo::Bar + +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/space-before-date.changes b/t/corpus/space-before-date.changes new file mode 100644 index 0000000..1c19762 --- /dev/null +++ b/t/corpus/space-before-date.changes @@ -0,0 +1,5 @@ +0.02 2010-06-17 + - Testing tabs + +0.01 2010-06-16 + - Initial release diff --git a/t/corpus/timestamp.changes b/t/corpus/timestamp.changes new file mode 100644 index 0000000..d56a457 --- /dev/null +++ b/t/corpus/timestamp.changes @@ -0,0 +1,32 @@ +0.11 2011-04-12T12:00:00+01:00 + - W3CDTF, with timezone + +0.10 2011-04-14 13:00:00.123 + - Factional Seconds + +0.09 2011-04-14 12:00:00 America/Halifax + - Dist::Zilla style date + +0.08 2011-04-13 12:00 Test + - Datetime w/o T or Z, plus note + +0.07 2011-04-12T12:00:00Z # JUNK! + - W3CDTF, with junk marker + +0.06 Mon, 11 Apr 2011 21:40:45 -0300 + - RFC 2822 + +0.05 2011-04-11 15:14 + - Similar to 0.04, without seconds + +0.04 2011-04-11 12:11:10 + - Datetime w/o T or Z + +0.03 Fri Mar 25 2011 + - Yet another release + +0.02 Fri Mar 25 12:18:36 2011 + - Another release + +0.01 Fri Mar 25 12:16:25 ADT 2011 + - Initial release diff --git a/t/corpus/unknown_date.changes b/t/corpus/unknown_date.changes new file mode 100644 index 0000000..93e2ed6 --- /dev/null +++ b/t/corpus/unknown_date.changes @@ -0,0 +1,9 @@ +0.05 Developer Release + +0.04 Development Release + +0.03 Unknown Release Date + +0.02 Not Released + +0.01 Unknown diff --git a/t/corpus/utf8.changes b/t/corpus/utf8.changes new file mode 100644 index 0000000..ef1df27 --- /dev/null +++ b/t/corpus/utf8.changes @@ -0,0 +1,2 @@ +0.01 2010-06-16 + - change made by käthe diff --git a/t/corpus/version-date-separator.changes b/t/corpus/version-date-separator.changes new file mode 100644 index 0000000..8707978 --- /dev/null +++ b/t/corpus/version-date-separator.changes @@ -0,0 +1,5 @@ +0.03 - 2013-12-11 + +0.02 == 2013-12-10 + +0.01 -=\/\/=- 2013-12-09 diff --git a/t/delete_empty_groups.t b/t/delete_empty_groups.t new file mode 100644 index 0000000..beed191 --- /dev/null +++ b/t/delete_empty_groups.t @@ -0,0 +1,54 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +subtest basic => sub { + plan tests => 2; + + my $changes = CPAN::Changes->load_string(<<'END_CHANGES'); +0.2 2012-02-01 + [D] + [E] + - Yadah + +0.1 2011-01-01 + [A] + - Stuff + [B] + [C] + - Blah +END_CHANGES + + $changes->delete_empty_groups; + + is_deeply( [ sort( ($changes->releases)[0]->groups ) ], [ qw/ A C / ] ); + is_deeply( [ sort( ($changes->releases)[1]->groups ) ], [ 'E' ] ); +}; + +subtest mixed => sub { + plan tests => 1; + + my $changes = CPAN::Changes->load_string(<<'END_CHANGES'); +Revision history for {{$dist->name}} + +0.2.0 + [BUGS FIXES] + - A + - B + +0.1.0 2012-03-19 + - C +END_CHANGES + + $changes->delete_empty_groups; + + is_deeply( [ sort( ($changes->releases)[0]->changes ) ], [ { + '' => [ 'C' ], + } ] ); + +}; + +done_testing; diff --git a/t/dist-zilla-changes.t b/t/dist-zilla-changes.t new file mode 100644 index 0000000..ab7ba11 --- /dev/null +++ b/t/dist-zilla-changes.t @@ -0,0 +1,45 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/dist-zilla.changes', + next_token => qr/\{\{\$NEXT\}\}/); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, 'Revision history for Catalyst-Plugin-Sitemap', + 'preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 3, 'has 3 releases' ); + +my $r = pop @releases; + +isa_ok( $r, 'CPAN::Changes::Release' ); +is( $r->version, '{{$NEXT}}', 'version' ); +is( $r->date, undef, 'date' ); +is_deeply( + $r->changes, + { '' => [ 'Something' ] }, + 'full changes' +); +is_deeply( [ $r->groups ], [ '' ], 'only the main group' ); + +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.0.1', 'version' ); +is( $releases[ 0 ]->date, '2010-09-29', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'original version unleashed on an unsuspecting world' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); + +isa_ok( $releases[ 1 ], 'CPAN::Changes::Release' ); +is( $releases[ 1 ]->version, '1.0.0', 'version' ); +is( $releases[ 1 ]->date, '2010-11-30', 'date' ); + +done_testing; diff --git a/t/moo_lazy_subclass.t b/t/moo_lazy_subclass.t new file mode 100644 index 0000000..240b143 --- /dev/null +++ b/t/moo_lazy_subclass.t @@ -0,0 +1,64 @@ +use strict; +use warnings; + +use Test::More; + +# ABSTRACT: Make sure subclassing the name is easy and lazy + +BEGIN { + eval "require Moo; 1" + or plan skip_all => "Moo required for this test"; +} + +use CPAN::Changes::Group; + +{ + + package CustomGroup; + + use Moo; + extends 'CPAN::Changes::Group'; + + has 'name' => ( is => ro =>, lazy => 1, builder => '_build_name' ); + has 'flavour' => ( is => ro =>, lazy => 1, builder => '_build_flavour' ); + + sub _build_name { + my ($self) = @_; + return 'Custom::Name / ' . $self->flavour; + } + + sub _build_flavour { + return 'Vanilla'; + } +} + +subtest 'nameonly' => sub { + my $object = CustomGroup->new( name => 'Bob' ); + + is( $object->name, 'Bob', 'Constructor attribute passthrough' ); + is( $object->flavour, 'Vanilla', 'Default flavour still exists' ); +}; + +subtest 'flavouronly' => sub { + my $object = CustomGroup->new( flavour => 'Earwax' ); + + is( + $object->name, + 'Custom::Name / Earwax', + 'Constructor attribute affects name lazily' + ); + is( $object->flavour, 'Earwax', 'Passed flavour propagates' ); +}; + +subtest 'noargs' => sub { + my $object = CustomGroup->new(); + + is( + $object->name, + 'Custom::Name / Vanilla', + 'Default attribute affects name lazily' + ); + is( $object->flavour, 'Vanilla', 'Default flavour propagates' ); +}; + +done_testing; diff --git a/t/read_basic.t b/t/read_basic.t new file mode 100644 index 0000000..3a6661e --- /dev/null +++ b/t/read_basic.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/basic.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 1, 'has 1 release' ); +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'Initial release' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ 'Initial release' ], + 'one change line' +); + +done_testing; diff --git a/t/read_different-indentation.t b/t/read_different-indentation.t new file mode 100644 index 0000000..2fcd063 --- /dev/null +++ b/t/read_different-indentation.t @@ -0,0 +1,47 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/different-indentation.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 2, 'has 2 releases' ); + +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'Initial release' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ 'Initial release' ], + 'one change line' +); + +isa_ok( $releases[ 1 ], 'CPAN::Changes::Release' ); +is( $releases[ 1 ]->version, '0.02', 'version' ); +is( $releases[ 1 ]->date, '2010-06-17', 'date' ); +is_deeply( + $releases[ 1 ]->changes, + { '' => [ 'New version' ] }, + 'full changes' +); +is_deeply( [ $releases[ 1 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 1 ]->changes( '' ), + [ 'New version' ], + 'one change line' +); + +done_testing; diff --git a/t/read_dist-zilla.t b/t/read_dist-zilla.t new file mode 100644 index 0000000..f2f5aab --- /dev/null +++ b/t/read_dist-zilla.t @@ -0,0 +1,18 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/dist-zilla_format.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); + +my @releases = $changes->releases; +is( scalar @releases, 1, 'has 1 release' ); + +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->date, '2010-12-28T00:15:12Z', 'date' ); + +done_testing; diff --git a/t/read_encoded.t b/t/read_encoded.t new file mode 100644 index 0000000..7c8c50f --- /dev/null +++ b/t/read_encoded.t @@ -0,0 +1,33 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +for my $file ( 't/corpus/utf8.changes', 't/corpus/latin1.changes' ) { + my $changes = CPAN::Changes->load( $file ); + + isa_ok( $changes, 'CPAN::Changes' ); + is( $changes->preamble, '', 'no preamble' ); + + my @releases = $changes->releases; + + is( scalar @releases, 1, 'has 1 release' ); + isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); + is( $releases[ 0 ]->version, '0.01', 'version' ); + is( $releases[ 0 ]->date, '2010-06-16', 'date' ); + is_deeply( + $releases[ 0 ]->changes, + { '' => [ "change made by k\x{00E4}the" ] }, + 'full changes' + ); + is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); + is_deeply( + $releases[ 0 ]->changes( '' ), + [ "change made by k\x{00E4}the" ], + 'one change line' + ); +} + +done_testing; diff --git a/t/read_group-brackets.t b/t/read_group-brackets.t new file mode 100644 index 0000000..8638563 --- /dev/null +++ b/t/read_group-brackets.t @@ -0,0 +1,37 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/group-brackets.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 1, 'has 1 release' ); +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { 'Group 1' => [ + 'Initial release [not a group], seriously.', + 'change [also] [not a group]', + ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ 'Group 1' ], 'one group' ); +is_deeply( + $releases[ 0 ]->changes( 'Group 1' ), + [ + 'Initial release [not a group], seriously.', + 'change [also] [not a group]', + ], + 'one change line' +); + +done_testing; diff --git a/t/read_group.t b/t/read_group.t new file mode 100644 index 0000000..d921567 --- /dev/null +++ b/t/read_group.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/group.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 1, 'has 1 release' ); +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { 'Group 1' => [ 'Initial release' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ 'Group 1' ], 'one group' ); +is_deeply( + $releases[ 0 ]->changes( 'Group 1' ), + [ 'Initial release' ], + 'one change line' +); + +done_testing; diff --git a/t/read_line-continuation.t b/t/read_line-continuation.t new file mode 100644 index 0000000..3d90d2b --- /dev/null +++ b/t/read_line-continuation.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/line-continuation.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 1, 'has 1 release' ); +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ "Initial release This line is part of the first" ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ "Initial release This line is part of the first" ], + 'one change line' +); + +done_testing; diff --git a/t/read_multiple_releases.t b/t/read_multiple_releases.t new file mode 100644 index 0000000..9d37330 --- /dev/null +++ b/t/read_multiple_releases.t @@ -0,0 +1,47 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/multiple_releases.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 2, 'has 2 releases' ); + +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'Initial release' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ 'Initial release' ], + 'one change line' +); + +isa_ok( $releases[ 1 ], 'CPAN::Changes::Release' ); +is( $releases[ 1 ]->version, '0.02', 'version' ); +is( $releases[ 1 ]->date, '2010-06-17', 'date' ); +is_deeply( + $releases[ 1 ]->changes, + { '' => [ 'New version' ] }, + 'full changes' +); +is_deeply( [ $releases[ 1 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 1 ]->changes( '' ), + [ 'New version' ], + 'one change line' +); + +done_testing; diff --git a/t/read_no-leading-space-for-change.t b/t/read_no-leading-space-for-change.t new file mode 100644 index 0000000..279d748 --- /dev/null +++ b/t/read_no-leading-space-for-change.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/no-leading-space-for-change.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 1, 'has 1 release' ); +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ "Initial release This line is part of the first" ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ "Initial release This line is part of the first" ], + 'one change line' +); + +done_testing; diff --git a/t/read_no_date.t b/t/read_no_date.t new file mode 100644 index 0000000..7c4ac64 --- /dev/null +++ b/t/read_no_date.t @@ -0,0 +1,32 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load_string(<<'END_CHANGES'); +1.01 Note + - Second + +1.00 + - First +END_CHANGES + +isa_ok( $changes, 'CPAN::Changes' ); + +my @releases = $changes->releases; +is( scalar @releases, 2, 'has 2 releases' ); + +my @expected = ( + { date => undef, note => undef }, + { date => undef, note => 'Note' }, +); + +for ( 0..@expected - 1 ) { + isa_ok( $releases[ $_ ], 'CPAN::Changes::Release' ); + is( $releases[ $_ ]->date, $expected[ $_ ]->{ date }, 'date' ); + is( $releases[ $_ ]->note, $expected[ $_ ]->{ note }, 'note' ); +} + +done_testing; diff --git a/t/read_preamble.t b/t/read_preamble.t new file mode 100644 index 0000000..fa3ba26 --- /dev/null +++ b/t/read_preamble.t @@ -0,0 +1,51 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +{ + my $changes = CPAN::Changes->load( 't/corpus/preamble.changes' ); + + isa_ok( $changes, 'CPAN::Changes' ); + is( $changes->preamble, 'Revision history for perl module Foo::Bar', + 'preamble' ); + + check_releases( $changes ); +} + +{ + my $changes = CPAN::Changes->load( 't/corpus/long_preamble.changes' ); + + isa_ok( $changes, 'CPAN::Changes' ); + is( $changes->preamble, 'Revision history for perl module Foo::Bar + +Yep.', + 'preamble' ); + + check_releases( $changes ); +} + +sub check_releases { + my $changes = shift; + my @releases = $changes->releases; + + is( scalar @releases, 1, 'has 1 release' ); + isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); + is( $releases[ 0 ]->version, '0.01', 'version' ); + is( $releases[ 0 ]->date, '2010-06-16', 'date' ); + is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'Initial release' ] }, + 'full changes' + ); + is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); + is_deeply( + $releases[ 0 ]->changes( '' ), + [ 'Initial release' ], + 'one change line' + ); +} + +done_testing; diff --git a/t/read_space-before-date.t b/t/read_space-before-date.t new file mode 100644 index 0000000..f32e469 --- /dev/null +++ b/t/read_space-before-date.t @@ -0,0 +1,47 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/space-before-date.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 2, 'has 2 release' ); + +isa_ok( $releases[ 0 ], 'CPAN::Changes::Release' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2010-06-16', 'date' ); +is_deeply( + $releases[ 0 ]->changes, + { '' => [ 'Initial release' ] }, + 'full changes' +); +is_deeply( [ $releases[ 0 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 0 ]->changes( '' ), + [ 'Initial release' ], + 'one change line' +); + +isa_ok( $releases[ 1 ], 'CPAN::Changes::Release' ); +is( $releases[ 1 ]->version, '0.02', 'version' ); +is( $releases[ 1 ]->date, '2010-06-17', 'date' ); +is_deeply( + $releases[ 1 ]->changes, + { '' => [ 'Testing tabs' ] }, + 'full changes' +); +is_deeply( [ $releases[ 1 ]->groups ], [ '' ], 'only the main group' ); +is_deeply( + $releases[ 1 ]->changes( '' ), + [ 'Testing tabs' ], + 'one change line' +); + +done_testing; diff --git a/t/read_timestamp.t b/t/read_timestamp.t new file mode 100644 index 0000000..49a72c0 --- /dev/null +++ b/t/read_timestamp.t @@ -0,0 +1,43 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/timestamp.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); + +my @releases = $changes->releases; +is( scalar @releases, 11, 'has 11 releases' ); + +my @expected = ( + qw( + 2011-03-25T12:16:25Z + 2011-03-25T12:18:36Z + 2011-03-25 + 2011-04-11T12:11:10Z + 2011-04-11T15:14Z + 2011-04-11T21:40:45-03:00 + ), + { d => '2011-04-12T12:00:00Z', n => '# JUNK!' }, + { d => '2011-04-13T12:00Z', n => 'Test' }, + { d => '2011-04-14T12:00:00Z', n => 'America/Halifax' }, + '2011-04-14T13:00:00.123Z', + { d => '2011-04-12T12:00:00+01:00', n => undef }, +); +for ( 0..@expected - 1 ) { + isa_ok( $releases[ $_ ], 'CPAN::Changes::Release' ); + + if( ref $expected[ $_ ] ) { + is( $releases[ $_ ]->date, $expected[ $_ ]->{ d }, 'date' ); + is( $releases[ $_ ]->note, $expected[ $_ ]->{ n }, 'note' ); + } + else { + is( $releases[ $_ ]->date, $expected[ $_ ], 'date' ); + is( $releases[ $_ ]->note, undef, 'note' ); + } +} + +done_testing; diff --git a/t/read_unknown_date.t b/t/read_unknown_date.t new file mode 100644 index 0000000..07cd7db --- /dev/null +++ b/t/read_unknown_date.t @@ -0,0 +1,31 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/unknown_date.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 5, 'has 5 releases' ); + +my @expected = ( + { version => '0.01', date => 'Unknown' }, + { version => '0.02', date => 'Not Released' }, + { version => '0.03', date => 'Unknown Release Date' }, + { version => '0.04', date => 'Development Release' }, + { version => '0.05', date => 'Developer Release' }, +); + +for ( 0..@expected - 1 ) { + isa_ok( $releases[ $_ ], 'CPAN::Changes::Release' ); + is( $releases[ $_ ]->version, $expected[ $_ ]->{ version }, 'version' ); + is( $releases[ $_ ]->date, $expected[ $_ ]->{ date }, 'date' ); +} + +done_testing; diff --git a/t/read_version-date-separator.t b/t/read_version-date-separator.t new file mode 100644 index 0000000..d51a286 --- /dev/null +++ b/t/read_version-date-separator.t @@ -0,0 +1,23 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 't/corpus/version-date-separator.changes' ); + +isa_ok( $changes, 'CPAN::Changes' ); +is( $changes->preamble, '', 'no preamble' ); + +my @releases = $changes->releases; + +is( scalar @releases, 3, 'has 3 releases' ); +is( $releases[ 2 ]->version, '0.03', 'version' ); +is( $releases[ 2 ]->date, '2013-12-11', 'date' ); +is( $releases[ 1 ]->version, '0.02', 'version' ); +is( $releases[ 1 ]->date, '2013-12-10', 'date' ); +is( $releases[ 0 ]->version, '0.01', 'version' ); +is( $releases[ 0 ]->date, '2013-12-09', 'date' ); + +done_testing; diff --git a/t/rt90605.t b/t/rt90605.t new file mode 100644 index 0000000..4926dd2 --- /dev/null +++ b/t/rt90605.t @@ -0,0 +1,23 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $a = CPAN::Changes->new(); +my $b = CPAN::Changes->new(); + +my $params = { version => '1.0'}; + +$a->add_release( $params ); +$b->add_release( $params ); + +my ( @changes ) = ( 'hello' ); + +$a->release( '1.0' )->add_changes( @changes ); + +is_deeply( $a->release( '1.0' )->changes, { '' => [ 'hello' ] }, 'changes on "A"' ); +is_deeply( $b->release( '1.0' )->changes, { }, 'no changes on "B"' ); + +done_testing; diff --git a/t/self.t b/t/self.t new file mode 100644 index 0000000..bf22a99 --- /dev/null +++ b/t/self.t @@ -0,0 +1,15 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load( 'Changes' ); +my @releases = $changes->releases; + +isa_ok( $changes, 'CPAN::Changes' ); +ok( scalar @releases, 'has releases' ); +isa_ok( $_, 'CPAN::Changes::Release' ) for @releases; + +done_testing; diff --git a/t/serialize.t b/t/serialize.t new file mode 100644 index 0000000..e9667b5 --- /dev/null +++ b/t/serialize.t @@ -0,0 +1,184 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->new; +$changes->add_release( + { date => '2010-06-16', + version => '0.01', + changes => { '' => [ 'Initial release' ] }, + } +); + +{ + my $expected = <serialize, $expected, 'serialize' ); +} + +{ + $changes->preamble( 'Revision history for perl module Foo::Bar' ); + + my $expected = <serialize, $expected, 'serialize with preamble' ); +} + +{ + my $release = $changes->release( '0.01' ); + $release->clear_changes; + $release->add_changes( { group => 'Group 1' }, 'Initial release' ); + + my $expected = <serialize, $expected, + 'serialize with ground and preamble' ); +} + +{ + $changes->add_release( + { version => '0.02', + date => '2010-06-17', + changes => { '' => [ 'New version' ] }, + } + ); + + my $expected = <serialize, $expected, 'serialize with multiple releases' ); +} + +{ + $changes->releases( + { version => '0.01', + date => '2010-06-16', + changes => { + '' => [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. In quis tortor ac urna faucibus feugiat.' + ] + }, + } + ); + + my $expected = <serialize, $expected, 'serialize with line-wrap' ); +} + +{ + $changes->releases( + { version => '0.01', + date => '2010-06-16', + note => 'Note', + changes => { + '' => [ + 'Test' + ] + }, + } + ); + + my $expected = <serialize, $expected, 'serialize with note' ); +} + +{ + $changes->releases( + { version => '0.01', + date => 'Unknown', + note => '(Oops)', + changes => { + '' => [ + 'Test' + ] + }, + } + ); + + my $expected = <serialize, $expected, 'serialize with unknown date and note' ); +} + +{ + my $changes = CPAN::Changes->new; + $changes->add_release( + { date => '', + version => '0.01', + note => '', + changes => { '' => [ 'Initial release' ] }, + } + ); + my $expected = <serialize, $expected, 'serialize w/ defined but empty date and note' ); +} + +{ + my $changes = CPAN::Changes->new; + $changes->add_release( + { date => '', + version => '0.01', + note => '', + changes => { '' => [ + 'http://www.cpan.org/abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz', + "\x{026B}this_entry_should_not_be_wrapped_on_the_nonbreaking_space\x{00A0}in_it_even_though_it_is_over_80_characters_long", + ] }, + } + ); + my $expected = <serialize, $expected, 'serialize does not wrap long tokens or split on nbsp' ); +} + +done_testing; diff --git a/t/sort_groups.t b/t/sort_groups.t new file mode 100644 index 0000000..6ec2d4f --- /dev/null +++ b/t/sort_groups.t @@ -0,0 +1,41 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my $changes = CPAN::Changes->load_string(<<'END_CHANGES'); +1.05 2011-04-17 + [A] + - stuff + [B] + - mo' stuff +1.04 2011-04-16 + [C] + - stuff + [D] + - mo' stuff +END_CHANGES + +like $changes->serialize => expected_order(qw/ A B C D/ ); +like $changes->serialize( group_sort => \&reverse_order ) => expected_order(qw/ B A D C/ ); + +my ($release) = reverse $changes->releases; +like $release->serialize => expected_order(qw/ A B / ); +like $release->serialize( group_sort => \&reverse_order ) => expected_order(qw/ B A / ); + +is_deeply [ $release->groups ], [qw/ A B /]; +is_deeply [ $release->groups( sort => \&reverse_order ) ], [qw/ B A /]; + +sub reverse_order { + return reverse sort @_; +} + +sub expected_order { + my @groups = @_; + my $re = join '.*', map { "\\[$_\\]" } @groups; + return qr/$re/s; +} + +done_testing; diff --git a/t/valid_dates.t b/t/valid_dates.t new file mode 100644 index 0000000..658fb82 --- /dev/null +++ b/t/valid_dates.t @@ -0,0 +1,23 @@ +use strict; +use warnings; + +use Test::More; + +use CPAN::Changes; + +my @dates = ( + '2000', + '2000-01', + '2000-01-01', + '2000-01-01T12:00', + '2000-01-01T12:00Z', + '2000-01-01T12:00+04:00', + '2000-01-01T12:00+04:00:00', + '2000-01-01 12:00', # optional "T" +); + +for my $date ( @dates ) { + ok( $date =~ m[^${CPAN::Changes::W3CDTF_REGEX}$], "Valid Date: $date" ); +} + +done_testing; diff --git a/xt/release/pod.t b/xt/release/pod.t new file mode 100644 index 0000000..f2e8196 --- /dev/null +++ b/xt/release/pod.t @@ -0,0 +1,3 @@ +use Test::More; +use Test::Pod 1.00; +all_pod_files_ok(); diff --git a/xt/release/pod_coverage.t b/xt/release/pod_coverage.t new file mode 100644 index 0000000..af84367 --- /dev/null +++ b/xt/release/pod_coverage.t @@ -0,0 +1,3 @@ +use Test::More; +use Test::Pod::Coverage 1.00; +all_pod_coverage_ok();