@@ -, +, @@ have been applied to a database --- Koha/AtomicUpdate.pm | 192 ++++++++ Koha/AtomicUpdater.pm | 440 ++++++++++++++++++ Koha/Schema/Result/Atomicupdate.pm | 115 +++++ cpanfile | 3 + installer/data/mysql/atomicupdate.pl | 208 +++++++++ .../atomicupdate/Bug-14698-AtomicUpdater.pl | 36 ++ .../atomicupdate/bug_14698-AtomicUpdater.perl | 20 + installer/data/mysql/kohastructure.sql | 14 + t/AtomicUpdater.t | 134 ++++++ t/db_dependent/Koha/AtomicUpdater.t | 287 ++++++++++++ 10 files changed, 1449 insertions(+) create mode 100644 Koha/AtomicUpdate.pm create mode 100644 Koha/AtomicUpdater.pm create mode 100644 Koha/Schema/Result/Atomicupdate.pm create mode 100644 installer/data/mysql/atomicupdate.pl create mode 100644 installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl create mode 100644 installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl create mode 100644 t/AtomicUpdater.t create mode 100644 t/db_dependent/Koha/AtomicUpdater.t --- a/Koha/AtomicUpdate.pm +++ a/Koha/AtomicUpdate.pm @@ -0,0 +1,192 @@ +package Koha::AtomicUpdate; + +# Copyright Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; +use Carp; +use File::Basename; + +use Koha::Database; + +use base qw(Koha::Object); + +use Koha::Exceptions::BadParameter; +use Koha::Exceptions::Parse; +use Koha::Exceptions::File; + + +sub _type { + return 'Atomicupdate'; +} + +=head new + + my $atomicUpdate = Koha::AtomicUpdate->new({filename => 'Bug54321-FixItPlease.pl'}); + +Creates a Koha::AtomicUpdate-object from the given parameters-HASH +@PARAM1 HASHRef of object parameters: + 'filename' => MANDATORY, The filename of the atomicupdate-script without the path-component. + 'issue_id' => OPTIONAL, the desired issue_id. It is better to let the module + find this from the filename, but is useful for testing purposes. +@RETURNS Koha::AtomicUpdate-object +@THROWS Koha::Exceptions::Parse from getIssueIdentifier() +@THROWS Koha::Exceptions::File from _validateFilename(); +=cut + +sub new { + my ($class, $params) = @_; + $class->_validateParams($params); + + my $self = {}; + bless($self, $class); + $self->set($params); + return $self; +} + +sub _validateParams { + my ($class, $params) = @_; + + my @mandatoryParams = ('filename'); + foreach my $mp (@mandatoryParams) { + Koha::Exceptions::BadParameter->throw( + error => "$class->_validateParams():> Param '$mp' must be given.") + unless($params->{$mp}); + } + $params->{filename} = $class->_validateFilename($params->{filename}); + + $params->{issue_id} = getIssueIdentifier($params->{filename}); +} + +=head _validateFilename + +Makes sure the given file is a valid AtomicUpdate-script. +Currently simply checks for naming convention and file suffix. + +NAMING CONVENTION: + Filename must contain one of the unique issue identifier prefixes from this + list @allowedIssueIdentifierPrefixes immediately followed by the numeric + id of the issue, optionally separated by any of the following [ :-] + Eg. Bug-45453, #102, #:53 + +@PARAM1 String, filename of validatable file, excluding path. +@RETURNS String, the koha.atomicupdates.filename if the given file is considered a well formed update script. + Removes the full path if present and returns only the filename component. + +@THROWS Koha::Exceptions::File, if the given file doesn't have a proper naming convention + +=cut + +sub _validateFilename { + my ($self, $fileName) = @_; + + Koha::Exceptions::File->throw(error => __PACKAGE__."->_validateFilename():> Filename '$fileName' has unknown suffix") + unless $fileName =~ /\.(sql|perl|pl)$/; #skip other files + + $fileName = File::Basename::basename($fileName); + + return $fileName; +} + +=head getIssueIdentifier +@STATIC + +Extracts the unique issue identifier from the atomicupdate DB upgrade script. + +@PARAM1 String, filename, excluding path. + OR +@PARAM2 String, Git commit title. +@RETURNS String, The unique issue identifier + +@THROWS Koha::Exceptions::Parse, if the unique identifier couldn't be parsed. +=cut + +sub getIssueIdentifier { + my ($fileName, $gitTitle) = @_; + + Koha::Exceptions::BadParameter->throw(error => "Either \$gitTitle or \$fileName must be given!") unless ($fileName || $gitTitle); + + my ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type); + ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type) = + getFileNameElements($fileName) + if $fileName; + ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type) = + getGitCommitTitleElements($gitTitle) + if $gitTitle; + + $prefix = uc $prefix if length $prefix <= 2; + $prefix = ucfirst(lc($prefix)) if length $prefix > 2; + + my @keys = ($prefix, $issueNumber); + push(@keys, $followupNumber) if $followupNumber; + return join('-', @keys); +} + +=head2 getFileNameElements +@STATIC + +Parses the given file name for atomicupdater markers. + +@PARAM1 String, base filename of the atomicupdate-file +@RETURNS ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType) +@THROWS Koha::Exceptions::Parse, if the fileName couldn't be parsed. + +=cut + +sub getFileNameElements { + my ($fileName) = @_; + + Koha::Exceptions::File->throw(error => + __PACKAGE__."->getIssueNameElements($fileName):> \$fileName cannot contain the comment-character '\x23'.". + " It will screw up the make build chain.") if $fileName =~ /\x23/; + + if ($fileName =~ /^([a-zA-Z]{1,3})(?:\W|[_])?(\d+)(?:(?:\W|[_])(\d+))?(?:(?:\W|[_])(.+?))?\.(\w{1,5})$/) { + return ($1, $2, $3, $4, $5); + } + + Koha::Exceptions::Parse->throw(error => __PACKAGE__."->getIssueNameElements($fileName):> Couldn't parse the given \$fileName"); +} + +=head2 getGitCommitTitleElements +@STATIC + +Parses the given Git commit title for atomicupdater markers. + +@PARAM1 String, git commit title +@RETURNS ($prefix, $issueNumber, $followupNumber, $issueDescription) +@THROWS Koha::Exceptions::Parse, if the title couldn't be parsed. + +=cut + +sub getGitCommitTitleElements { + my ($title) = @_; + + Koha::Exceptions::File->throw(error => + __PACKAGE__."->getGitCommitTitleElements($title):> \$prefix cannot contain the comment-character '\x23'.". + " It will screw up the make build chain.") if $title =~ /^.{0,2}\x23.{0,2} ?\W ?/; + + if ($title =~ /^(\w{1,3})(?: ?\W ?)(\d+)(?:(?:\W)(\d+))?(?: ?\W? ?)(.+?)$/) { + + #my ($prefix, $issueNumber, $followupNumber, $issueDescription) = ($1, $2, $3, $4); + #return ($prefix, $issueNumber, $followupNumber, $issueDescription); + return ($1, $2, $3, $4); + } + + Koha::Exceptions::Parse->throw(error => __PACKAGE__."->getGitCommitTitleElements($title):> Couldn't parse the given \$title"); +} + +1; --- a/Koha/AtomicUpdater.pm +++ a/Koha/AtomicUpdater.pm @@ -0,0 +1,440 @@ +package Koha::AtomicUpdater; + +# Copyright Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; +use Carp; +use Scalar::Util qw(blessed); +use Try::Tiny; +use Data::Format::Pretty::Console qw(format_pretty); +use Git; +use YAML::XS; +use File::Slurp; + +use C4::Installer; + +use Koha::Database; +use Koha::Cache; +use Koha::AtomicUpdate; + +use base qw(Koha::Objects); + +use Koha::Exceptions::File; +use Koha::Exceptions::Parse; +use Koha::Exceptions::BadParameter; +use Koha::Exceptions::DuplicateObject; + +sub _type { + return 'Atomicupdate'; +} +sub object_class { + return 'Koha::AtomicUpdate'; +} +sub _get_castable_unique_columns { + return ['atomicupdate_id']; +} + +=head find +@OVERLOADS + my $$atomicUpdate = $atomicUpdater->find($issue_id || $atomicupdate_id); + +@PARAM1 Scalar, issue_id or atomicupdate_id +@RETURNS Koha::AtomicUpdate +@THROWS Koha::Exceptions::BadParameter, if @PARAM1 is not a scalar + Koha::Exceptions::DuplicateObject, if @PARAM1 matches both the issue_id and atomicupdate_id, + you should change your issue naming convention. +=cut + +sub find { + my ( $self, $id ) = @_; + return unless $id; + if (ref($id)) { + return $self->SUPER::find($id); + } + + my @results = $self->_resultset()->search({'-or' => [ + {issue_id => $id}, + {atomicupdate_id => $id} + ]}); + return unless @results; + if (scalar(@results > 1)) { + my @cc1 = caller(1); + my @cc0 = caller(0); + Koha::Exceptions::DuplicateObject->throw(error => $cc1[3]."() -> ".$cc0[3]."():> Given \$id '$id' matches multiple issue_ids and atomicupdate_ids. Aborting because couldn't get a uniquely identifying AtomicUpdate."); + } + + my $object = $self->object_class()->_new_from_dbic( $results[0] ); + return $object; +} + +my $updateOrderFilename = '_updateorder'; + +sub new { + my ($class, $params) = @_; + + my $cache = Koha::Cache->new(); + my $self = $cache->get_from_cache('Koha::AtomicUpdater') || {}; + bless($self, $class); + + $self->{verbose} = $params->{verbose} || $self->{verbose} || 0; + $self->{scriptDir} = $params->{scriptDir} || $self->{scriptDir} || C4::Context->config('intranetdir') . '/installer/data/mysql/atomicupdate/'; + $self->{confFile} = $params->{confFile} || $self->{confFile} || C4::Context->config('intranetdir') . '/installer/data/mysql/atomicupdate.conf'; + $self->{gitRepo} = $params->{gitRepo} || $self->{gitRepo} || $ENV{KOHA_PATH}; + $self->{dryRun} = $params->{dryRun} || $self->{dryRun} || 0; + + $self->_loadConfig(); + return $self; +} + +=head getAtomicUpdates + + my $atomicUpdates = $atomicUpdater->getAtomicUpdates(); + +Gets all the AtomicUpdate-objects in the DB. This result should be Koha::Cached. +@RETURNS HASHRef of Koha::AtomicUpdate-objects, keyed with the issue_id +=cut + +sub getAtomicUpdates { + my ($self) = @_; + + my @au = $self->search({}); + my %au; #HASHify the AtomicUpdate-objects for easy searching. + foreach my $au (@au) { + $au{$au->issue_id} = $au; + } + return \%au; +} + +sub addAtomicUpdate { + my ($self, $params) = @_; + print "Adding atomicupdate '".($params->{issue_id} || $params->{filename})."'\n" if $self->{verbose} > 2; + + my $atomicupdate = Koha::AtomicUpdate->new($params); + $atomicupdate->store(); + $atomicupdate = $self->find($atomicupdate->issue_id); + return $atomicupdate; +} + +sub removeAtomicUpdate { + my ($self, $issueId) = @_; + print "Deleting atomicupdate '$issueId'\n" if $self->{verbose} > 2; + + my $atomicupdate = $self->find($issueId); + if ($atomicupdate) { + $atomicupdate->delete; + print "Deleted atomicupdate '$issueId'\n" if $self->{verbose} > 2; + } + else { + Koha::Exceptions::BadParameter->throw(error => __PACKAGE__."->removeIssueFromLog():> No such Issue '$issueId' stored to the atomicupdates-table"); + } +} + +sub listToConsole { + my ($self) = @_; + my @stringBuilder; + + my @atomicupdates = $self->search({}); + foreach my $au (@atomicupdates) { + push @stringBuilder, $au->unblessed(); + } + return Data::Format::Pretty::Console::format_pretty(\@stringBuilder); +} + +sub listPendingToConsole { + my ($self) = @_; + my @stringBuilder; + + my $atomicUpdates = $self->getPendingAtomicUpdates(); + foreach my $key (sort keys %$atomicUpdates) { + my $au = $atomicUpdates->{$key}; + push @stringBuilder, $au->unblessed(); + } + return Data::Format::Pretty::Console::format_pretty(\@stringBuilder); +} + +sub getPendingAtomicUpdates { + my ($self) = @_; + + my %pendingAtomicUpdates; + my $atomicupdateFiles = $self->_getValidAtomicUpdateScripts(); + my $atomicUpdatesDeployed = $self->getAtomicUpdates(); + foreach my $key (keys(%$atomicupdateFiles)) { + my $au = $atomicupdateFiles->{$key}; + my $parsedissueId = $self->_parseIssueIds($au->issue_id); + unless ($atomicUpdatesDeployed->{$au->issue_id} || $atomicUpdatesDeployed->{$parsedissueId}) { + #This script hasn't been deployed. + $pendingAtomicUpdates{$au->issue_id} = $au; + } + } + return \%pendingAtomicUpdates; +} + +=head applyAtomicUpdates + + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $appliedAtomicupdates = $atomicUpdater->applyAtomicUpdates(); + +Checks the atomicupdates/-directory for any not-applied update scripts and +runs them in the order specified in the _updateorder-file in atomicupdate/-directory. + +@RETURNS ARRAYRef of Koha::AtomicUpdate-objects deployed on this run +=cut + +sub applyAtomicUpdates { + my ($self) = @_; + + my %appliedUpdates; + + my $atomicUpdates = $self->getPendingAtomicUpdates(); + my $updateOrder = $self->getUpdateOrder(); + foreach my $issueId ( @$updateOrder ) { + my $atomicUpdate = $atomicUpdates->{$issueId}; + next unless $atomicUpdate; #Not each ordered Git commit necessarily have a atomicupdate-script. + + $self->applyAtomicUpdate($atomicUpdate); + $appliedUpdates{$issueId} = $atomicUpdate; + } + + #Check that we have actually applied all the updates. + my $stillPendingAtomicUpdates = $self->getPendingAtomicUpdates(); + if (scalar(%$stillPendingAtomicUpdates)) { + my @issueIds = sort keys %$stillPendingAtomicUpdates; + print "Warning! After upgrade, the following atomicupdates are still pending '@issueIds'\n Try rebuilding the atomicupdate-scripts update order from the original Git repository.\n"; + } + + return \%appliedUpdates; +} + +sub applyAtomicUpdate { + my ($self, $atomicUpdate) = @_; + #Validate params + unless ($atomicUpdate) { + Koha::Exceptions::BadParameter->throw(error => __PACKAGE__."->applyAtomicUpdate($atomicUpdate):> Parameter must be a Koha::AtomicUpdate-object or a path to a valid atomicupdates-script!"); + } + if ($atomicUpdate && ref($atomicUpdate) eq '') { #We have a scalar, presumably a filepath to atomicUpdate-script. + $atomicUpdate = Koha::AtomicUpdate->new({filename => $atomicUpdate}); + } + + #$atomicUpdate = Koha::AtomicUpdater->cast($atomicUpdate); + + my $filename = $atomicUpdate->filename; + print "Applying file '$filename'\n" if $self->{verbose} > 2; + + unless ($self->{dryRun}) { + my $rv; + if ( $filename =~ /\.sql$/ ) { + my $installer = C4::Installer->new(); + $rv = $installer->load_sql( $self->{scriptDir}.'/'.$filename ) ? 0 : 1; + } elsif ( $filename =~ /\.(perl|pl)$/ ) { + my $fileAndPath = $self->{scriptDir}.'/'.$filename; + $rv = do $fileAndPath; + unless ($rv) { + warn "couldn't parse $fileAndPath: $@\n" if $@; + warn "couldn't do $fileAndPath: $!\n" unless defined $rv; + warn "couldn't run $fileAndPath\n" unless $rv; + } + } + print 'AtomicUpdate '.$atomicUpdate->filename." done.\n" if $self->{verbose} > 0; + $atomicUpdate->store(); + } + + print "File '$filename' applied\n" if $self->{verbose} > 2; +} + +=head _getValidAtomicUpdateScripts + +@RETURNS HASHRef of Koha::AtomicUpdate-objects, of all the files + in the atomicupdates/-directory that can be considered valid. + Validity is currently conforming to the naming convention. + Keys are the issue_id of atomicupdate-scripts + Eg. {'Bug8584' => Koha::AtomicUpdate, + ... + } +=cut + +sub _getValidAtomicUpdateScripts { + my ($self) = @_; + + my %atomicUpdates; + opendir( my $dirh, $self->{scriptDir} ); + foreach my $file ( sort readdir $dirh ) { + print "Looking at file '$file'\n" if $self->{verbose} > 2; + + my $atomicUpdate; + try { + $atomicUpdate = Koha::AtomicUpdate->new({filename => $file}); + } catch { + if (blessed($_)) { + if ($_->isa('Koha::Exceptions::File') || $_->isa('Koha::Exceptions::Parse')) { + print "File-error for file '$file': ".$_->error()." \n" if $self->{verbose} > 2; + #We can ignore filename validation issues, since the directory has + #loads of other types of files as well. Like README . .. + } + else { + $_->rethrow(); + } + } + else { + die $_; #Rethrow the unknown Exception + } + }; + next unless $atomicUpdate; + + $atomicUpdates{$atomicUpdate->issue_id} = $atomicUpdate; + } + return \%atomicUpdates; +} + +=head getUpdateOrder + + $atomicUpdater->getUpdateOrder(); + +@RETURNS ARRAYRef of Strings, IssueIds ordered from the earliest to the newest. +=cut + +sub getUpdateOrder { + my ($self) = @_; + + my $updateOrderFilepath = $self->{scriptDir}."/$updateOrderFilename"; + open(my $FH, "<:encoding(UTF-8)", $updateOrderFilepath) or die "Koha::AtomicUpdater->_saveAsUpdateOrder():> Couldn't open the updateOrderFile for reading\n$!\n"; + my @updateOrder = map {chomp($_); $_;} <$FH>; + close $FH; + return \@updateOrder; +} + +=head + + my $issueIdOrder = Koha::AtomicUpdater->buildUpdateOrderFromGit(10000); + +Creates a update order file '_updateorder' for atomicupdates to know which updates come before which. +This is a simple way to make sure the atomicupdates are applied in the correct order. +The update order file is by default in your $KOHA_PATH/installer/data/mysql/atomicupdate/_updateorder + +This requires a Git repository to be in the $ENV{KOHA_PATH} to be effective. + +@PARAM1 Integer, How many Git commits to include to the update order file, + 10000 is a good default. +@RETURNS ARRAYRef of Strings, The update order of atomicupdates from oldest to newest. +=cut + +sub buildUpdateOrderFromGit { + my ($self, $gitCommitsCount) = @_; + + my %orderedCommits; #Store the commits we have ordered here, so we don't reorder any followups. + my @orderedCommits; + + my $i = 0; #Index of array where we push issue_ids + my $commits = $self->_getGitCommits($gitCommitsCount); + foreach my $commit (reverse @$commits) { + + my ($commitHash, $commitTitle) = $self->_parseGitOneliner($commit); + unless ($commitHash && $commitTitle) { + next(); + } + + my $issueId; + try { + $issueId = Koha::AtomicUpdate::getIssueIdentifier(undef, $commitTitle); + } catch { + if (blessed($_)) { + if($_->isa('Koha::Exceptions::Parse')) { + #Silently ignore parsing errors + print "Koha::AtomicUpdater->buildUpdateOrderFromGit():> Couldn't parse issue_id from Git commit title '$commitTitle'.\n" + if $self->{verbose} > 1; + } + else { + $_->rethrow(); + } + } + else { + die $_; + } + }; + next unless $issueId; + + if ($orderedCommits{ $issueId }) { + next(); + } + else { + $orderedCommits{ $issueId } = $issueId; + $orderedCommits[$i] = $issueId; + $i++; + } + } + + $self->_saveAsUpdateOrder(\@orderedCommits); + return \@orderedCommits; +} + +sub _parseIssueIds { + my ($self, $issueId) = @_; + + my @keys = split /(-)/, $issueId; + delete $keys[1]; + @keys = grep defined, @keys; + + return join('', @keys); +} + +sub _getGitCommits { + my ($self, $count) = @_; + my $repo = Git->repository(Directory => $self->{gitRepo}); + + #We can read and print 10000 git commits in less than three seconds :) good Git! + my @commits = $repo->command('show', '--pretty=oneline', '--no-patch', '-'.$count); + return \@commits; +} + +sub _parseGitOneliner { + my ($self, $gitLiner) = @_; + + my ($commitHash, $commitTitle) = ($1, $2) if $gitLiner =~ /^(\w{40}) (.+)$/; + unless ($commitHash && $commitTitle) { + print "Koha::AtomicUpdater->parseGitOneliner():> Couldn't parse Git commit '$gitLiner' to hash and title.\n" + if $self->{verbose} > 1; + return(); + } + return ($commitHash, $commitTitle); +} + +sub _saveAsUpdateOrder { + my ($self, $orderedUpdates) = @_; + + my $updateOrderFilepath = $self->{scriptDir}."/$updateOrderFilename"; + my $text = join("\n", @$orderedUpdates); + open(my $FH, ">:encoding(UTF-8)", $updateOrderFilepath) or die "Koha::AtomicUpdater->_saveAsUpdateOrder():> Couldn't open the updateOrderFile for writing\n$!\n"; + print $FH $text; + close $FH; +} + +=head %config +Package static variable to the configurations Hash. +=cut + +my $config; + +sub _loadConfig { + my ($self) = @_; + + if (-e $self->{confFile}) { + my $yaml = File::Slurp::read_file( $self->{confFile}, { binmode => ':utf8' } ) ; + $config = YAML::XS::Load($yaml); + } +} + +1; --- a/Koha/Schema/Result/Atomicupdate.pm +++ a/Koha/Schema/Result/Atomicupdate.pm @@ -0,0 +1,115 @@ +use utf8; +package Koha::Schema::Result::Atomicupdate; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::Atomicupdate + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("atomicupdates"); + +=head1 ACCESSORS + +=head2 atomicupdate_id + + data_type: 'integer' + extra: {unsigned => 1} + is_auto_increment: 1 + is_nullable: 0 + +=head2 issue_id + + data_type: 'varchar' + is_nullable: 0 + size: 20 + +=head2 filename + + data_type: 'varchar' + is_nullable: 0 + size: 128 + +=head2 modification_time + + data_type: 'timestamp' + datetime_undef_if_invalid: 1 + default_value: current_timestamp + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "atomicupdate_id", + { + data_type => "integer", + extra => { unsigned => 1 }, + is_auto_increment => 1, + is_nullable => 0, + }, + "issue_id", + { data_type => "varchar", is_nullable => 0, size => 20 }, + "filename", + { data_type => "varchar", is_nullable => 0, size => 128 }, + "modification_time", + { + data_type => "timestamp", + datetime_undef_if_invalid => 1, + default_value => \"current_timestamp", + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("atomicupdate_id"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("atomic_issue_id", ["issue_id"]); + + +# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-03-23 14:13:35 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:iZ8fpCOXTPc1a2v86F6zRw + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration + +sub koha_objects_class { + 'Koha::AtomicUpdater'; +} +sub koha_object_class { + 'Koha::AtomicUpdate'; +} + +1; --- a/cpanfile +++ a/cpanfile @@ -26,6 +26,7 @@ requires 'DBIx::RunSQL', '0.14'; requires 'Data::Dumper', '2.121'; requires 'Data::ICal', '0.13'; requires 'Date::Calc', '5.4'; +requires 'Data::Format::Pretty::Console', '0.34'; requires 'Date::Manip', '5.44'; requires 'DateTime', '0.58'; requires 'DateTime::Event::ICal', '0.08'; @@ -39,12 +40,14 @@ requires 'Email::Date', '1.103'; requires 'Email::MessageID', '1.406'; requires 'Email::Valid', '0.190'; requires 'Exception::Class', '1.38'; +requires 'File::Fu::File', '1'; requires 'File::Slurp', '9999.13'; requires 'Font::TTF', '0.45'; requires 'GD', '2.39'; requires 'GD::Barcode::UPCE', '1.1'; requires 'Getopt::Long', '2.35'; requires 'Getopt::Std', '1.05'; +requires 'Git', '0.41'; requires 'HTML::Entities', '3.69'; requires 'HTML::FormatText', '1.23'; requires 'HTML::Scrubber', '0.08'; --- a/installer/data/mysql/atomicupdate.pl +++ a/installer/data/mysql/atomicupdate.pl @@ -0,0 +1,208 @@ +#!/usr/bin/perl +# +# Copyright Vaara-kirjastot 2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +use Modern::Perl; +use Getopt::Long qw(:config no_ignore_case); + +use C4::Context; + +use Koha::AtomicUpdater; + +my $verbose = 0; +my $help = 0; +my $apply = 0; +my $remove = ''; +my $dryRun = 0; +my $insert = ''; +my $list = 0; +my $pending = 0; +my $directory = ''; +my $git = ''; +my $single = ''; +my $configurationFile = ''; + +GetOptions( + 'v|verbose:i' => \$verbose, + 'h|help' => \$help, + 'a|apply' => \$apply, + 'D|dry-run' => \$dryRun, + 'd|directory:s' => \$directory, + 'r|remove:s' => \$remove, + 'i|insert:s' => \$insert, + 'l|list' => \$list, + 'p|pending' => \$pending, + 'g|git:s' => \$git, + 's|single:s' => \$single, + 'c|config:s' => \$configurationFile, +); + +my $usage = << 'ENDUSAGE'; + +Runs all the not-yet-applied atomicupdate-scripts and sql in the +atomicupdates-directory, in the order specified by the _updateorder-file. + +This script uses koha.atomicupdates-table to see if the update has already been +applied. + +Also acts as a gateway to CRUD the koha.atomicupdates-table. + +Naming conventions for atomicupdate-scripts: +-------------------------------------------- +All atomicupdate-scripts must follow this naming convention + +eg. +"Bug-1234-ThreeLittleMusketeers.pl" +"Bug:1234-1-ThreeLittleMusketeersFollowup1.pl" +"Bug 1234-2-ThreeLittleMusketeersFollowup2.pl" +"Bug-1235-FeaturelessFeature.sql" +"bug_7534.perl" +See --config for allowed prefix values. + + + -v --verbose Integer, 1 is not so verbose, 3 is maximally verbose. + + -D --dry-run Flag, Run the script but don't execute any atomicupdates. + You should use --verbose 3 to see what is happening. + + -h --help Flag, This nice help! + + -a --apply Flag, Apply all the pending atomicupdates from the + atomicupdates-directory. + + -d --directory Path, From which directory to look for atomicupdate-scripts. + Defaults to '$KOHA_PATH/installer/data/mysql/atomicupdate/' + + -s --single Path, execute a single atomicupdate-script. + eg. atomicupdate/Bug01243-SingleFeature.pl + + -r --remove String, Remove the upgrade entry from koha.atomicupdates + eg. --remove "Bug71337" + + -i --insert Path, Add an upgrade log entry for the given atomicupdate-file. + Useful to revert an accidental --remove -operation or for + testing. Does not execute the update script, simply adds + the log entry. + eg. -i installer/data/mysql/atomicupdate/Bug5453-Example.pl + + -l --list Flag, List all entries in the koha.atomicupdates-table. + This typically means all applied atomicupdates. + + -p --pending Flag, List all pending atomicupdates from the + atomicupdates-directory. + + -g --git Path, Build the update order from the Git repository given, + or default to the Git repository in $KOHA_PATH. + Eg. --git 1, to build with default values, or + --git /tmp/kohaclone/ to look for another repository + + -c --config The configuration file to load. Defaults to + '$KOHA_PATH/installer/data/mysql/atomicupdate.conf' + + The configuration file is an YAML-file, and must have the + following definitions: + + "Defines the prefixes used to identify the unique issue + identifier. You can give a normalizer function to the + identifier prefix." + example: + allowedIssueIdentifierPrefixes: + Bug: + ucfirst + "#": + normal + KD: + normal + + +EXAMPLES: + + atomicupdate.pl -g 1 -a + +Looks for the Git repository in $KOHA_PATH, parses the issue/commit identifiers +from the top 10000 commits and generates the _updateorder-file to tell in which +order the atomicupdates-scripts are executed. +Then applies all pending atomicupdate-scripts in the order (oldest to newest) +presented in the Git repository. + + + atomicupdate --apply -d /home/koha/kohaclone/installer/data/mysql/atomicupdate/ + +Applies all pending atomicupdate-scripts from the given directory. If the file +'_updateorder' is not present, it must be first generated, for example with the +--git 1 argument. + +UPDATEORDER: + +When deploying more than one atomicupdate, it is imperative to know in which order +the updates are applied. Atomicupdates can easily depend on each other and fail in +very strange and hard-to-debug -ways if the prerequisite modifications are not +in effect. +The correct update order is defined in the atomicupdates/_updateorder-file. This is +a simple list of issue/commit identifiers, eg. + + Bug5454 + Bug12432 + Bug12432-1 + Bug12432-2 + Bug3218 + #45 + +This file is most easily generated directly from the original Git repository, since +the order in which the Commits have been introduced most definetely is the order +they should be applied. +When deploying the atomicupdates to production environments without the +Git repository, the _updateorder file must be copied along the atomicupdate-scripts. + +P.S. Remember to put atomicupdate/_updateorder to your .gitignore + +ENDUSAGE + +if ( $help ) { + print $usage; + exit; +} + +my $atomicupdater = Koha::AtomicUpdater->new({verbose => $verbose, + scriptDir => $directory, + gitRepo => (length($git) == 1) ? '' : $git, + dryRun => $dryRun,} + ,); + +if ($git) { + $atomicupdater->buildUpdateOrderFromGit(10000); +} +if ($remove) { + $atomicupdater->removeAtomicUpdate($remove); +} +if ($insert) { + $atomicupdater->addAtomicUpdate({filename => $insert}); +} +if ($list) { + print $atomicupdater->listToConsole(); +} +if ($pending) { + print $atomicupdater->listPendingToConsole(); +} +if ($apply) { + $atomicupdater->applyAtomicUpdates(); +} +if ($single) { + $atomicupdater->applyAtomicUpdate($single); +} --- a/installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl +++ a/installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl @@ -0,0 +1,36 @@ +#!/usr/bin/perl + +# Copyright Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use C4::Context; + +my $dbh = C4::Context->dbh(); + +$dbh->do(" +CREATE TABLE `atomicupdates` ( + `atomicupdate_id` int(11) unsigned NOT NULL auto_increment, + `issue_id` varchar(20) NOT NULL, + `filename` varchar(30) NOT NULL, + `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`atomicupdate_id`), + UNIQUE KEY `origincode` (`issue_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +"); +$dbh->do("INSERT INTO atomicupdates (issue_id, filename) VALUES ('Bug14698', 'Bug14698-AtomicUpdater.pl')"); + +print "Upgrade to Bug 14698 - AtomicUpdater - Keeps track of which updates have been applied to a database done\n"; --- a/installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl +++ a/installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl @@ -0,0 +1,20 @@ +$DBversion = 'XXX'; # will be replaced by the RM +if( CheckVersion( $DBversion ) ) { + # you can use $dbh here like: + $dbh->do(" + CREATE TABLE `atomicupdates` ( + `atomicupdate_id` int(11) unsigned NOT NULL auto_increment, + `issue_id` varchar(20) NOT NULL, + `filename` varchar(128) NOT NULL, + `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`atomicupdate_id`), + UNIQUE KEY `atomic_issue_id` (`issue_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + + $dbh->do("INSERT INTO atomicupdates (issue_id, filename) VALUES ('Bug-14698', 'bug_14698-AtomicUpdater.perl')"); + + # Always end with this (adjust the bug info) + SetVersion( $DBversion ); + print "Upgrade to $DBversion done (Bug 14698 - AtomicUpdater - Keeps track of which updates have been applied to a database done)\n"; +} --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -15,6 +15,20 @@ /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +-- +-- Table structure for table `atomicupdates` +-- + +DROP TABLE IF EXISTS `atomicupdates`; +CREATE TABLE `atomicupdates` ( + `atomicupdate_id` int(11) unsigned NOT NULL auto_increment, + `issue_id` varchar(20) NOT NULL, + `filename` varchar(128) NOT NULL, + `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`atomicupdate_id`), + UNIQUE KEY `atomic_issue_id` (`issue_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- -- Table structure for table `auth_header` -- --- a/t/AtomicUpdater.t +++ a/t/AtomicUpdater.t @@ -0,0 +1,134 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Try::Tiny; +use Scalar::Util qw(blessed); + +use Test::More; + +use Koha::AtomicUpdate; + +plan tests => 3; + +use_ok('Koha::AtomicUpdate'); + +subtest "Check allowed atomicupdate file naming conventions" => sub { + + plan tests => 6; + + my (); + + my @goodTests = ( + [ 'Bug.1234-2:Trollollollol.perl', + 'Bug', '1234', '2', 'Trollollollol', 'perl', ], + [ 'KD-257-Mahtava_patchi.sql', + 'KD', '257', undef, 'Mahtava_patchi', 'sql', ], + [ 'bug_666_lazy_Koha_man_cant_type_proper_atomicupdate_filename.sql', + 'bug', '666', undef, 'lazy_Koha_man_cant_type_proper_atomicupdate_filename', 'sql', ], + [ 'G-8-Banksters.pl', + 'G', '8', undef, 'Banksters', 'pl', ], + [ 'B-12-6:Is_important.cpp', + 'B', '12', '6', 'Is_important', 'cpp', ], + ); + + my @forbiddenCharacters = ( + [ '#:445-HashTagForbidden', 'Koha::Exceptions::File' ], + ); + + foreach my $t (@goodTests) { + testName('fileName', @$t); + } + + foreach my $t (@forbiddenCharacters) { + my ($fileName, $exception) = @$t; + try { + Koha::AtomicUpdate::getFileNameElements($fileName); + ok(0, "$fileName should have crashed with $exception"); + } catch { + is(ref($_), $exception, "$fileName crashed with $exception"); + }; + } + +}; + + + +subtest "Check allowed atomicupdate Git title naming conventions" => sub { + + plan tests => 7; + + my (); + + my @goodTests = ( + [ 'Bug 1234-2 : Trollollollol', + 'Bug', '1234', '2', 'Trollollollol', ], + [ 'KD-257-Mahtava_patchi.sql', + 'KD', '257', undef, 'Mahtava_patchi.sql', ], + [ 'G - 8 - Banksters:Hip.Top', + 'G', '8', undef, 'Banksters:Hip.Top', ], + [ 'B -12- 6:Is_important.like.no.other', + 'B', '12', undef, '6:Is_important.like.no.other', ], + [ 'HSH-12412-1: Remove any # of characters', + 'HSH', '12412', 1, 'Remove any # of characters', ], + ); + + my @forbiddenCharacters = ( + [ '#:445-HashTagForbidden', 'Koha::Exceptions::File' ], + [ 'bug_666_lazy_Koha_man_cant_type_proper_atomicupdate_filename', 'Koha::Exceptions::Parse', ], + ); + + foreach my $t (@goodTests) { + testName('git', @$t); + } + + foreach my $t (@forbiddenCharacters) { + my ($title, $exception) = @$t; + try { + Koha::AtomicUpdate::getGitCommitTitleElements($title); + ok(0, "$title should have crashed with $exception"); + } catch { + is(ref($_), $exception, "$title crashed with $exception"); + }; + } + +}; + +################### +## TEST HELPERS ## + +sub testName { + my ($type, $nameTitle, $e_prefix, $e_issueNumber, $e_followupNumber, $e_issueDescription, $e_fileType) = @_; + + subtest "testName($nameTitle)" => sub { + + my ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType); + ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType) = + Koha::AtomicUpdate::getFileNameElements($nameTitle) + if $type eq 'fileName'; + ($prefix, $issueNumber, $followupNumber, $issueDescription) = + Koha::AtomicUpdate::getGitCommitTitleElements($nameTitle) + if $type eq 'git'; + + is($prefix, $e_prefix, 'prefix'); + is($issueNumber, $e_issueNumber, 'issue number'); + is($followupNumber, $e_followupNumber, 'followup number'); + is($issueDescription, $e_issueDescription, 'issue description'); + is($fileType, $e_fileType, 'file type') if $type eq 'fileName'; + + }; +} --- a/t/db_dependent/Koha/AtomicUpdater.t +++ a/t/db_dependent/Koha/AtomicUpdater.t @@ -0,0 +1,287 @@ +#!/usr/bin/perl + +# Copyright 2015 Open Source Freedom Fighters +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More; +use Try::Tiny; +use Encode; +use File::Spec; +use File::Path; +use File::Fu::File; + +use Koha::Database; +use Koha::AtomicUpdater; + +use t::lib::TestBuilder; + +plan tests => 8; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +#Create the _updateorder-file to a temp directory. +my $test_file = create_file({ + filepath => 'atomicupdate/', + filename => '_updateorder', + content => '' +}); + +use_ok('Koha::AtomicUpdater'); + +my $atomicupdate1 = Koha::AtomicUpdate->new({filename => 'Bug-12-WatchExMachinaYoullLikeIt.pl'})->store; +my $atomicupdate2 = Koha::AtomicUpdate->new({filename => 'Bug-14-ReturnOfZorro.perl'})->store; +my $atomicupdate3 = Koha::AtomicUpdate->new({filename => 'KD-14-RobotronInDanger.sql'})->store; +my $atomicupdate4 = Koha::AtomicUpdate->new({filename => 'KD-15-ILikedPrometheusButAlienWasBetter.pl'})->store; + +#Make sure we get the correct update order, otherwise we get unpredictable results. +{ #Overload existing subroutines to provide a Mock implementation + no warnings 'redefine'; + package Koha::AtomicUpdater; + sub _getGitCommits { #instead of requiring a Git repository, we just mock the input. + return [#Newest commit + '2e8a39762b506738195f21c8ff67e4e7bfe6dbba Bug_01243-SingleUpdate', + '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab KD:55 : Fiftyfive', + '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab KD-54 - KohaCon in Finland next year', + 'b447b595acacb0c4823582acf9d8a08902118e59 KD-53 - Place to be.pl', + '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab bug 112 - Lapinlahden linnut', + '5ac7101d4071fe11f7a5d1445bb97ed1a603a9b5 Bug:911 - What are you going to do?', + '1d54601b9cac0bd75ee97e071cf52ed49daef8bd KD-911 - Who are you going to call', + '1d54601b9cac0bd75ee97e071cf52ed49daef8bd bug 30 - Feature Yes yes', + '5ac7101d4071fe11f7a5d1445bb97ed1a603a9b5 KD-29 - Bug squashable', + '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab Bug : 28 - Feature Squash', + 'b447b595acacb0c4823582acf9d8a08902118e59 BUG 27 - Bug help', + #Oldest commit + ]; + } +} + +subtest "Followup naming convention" => sub { + + plan tests => 1; + + my $au = Koha::AtomicUpdate->new({filename => "Bug-535455-1-TestingFollowups.pl"}); + is($au->issue_id, "Bug-535455-1", "Followup Bug-535455-1 recognized"); + +}; + + +subtest "Create update order from Git repository" => sub { + plan tests => 8; + + #Instantiate the AtomicUpdater to operate on a temp directory. + my $atomicUpdater = Koha::AtomicUpdater->new({ + scriptDir => $test_file->dirname(), + }); + + #Start real testing. + my $issueIds = $atomicUpdater->buildUpdateOrderFromGit(4); + is($issueIds->[0], 'Bug-27', "First atomicupdate to deploy"); + is($issueIds->[1], 'Bug-28', "Second atomicupdate to deploy"); + is($issueIds->[2], 'KD-29', "Third atomicupdate to deploy"); + is($issueIds->[3], 'Bug-30', "Last atomicupdate to deploy"); + + #Testing file access + $issueIds = $atomicUpdater->getUpdateOrder(); + is($issueIds->[0], 'Bug-27', "First atomicupdate to deploy, from _updateorder"); + is($issueIds->[1], 'Bug-28', "Second atomicupdate to deploy, from _updateorder"); + is($issueIds->[2], 'KD-29', "Third atomicupdate to deploy, from _updateorder"); + is($issueIds->[3], 'Bug-30', "Last atomicupdate to deploy, from _updateorder"); + +}; + +subtest "List all deployed atomicupdates" => sub { + + plan tests => 4; + + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $text = $atomicUpdater->listToConsole(); + print $text; + + ok($text =~ m/Bug-12-WatchExMachinaYoullLik/, "Bug12-WatchExMachinaYoullLikeIt"); + ok($text =~ m/Bug-14-ReturnOfZorro.perl/, "Bug14-ReturnOfZorro"); + ok($text =~ m/KD-14-RobotronInDanger.sql/, "KD-14-RobotronInDanger"); + ok($text =~ m/KD-15-ILikedPrometheusButAli/, "KD-15-ILikedPrometheusButAlienWasBetter"); + +}; + +subtest "Delete an atomicupdate entry" => sub { + + plan tests => 2; + + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $atomicupdates = $atomicUpdater->search(); + ok($atomicupdate1->id, "AtomicUpdate '".$atomicupdate1->issue_id."' exists prior to deletion"); + + $atomicUpdater->removeAtomicUpdate($atomicupdate1->issue_id); + my $atomicupdate = $atomicUpdater->find($atomicupdate1->id); + ok(not($atomicupdate), "AtomicUpdate '".$atomicupdate1->issue_id."' deleted"); + +}; + +subtest "Insert an atomicupdate entry" => sub { + + plan tests => 2; + + my $atomicUpdater = Koha::AtomicUpdater->new(); + $atomicUpdater->addAtomicUpdate({filename => "Bug-15-Inserted.pl"}); + + my $atomicupdate = $atomicUpdater->find('Bug-15'); + ok($atomicupdate, "Bug-15-Inserted.pl inserted"); + + $atomicUpdater->removeAtomicUpdate($atomicupdate->issue_id); + $atomicupdate = $atomicUpdater->find('Bug-15'); + ok(not($atomicupdate), "Bug-15-Inserted.pl deleted"); + +}; + +subtest "List pending atomicupdates" => sub { + + plan tests => 13; + + ##Test adding update scripts and deploy them, confirm that no pending scripts detected + my $test_file1 = create_file({ + filepath => 'atomicupdate/', + filename => 'KD-911-WhoYouGonnaCall.pl', + content => '$ENV{ATOMICUPDATE_TESTS} = 1;', + }); + + my $test_file2 =create_file({ + filepath => 'atomicupdate/', + filename => 'Bug-911-WhatchaGonnaDo.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;', + }); + + my $test_file3 = create_file({ + filepath => 'atomicupdate/', + filename => 'Bug-112-LapinlahdenLinnut.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;', + }); + + my $atomicUpdater = Koha::AtomicUpdater->new({ + scriptDir => $test_file->dirname() + }); + + my $text = $atomicUpdater->listPendingToConsole(); + ok($text =~ m/KD-911-WhoYouGonnaCall.pl/, "KD-911-WhoYouGonnaCall is pending"); + ok($text =~ m/Bug-911-WhatchaGonnaDo.pl/, "Bug-911-WhatchaGonnaDo is pending"); + ok($text =~ m/Bug-112-LapinlahdenLinnut.pl/, 'Bug-112-LapinlahdenLinnut is pending'); + + my $atomicupdates = $atomicUpdater->applyAtomicUpdates(); + + is($atomicupdates->{'KD-911'}->issue_id, 'KD-911', "KD-911-WhoYouGonnaCall.pl deployed"); + is($atomicupdates->{'Bug-112'}->issue_id, 'Bug-112', 'Bug-112-LapinlahdenLinnut.pl deployed'); + is($atomicupdates->{'Bug-911'}->issue_id, 'Bug-911', "Bug-911-WhatchaGonnaDo.pl deployed"); + + ##Test adding scripts to the atomicupdates directory and how we deal with such change. + my $test_file4 = create_file({ + filepath => 'atomicupdate/', + filename => 'KD-53-PlaceToBe.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;', + }); + + my $test_file5 = create_file({ + filepath => 'atomicupdate/', + filename => 'KD-54-KohaConInFinlandNextYear.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;', + }); + + my $test_file6 = create_file({ + filepath => 'atomicupdate/', + filename => 'KD-55-Fiftyfive.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;', + }); + + $text = $atomicUpdater->listPendingToConsole(); + print $text; + + ok($text =~ m/KD-53-PlaceToBe.pl/, "KD-53-PlaceToBe.pl is pending"); + ok($text =~ m/KD-54-KohaConInFinlandNextYear.pl/, "KD-54-KohaConInFinlandNextYear.pl is pending"); + ok($text =~ m/KD-55-Fiftyfive.pl/u, 'KD-55-Fiftyfive.pl is pending'); + + $atomicupdates = $atomicUpdater->applyAtomicUpdates(); + + is($atomicupdates->{'KD-53'}->issue_id, 'KD-53', "KD-53-PlaceToBe.pl deployed"); + is($atomicupdates->{'KD-54'}->issue_id, 'KD-54', 'KD-54-KohaConInFinlandNextYear.pl deployed'); + is($atomicupdates->{'KD-55'}->issue_id, 'KD-55', "KD-55-Fiftyfive.pl deployed"); + + is($ENV{ATOMICUPDATE_TESTS}, 6, "All configured AtomicUpdates deployed"); + + $test_file1->remove; + $test_file2->remove; + $test_file3->remove; + $test_file4->remove; + $test_file5->remove; + $test_file6->remove; + +}; + + +subtest "Apply single atomicupdate from file" => sub { + + plan tests => 4; + + my $test_file = create_file({ + filepath => 'atomicupdate/', + filename => 'Bug_01243-SingleUpdate.pl', + content => '$ENV{ATOMICUPDATE_TESTS_2} = 10;', + }); + + ### Try first as a dry-run ### + my $atomicUpdater = Koha::AtomicUpdater->new({ + scriptDir => $test_file->dirname(), + dryRun => 1, + }); + + $atomicUpdater->applyAtomicUpdate($test_file->stringify); + my $atomicUpdate = $atomicUpdater->find('Bug-01243'); + + ok(not($atomicUpdate), "--dry-run doesn't add anything"); + is($ENV{ATOMICUPDATE_TESTS_2}, undef, "--dry-run doesn't execute anything"); + + ### Make a change! ### + $atomicUpdater = Koha::AtomicUpdater->new({ + scriptDir => $test_file->dirname(), + }); + + $atomicUpdater->applyAtomicUpdate($test_file->stringify); + $atomicUpdate = $atomicUpdater->find('Bug-01243'); + + is($atomicUpdate->filename, "Bug_01243-SingleUpdate.pl", "Bug_01243-SingleUpdate.pl added to DB"); + is($ENV{ATOMICUPDATE_TESTS_2}, 10, "Bug_01243-SingleUpdate.pl executed"); + + $test_file->remove; + +}; + +$test_file->remove; +$schema->storage->txn_rollback; + +sub create_file { + my ($file) = @_; + my $tmpdir = File::Spec->tmpdir(); + my $path = $tmpdir.'/'.$file->{filepath}; + File::Path::make_path($path); + my $test_file = File::Fu::File->new($path.'/'.$file->{filename}); + + $test_file->write($file->{content}) if $file->{content}; + + return $test_file; +} --