Bugzilla – Attachment 102900 Details for
Bug 14698
AtomicUpdater - Keeps track of which updates have been applied to a database
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 14698: AtomicUpdater - Keeps track of which updates have been applied to a database
0002-Bug-14698-AtomicUpdater-Keeps-track-of-which-updates.patch (text/plain), 53.85 KB, created by
Emmi Takkinen
on 2020-04-14 08:24:50 UTC
(
hide
)
Description:
Bug 14698: AtomicUpdater - Keeps track of which updates have been applied to a database
Filename:
MIME Type:
Creator:
Emmi Takkinen
Created:
2020-04-14 08:24:50 UTC
Size:
53.85 KB
patch
obsolete
>From 6e0cfe69008282c89484189be1ca6e2c22ff34a5 Mon Sep 17 00:00:00 2001 >From: Olli-Antti Kivilahti <olli-antti.kivilahti@jns.fi> >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<atomicupdates> >+ >+=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</atomicupdate_id> >+ >+=back >+ >+=cut >+ >+__PACKAGE__->set_primary_key("atomicupdate_id"); >+ >+=head1 UNIQUE CONSTRAINTS >+ >+=head2 C<atomic_issue_id> >+ >+=over 4 >+ >+=item * L</issue_id> >+ >+=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 >+<prefix><separator><issue_number><separator><followup_number><separator><issueDescription><file_type> >+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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 14698
:
42112
|
42113
|
42385
|
42386
|
42395
|
42398
|
42426
|
42521
|
42523
|
62141
|
64404
|
102899
| 102900 |
102901
|
102902
|
102903