From a8da178c3e9abd1cd9673510f541dbbbbb190ddc Mon Sep 17 00:00:00 2001 From: Olli-Antti Kivilahti Date: Thu, 20 Aug 2015 14:10:02 +0300 Subject: [PATCH] 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. 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. --- C4/Installer/PerlDependencies.pm | 5 + Koha/AtomicUpdate.pm | 121 +++++++++++ Koha/AtomicUpdater.pm | 223 +++++++++++++++++++++ installer/data/mysql/atomicupdate.pl | 96 +++++++++ .../mysql/atomicupdate/Bug14698-AtomicUpdater.pl | 36 ++++ installer/data/mysql/kohastructure.sql | 14 ++ t/db_dependent/Koha/AtomicUpdater.t | 200 ++++++++++++++++++ t/lib/TestObjects/AtomicUpdateFactory.pm | 105 ++++++++++ t/lib/TestObjects/ObjectFactory.pm | 5 + t/lib/TestObjects/objectFactories.t | 39 ++++ 10 files changed, 844 insertions(+) create mode 100644 Koha/AtomicUpdate.pm create mode 100644 Koha/AtomicUpdater.pm create mode 100644 installer/data/mysql/atomicupdate.pl create mode 100644 installer/data/mysql/atomicupdate/Bug14698-AtomicUpdater.pl create mode 100644 t/db_dependent/Koha/AtomicUpdater.t create mode 100644 t/lib/TestObjects/AtomicUpdateFactory.pm diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index d052fde..189bf77 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -232,6 +232,11 @@ our $PERL_DEPS = { 'min_ver' => '0.45', # Also needed for our use of PDF::Reuse }, + 'Data::Format::Pretty::Console' => { + 'usage' => 'Core', + 'required' => '1', + 'min_ver' => '0.34', + }, 'DateTime' => { 'usage' => 'Core', 'required' => '1', diff --git a/Koha/AtomicUpdate.pm b/Koha/AtomicUpdate.pm new file mode 100644 index 0000000..92fbbff --- /dev/null +++ b/Koha/AtomicUpdate.pm @@ -0,0 +1,121 @@ +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::Exception::BadParameter; + +sub type { + return 'Atomicupdate'; +} + +=head @allowedIssueIdentifierPrefixes +Define the prefixes you want to attach to your atomicupdate filenames here. +This could be a syspref or in KOHA_CONF, but it is rather easy to just add more +generally used issue number prefixes here. +Nobody wants more sysprefs. +=cut + +my @allowedIssueIdentifierPrefixes = ( + 'Bug', + '#', +); + +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::Exception::BadParameter->throw( + error => "$class->_validateParams():> Param '$mp' must be given.") + unless($params->{$mp}); + } + $params->{filename} = $class->_validateFilename($params->{filename}); + + $params->{issue_id} = $class->_getIssueIdentifier($params->{issue_id} || $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::Exception::File, if the given file doesn't have a proper naming convention + +=cut + +sub _validateFilename { + my ($self, $fileName) = @_; + + Koha::Exception::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 + +Extracts the unique issue identifier from the atomicupdate DB upgrade script. + +@PARAM1 String, filename of validatable file, excluding path. +@RETURNS String, The unique issue identifier + +@THROWS Koha::Exception::Parse, if the unique identifier couldn't be parsed. +=cut + +sub _getIssueIdentifier { + my ($self, $fileName) = @_; + + foreach my $prefix (@allowedIssueIdentifierPrefixes) { + if ($fileName =~ m/$prefix[ -:]*?(\d+)/) { + return "$prefix$1"; + } + } + Koha::Exception::Parse->throw(error => __PACKAGE__."->_getIssueIdentifier($fileName):> couldn't parse the unique issue identifier from filename using allowed prefixes '@allowedIssueIdentifierPrefixes'"); +} + +1; diff --git a/Koha/AtomicUpdater.pm b/Koha/AtomicUpdater.pm new file mode 100644 index 0000000..1a9aafd --- /dev/null +++ b/Koha/AtomicUpdater.pm @@ -0,0 +1,223 @@ +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 Koha::Database; +use Koha::Cache; +use Koha::AtomicUpdate; + +use base qw(Koha::Objects); + +use Koha::Exception::File; +use Koha::Exception::Parse; +use Koha::Exception::BadParameter; + +sub type { + return 'Atomicupdate'; +} + +sub object_class { + return 'Koha::AtomicUpdate'; +} + +sub _get_castable_unique_columns { + return ['atomicupdate_id']; +} + +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/'; + + return $self; +} + +=head getAtomicUpdates + + my $atomicUpdates = $atomicUpdater->getAtomicUpdates(); + +Gets all the AtomicUpdate-objects in the DB. This result should be Koha::Cached. +@RETURNS ARRAYRef of Koha::AtomicUpdate-objects. +=cut + +sub getAtomicUpdates { + my ($self) = @_; + + my @au = $self->search({}); + my %au; #HASHify the AtomicUpdate-objects for easy searching. + foreach my $au (@au) { + $au{$au->filename} = $au; + } + return \%au; +} + +sub addAtomicUpdate { + my ($self, $params) = @_; + print "Adding atomicupdate '".$params->{issue_id}."'\n" if $self->{verbose} > 2; + + my $atomicupdate = Koha::AtomicUpdate->new($params); + $atomicupdate->store(); + $atomicupdate = $self->find({issue_id => $atomicupdate->issue_id}); + return $atomicupdate; +} + +sub removeAtomicUpdate { + my ($self, $issueId) = @_; + print "Deleting atomicupdate '$issueId'\n" if $self->{verbose} > 2; + + my $atomicupdate = $self->find({issue_id => $issueId}); + if ($atomicupdate) { + $atomicupdate->delete; + print "Deleted atomicupdate '$issueId'\n" if $self->{verbose} > 2; + } + else { + Koha::Exception::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}; + unless ($atomicUpdatesDeployed->{$au->filename}) { + #This script hasn't been deployed. + $pendingAtomicUpdates{$au->filename} = $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. + +@RETURNS ARRAYRef of Koha::AtomicUpdate-objects deployed on this run +=cut + +sub applyAtomicUpdates { + my ($self) = @_; + + my %appliedUpdates; + + my $atomicUpdates = $self->getPendingAtomicUpdates(); + foreach my $key ( keys %$atomicUpdates ) { + my $atomicUpdate = $atomicUpdates->{$key}; + my $filename = $atomicUpdate->filename; + print "Looking at file '$filename'\n" if $self->{verbose} > 2; + + if ( $filename =~ /\.sql$/ ) { + my $installer = C4::Installer->new(); + my $rv = $installer->load_sql( $self->{scriptDir}.'/'.$filename ) ? 0 : 1; + } elsif ( $filename =~ /\.(perl|pl)$/ ) { + do $self->{scriptDir}.'/'.$filename; + } + + $atomicUpdate->store(); + $appliedUpdates{$filename} = $atomicUpdate; + print "File '$filename' applied" if $self->{verbose} > 2; + } + + return \%appliedUpdates; +} + +=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 filename excluding the full path. + Eg. {'Bug8584-FoundLeBug.pl' => 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::Exception::File')) { + #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{$file} = $atomicUpdate; + } + return \%atomicUpdates; +} + +1; diff --git a/installer/data/mysql/atomicupdate.pl b/installer/data/mysql/atomicupdate.pl new file mode 100644 index 0000000..5077174 --- /dev/null +++ b/installer/data/mysql/atomicupdate.pl @@ -0,0 +1,96 @@ +#!/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; + +use C4::Context; + +use Koha::AtomicUpdater; + +my $verbose = 0; +my $help = 0; +my $apply = 0; +my $remove = ''; +my $insert = ''; +my $list = 0; +my $pending = 0; +my $directory = ''; + +GetOptions( + 'v|verbose:i' => \$verbose, + 'h|help' => \$help, + 'a|apply' => \$apply, + 'd|directory:s' => \$directory, + 'r|remove:s' => \$remove, + 'i|insert:s' => \$insert, + 'l|list' => \$list, + 'p|pending' => \$pending, +); + +my $usage = << 'ENDUSAGE'; + +Runs all the atomicupdate-scripts and sql in the atomicupdates-directory. +Checks from koha.database_updates if the update has already been applied. + +Also acts as a gateway to CRUD the koha.database_updates-table to easily remove +existing upgrade logs. + + -v --verbose Integer, 1 is not so verbose, 3 is maximally verbose. + -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/' + -r --remove String, Remove the upgrade entry from koha.database_updates + 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. + eg. -i installer/data/mysql/atomicupdate/Bug5453-Example.pl + -l --list Flag, List all entries in the koha.database_updates-table. + This typically means all applied atomicupdates. + -p --pending Flag, List all pending atomicupdates from the + atomicupdates-directory. + +ENDUSAGE + +if ( $help ) { + print $usage; + exit; +} + +my $atomicupdater = Koha::AtomicUpdater->new({verbose => $verbose}); + +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(); +} diff --git a/installer/data/mysql/atomicupdate/Bug14698-AtomicUpdater.pl b/installer/data/mysql/atomicupdate/Bug14698-AtomicUpdater.pl new file mode 100644 index 0000000..177f150 --- /dev/null +++ b/installer/data/mysql/atomicupdate/Bug14698-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=utf8 COLLATE=utf8_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/kohastructure.sql b/installer/data/mysql/kohastructure.sql index fba99ac..006495b 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -1801,6 +1801,20 @@ CREATE TABLE `printers_profile` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -- +-- 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=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `repeatable_holidays` -- diff --git a/t/db_dependent/Koha/AtomicUpdater.t b/t/db_dependent/Koha/AtomicUpdater.t new file mode 100644 index 0000000..436086f --- /dev/null +++ b/t/db_dependent/Koha/AtomicUpdater.t @@ -0,0 +1,200 @@ +#!/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 t::lib::TestObjects::ObjectFactory; +use t::lib::TestObjects::AtomicUpdateFactory; +use t::lib::TestObjects::FileFactory; +use Koha::AtomicUpdater; + +my $testContext = {}; +my $atomicupdates = t::lib::TestObjects::AtomicUpdateFactory->createTestGroup([ + {issue_id => 'Bug12', + filename => 'Bug12-WatchExMachinaYoullLikeIt.pl'}, + {issue_id => 'Bug14', + filename => 'Bug14-ReturnOfZorro.perl'}, + {issue_id => '#14', + filename => '#14-RobotronInDanger.sql'}, + {issue_id => '#15', + filename => '#15-ILikedPrometheusButAlienWasBetter.pl'}, + ], undef, $testContext); + +subtest "List all deployed atomicupdates" => \&listAtomicUpdates; +sub listAtomicUpdates { + eval { + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $text = $atomicUpdater->listToConsole(); + print $text; + + ok($text =~ m/Bug12-WatchExMachinaYoullLik/, + "Bug12-WatchExMachinaYoullLikeIt"); + ok($text =~ m/Bug14-ReturnOfZorro.perl/, + "Bug14-ReturnOfZorro"); + ok($text =~ m/#14-RobotronInDanger.sql/, + "#14-RobotronInDanger"); + ok($text =~ m/#15-ILikedPrometheusButAli/, + "#15-ILikedPrometheusButAlienWasBetter"); + + }; + if ($@) { + ok(0, $@); + } +} + +subtest "Delete an atomicupdate entry" => \&deleteAtomicupdate; +sub deleteAtomicupdate { + eval { + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $atomicupdate = $atomicUpdater->cast($atomicupdates->{Bug12}->id); + ok($atomicupdate, + "AtomicUpdate '".$atomicupdates->{Bug12}->issue_id."' exists prior to deletion"); + + $atomicUpdater->removeAtomicUpdate($atomicupdate->issue_id); + $atomicupdate = $atomicUpdater->find($atomicupdates->{Bug12}->id); + ok(not($atomicupdate), + "AtomicUpdate '".$atomicupdates->{Bug12}->issue_id."' deleted"); + + }; + if ($@) { + ok(0, $@); + } +} + +subtest "Insert an atomicupdate entry" => \&insertAtomicupdate; +sub insertAtomicupdate { + eval { + my $atomicUpdater = Koha::AtomicUpdater->new(); + my $subtestContext = {}; + my $atomicupdates = t::lib::TestObjects::AtomicUpdateFactory->createTestGroup([ + {issue_id => 'Bug15', + filename => 'Bug15-Inserted.pl'}, + ], undef, $subtestContext, $testContext); + my $atomicupdate = $atomicUpdater->find({issue_id => 'Bug15'}); + ok($atomicupdate, + "Bug15-Inserted.pl"); + + t::lib::TestObjects::ObjectFactory->tearDownTestContext($subtestContext); + + $atomicupdate = $atomicUpdater->find({issue_id => 'Bug15'}); + ok(not($atomicupdate), + "Bug15-Inserted.pl deleted"); + }; + if ($@) { + ok(0, $@); + } +} + +subtest "List pending atomicupdates" => \&listPendingAtomicupdates; +sub listPendingAtomicupdates { + my ($atomicUpdater, $files, $text, $atomicupdates); + my $subtestContext = {}; + eval { + ##Test adding update scripts and deploy them, confirm that no pending scripts detected + $files = t::lib::TestObjects::FileFactory->createTestGroup([ + { filepath => 'atomicupdate/', + filename => '#911-WhoYouGonnaCall.pl', + content => '$ENV{ATOMICUPDATE_TESTS} = 1;',}, + { filepath => 'atomicupdate/', + filename => 'Bug911-WhatchaGonnaDo.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;',}, + { filepath => 'atomicupdate/', + filename => 'Bug112-LapinlahdenLinnut.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;',}, + ], + undef, $subtestContext, $testContext); + $atomicUpdater = Koha::AtomicUpdater->new({ + scriptDir => $files->{'#911-WhoYouGonnaCall.pl'}->dirname() + }); + + $text = $atomicUpdater->listPendingToConsole(); + print $text; + + ok($text =~ m/#911-WhoYouGonnaCall.pl/, + "#911-WhoYouGonnaCall is pending"); + ok($text =~ m/Bug911-WhatchaGonnaDo.pl/, + "Bug911-WhatchaGonnaDo is pending"); + ok($text =~ m/Bug112-LapinlahdenLinnut.pl/, + 'Bug112-LapinlahdenLinnut is pending'); + + $atomicupdates = $atomicUpdater->applyAtomicUpdates(); + t::lib::TestObjects::AtomicUpdateFactory->addToContext($atomicupdates, undef, $subtestContext, $testContext); #Keep track of changes + + is($atomicupdates->{'#911-WhoYouGonnaCall.pl'}->issue_id, + '#911', + "#911-WhoYouGonnaCall.pl deployed"); + is($atomicupdates->{'Bug112-LapinlahdenLinnut.pl'}->issue_id, + 'Bug112', + 'Bug112-LapinlahdenLinnut.pl deployed'); + is($atomicupdates->{'Bug911-WhatchaGonnaDo.pl'}->issue_id, + 'Bug911', + "Bug911-WhatchaGonnaDo.pl deployed"); + + ##Test adding scripts to the atomicupdates directory and how we deal with such change. + $files = t::lib::TestObjects::FileFactory->createTestGroup([ + { filepath => 'atomicupdate/', + filename => '#53-PlaceToBe.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;',}, + { filepath => 'atomicupdate/', + filename => '#54-KohaConInFinlandNextYear.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;',}, + { filepath => 'atomicupdate/', + filename => '#55-Fiftyfive.pl', + content => '$ENV{ATOMICUPDATE_TESTS}++;',}, + ], + undef, $subtestContext, $testContext); + + $text = $atomicUpdater->listPendingToConsole(); + print $text; + + ok($text =~ m/#53-PlaceToBe.pl/, + "#53-PlaceToBe.pl is pending"); + ok($text =~ m/#54-KohaConInFinlandNextYear.pl/, + "#54-KohaConInFinlandNextYear.pl is pending"); + ok($text =~ m/#55-Fiftyfive.pl/u, + '#55-Fiftyfive.pl'); + + $atomicupdates = $atomicUpdater->applyAtomicUpdates(); + t::lib::TestObjects::AtomicUpdateFactory->addToContext($atomicupdates, undef, $subtestContext, $testContext); #Keep track of changes + + is($atomicupdates->{'#53-PlaceToBe.pl'}->issue_id, + '#53', + "#53-PlaceToBe.pl deployed"); + is($atomicupdates->{'#54-KohaConInFinlandNextYear.pl'}->issue_id, + '#54', + '#54-KohaConInFinlandNextYear.pl deployed'); + is($atomicupdates->{'#55-Fiftyfive.pl'}->issue_id, + '#55', + "#55-Fiftyfive.pl deployed"); + + is($ENV{ATOMICUPDATE_TESTS}, + 6, + "All configured AtomicUpdates deployed"); + }; + if ($@) { + ok(0, $@); + } + t::lib::TestObjects::AtomicUpdateFactory->tearDownTestContext($subtestContext); +} + +t::lib::TestObjects::ObjectFactory->tearDownTestContext($testContext); +done_testing; \ No newline at end of file diff --git a/t/lib/TestObjects/AtomicUpdateFactory.pm b/t/lib/TestObjects/AtomicUpdateFactory.pm new file mode 100644 index 0000000..4aff6f1 --- /dev/null +++ b/t/lib/TestObjects/AtomicUpdateFactory.pm @@ -0,0 +1,105 @@ +package t::lib::TestObjects::AtomicUpdateFactory; + +# 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 Carp; + +use Koha::AtomicUpdater; +use Koha::Database; + +use Koha::Exception::UnknownProgramState; + +use base qw(t::lib::TestObjects::ObjectFactory); + +sub getDefaultHashKey { + return 'issue_id'; +} +sub getObjectType { + return 'Koha::AtomicUpdate'; +} + +=head t::lib::TestObjects::createTestGroup + + my $atomicupdates = t::lib::TestObjects::AtomicUpdateFactory->createTestGroup([ + {'issue_id' => 'Bug3432', + 'filename' => 'Bug3432-RavingRabbitsMayhem', + 'modification_time' => '2015-01-02 15:59:32', + }, + ], undef, $testContext1, $testContext2, $testContext3); + +Calls Koha::AtomicUpdater to add a Koha::AtomicUpdate object to DB. + +The HASH is keyed with the 'koha.atomicupdates.issue_id', or the given $hashKey. + +There is a duplication check to first look for atomicupdate-rows with the same 'issue_id'. +If a matching atomicupdate is found, then we use the existing Record instead of adding a new one. + +@RETURNS HASHRef of Koha::AtomicUpdate-objects + +See t::lib::TestObjects::ObjectFactory for more documentation +=cut + +sub handleTestObject { + my ($class, $object, $stashes) = @_; + + ##First see if the given Record already exists in the DB. For testing purposes we use the isbn as the UNIQUE identifier. + my $atomicupdate = Koha::AtomicUpdater->find({issue_id => $object->{issue_id}}); + unless ($atomicupdate) { + my $atomicupdater = Koha::AtomicUpdater->new(); + $atomicupdate = $atomicupdater->addAtomicUpdate($object); + } + + Koha::Exception::UnknownProgramState->throw(error => "$class->handleTestObject():> Cannot create a new object\n$@\n") + unless $atomicupdate; + + return $atomicupdate; +} + +=head validateAndPopulateDefaultValues +@OVERLOAD + +Validates given Object parameters and makes sure that critical fields are given +and populates defaults for missing values. +=cut + +sub validateAndPopulateDefaultValues { + my ($self, $object, $hashKey) = @_; + + $object->{issue_id} = 'BugRancidacid' unless $object->{issue_id}; + $object->{filename} = 'BugRancidacid-LaboratoryExperimentsGoneSour' unless $object->{filename}; + + $self->SUPER::validateAndPopulateDefaultValues($object, $hashKey); +} + +sub deleteTestGroup { + my ($class, $objects) = @_; + + while( my ($key, $object) = each %$objects) { + my $atomicupdate = Koha::AtomicUpdater->cast($object); + eval { + $atomicupdate->delete(); + }; + if ($@) { + warn "$class->deleteTestGroup():> Error hapened: $@\n"; + } + } +} + +1; diff --git a/t/lib/TestObjects/ObjectFactory.pm b/t/lib/TestObjects/ObjectFactory.pm index e4f6441..fb6de71 100644 --- a/t/lib/TestObjects/ObjectFactory.pm +++ b/t/lib/TestObjects/ObjectFactory.pm @@ -166,6 +166,11 @@ sub tearDownTestContext { t::lib::TestObjects::BiblioFactory->deleteTestGroup($stash->{biblio}); delete $stash->{biblio}; } + if ($stash->{atomicupdate}) { + require t::lib::TestObjects::AtomicUpdateFactory; + t::lib::TestObjects::AtomicUpdateFactory->deleteTestGroup($stash->{atomicupdate}); + delete $stash->{atomicupdate}; + } if ($stash->{borrower}) { require t::lib::TestObjects::BorrowerFactory; t::lib::TestObjects::BorrowerFactory->deleteTestGroup($stash->{borrower}); diff --git a/t/lib/TestObjects/objectFactories.t b/t/lib/TestObjects/objectFactories.t index c6668b9..7a4ef65 100644 --- a/t/lib/TestObjects/objectFactories.t +++ b/t/lib/TestObjects/objectFactories.t @@ -30,6 +30,8 @@ use t::lib::TestObjects::BorrowerFactory; use Koha::Borrowers; use t::lib::TestObjects::ItemFactory; use Koha::Items; +use t::lib::TestObjects::AtomicUpdateFactory; +use Koha::AtomicUpdater; use t::lib::TestObjects::BiblioFactory; use Koha::Biblios; use t::lib::TestObjects::CheckoutFactory; @@ -472,6 +474,43 @@ sub testLetterTemplateFactory { +########## AtomicUpdateFactory subtests ########## +subtest 't::lib::TestObjects::AtomicUpdateFactory' => \&testAtomicUpdateFactory; +sub testAtomicUpdateFactory { + my ($atomicUpdater, $atomicupdate); + my $subtestContext = {}; + ##Create and Delete using dependencies in the $testContext instantiated in previous subtests. + my $atomicupdates = t::lib::TestObjects::AtomicUpdateFactory->createTestGroup([ + {'issue_id' => 'Bug10', + 'filename' => 'Bug10-RavingRabbitsMayhem', + 'modification_time' => '2015-01-02 15:59:32',}, + {'issue_id' => 'Bug11', + 'filename' => 'Bug11-RancidSausages', + 'modification_time' => '2015-01-02 15:59:33',}, + ], + undef, $subtestContext); + $atomicUpdater = Koha::AtomicUpdater->new(); + $atomicupdate = $atomicUpdater->find({issue_id => $atomicupdates->{Bug10}->issue_id}); + is($atomicupdate->issue_id, + 'Bug10', + "Bug10-RavingRabbitsMayhem created"); + $atomicupdate = $atomicUpdater->find({issue_id => $atomicupdates->{Bug11}->issue_id}); + is($atomicupdate->issue_id, + 'Bug11', + "Bug11-RancidSausages created"); + + t::lib::TestObjects::ObjectFactory->tearDownTestContext($subtestContext); + + $atomicupdate = $atomicUpdater->find({issue_id => $atomicupdates->{Bug10}->issue_id}); + ok(not($atomicupdate), + "Bug10-RavingRabbitsMayhem deleted"); + $atomicupdate = $atomicUpdater->find({issue_id => $atomicupdates->{Bug11}->issue_id}); + ok(not($atomicupdate), + "Bug11-RancidSausages created"); +}; + + + ########## SystemPreferenceFactory subtests ########## subtest 't::lib::TestObjects::SystemPreferenceFactory' => \&testSystemPreferenceFactory; sub testSystemPreferenceFactory { -- 1.9.1