From 6e0cfe69008282c89484189be1ca6e2c22ff34a5 Mon Sep 17 00:00:00 2001 From: Olli-Antti Kivilahti Date: Wed, 15 Mar 2017 16:49:55 +0200 Subject: [PATCH 2/5] Bug 14698: AtomicUpdater - Keeps track of which updates have been applied to a database When deploying production databases to test environments, it is imperative to keep track of which database changes have been applied. When one is running, in production, features that require DB changes, and want to add more features which need more DB changes, keeping track of which updates have been applied is challenging. After several cycles of upgrade-deploy, with multiple atomicupdate-scripts piling to the atomicupdates/-directory, it is impossible to easily distinguish which updates have been applied and which are not. Rerunning the same update scripts again and again causes lots of noise in the upgrade log and hides real issues from being detected. Also repeatedly running upgrade scripts might cause bad side effects which are potentially hard to repair. This feature adds a script atomicupdate.pl which runs all atomicupdate/* scripts and .sqls and logs the event to the koha.atomicupdates-table. On subsequent runs of atomicupdate.pl, the already deployed upgrades are skipped, greatly reducing the unnecessary clutter and risk of upgrading. Also the order in which the atomicupdates need to be applied is important. This _updateorder is automatically generatable from the original Git repository. You can also remove existing log entries to allow rerunning the same upgrade script again, list all applied upgrades and show pending updates. Unit tests included. Prove t/AtomicUpdater.t and t/db_dependent/Koha/AtomicUpdater.t TEST PLAN: 1. Run "perl installer/data/mysql/atomicupdate.pl --help" 2. Read the code. 3. git bz apply some bugs which introduce atomicupdates, or cook your own 4. Deploy the atomicupdates using the atomicupdate.pl-script. 5. Repeat steps 3-4 to your heart's content. 6+. Understand the code and come up with improvement ideas! --- 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 diff --git a/Koha/AtomicUpdate.pm b/Koha/AtomicUpdate.pm new file mode 100644 index 0000000000..fd19c73497 --- /dev/null +++ b/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; diff --git a/Koha/AtomicUpdater.pm b/Koha/AtomicUpdater.pm new file mode 100644 index 0000000000..b9625c888a --- /dev/null +++ b/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; diff --git a/Koha/Schema/Result/Atomicupdate.pm b/Koha/Schema/Result/Atomicupdate.pm new file mode 100644 index 0000000000..2ca8c8dfa9 --- /dev/null +++ b/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; diff --git a/cpanfile b/cpanfile index f7d69c3f84..2e7dc488e7 100644 --- a/cpanfile +++ b/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'; diff --git a/installer/data/mysql/atomicupdate.pl b/installer/data/mysql/atomicupdate.pl new file mode 100644 index 0000000000..e61d88ee84 --- /dev/null +++ b/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); +} diff --git a/installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl b/installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl new file mode 100644 index 0000000000..d07f2605cb --- /dev/null +++ b/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"; diff --git a/installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl b/installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl new file mode 100644 index 0000000000..808e66e849 --- /dev/null +++ b/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"; +} diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index 4308bcdc21..32a477aecf 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/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` -- diff --git a/t/AtomicUpdater.t b/t/AtomicUpdater.t new file mode 100644 index 0000000000..e91d20d5ae --- /dev/null +++ b/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'; + + }; +} diff --git a/t/db_dependent/Koha/AtomicUpdater.t b/t/db_dependent/Koha/AtomicUpdater.t new file mode 100644 index 0000000000..14abf95971 --- /dev/null +++ b/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; +} \ No newline at end of file -- 2.17.1