View | Details | Raw Unified | Return to bug 14698
Collapse All | Expand All

(-)a/Koha/AtomicUpdate.pm (+192 lines)
Line 0 Link Here
1
package Koha::AtomicUpdate;
2
3
# Copyright Open Source Freedom Fighters
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
use Carp;
22
use File::Basename;
23
24
use Koha::Database;
25
26
use base qw(Koha::Object);
27
28
use Koha::Exceptions::BadParameter;
29
use Koha::Exceptions::Parse;
30
use Koha::Exceptions::File;
31
32
33
sub _type {
34
    return 'Atomicupdate';
35
}
36
37
=head new
38
39
    my $atomicUpdate = Koha::AtomicUpdate->new({filename => 'Bug54321-FixItPlease.pl'});
40
41
Creates a Koha::AtomicUpdate-object from the given parameters-HASH
42
@PARAM1 HASHRef of object parameters:
43
        'filename' => MANDATORY, The filename of the atomicupdate-script without the path-component.
44
        'issue_id' => OPTIONAL, the desired issue_id. It is better to let the module
45
                                find this from the filename, but is useful for testing purposes.
46
@RETURNS Koha::AtomicUpdate-object
47
@THROWS Koha::Exceptions::Parse from getIssueIdentifier()
48
@THROWS Koha::Exceptions::File from _validateFilename();
49
=cut
50
51
sub new {
52
    my ($class, $params) = @_;
53
    $class->_validateParams($params);
54
55
    my $self = {};
56
    bless($self, $class);
57
    $self->set($params);
58
    return $self;
59
}
60
61
sub _validateParams {
62
    my ($class, $params) = @_;
63
64
    my @mandatoryParams = ('filename');
65
    foreach my $mp (@mandatoryParams) {
66
        Koha::Exceptions::BadParameter->throw(
67
            error => "$class->_validateParams():> Param '$mp' must be given.")
68
                unless($params->{$mp});
69
    }
70
    $params->{filename} = $class->_validateFilename($params->{filename});
71
72
    $params->{issue_id} = getIssueIdentifier($params->{filename});
73
}
74
75
=head _validateFilename
76
77
Makes sure the given file is a valid AtomicUpdate-script.
78
Currently simply checks for naming convention and file suffix.
79
80
NAMING CONVENTION:
81
    Filename must contain one of the unique issue identifier prefixes from this
82
    list @allowedIssueIdentifierPrefixes immediately followed by the numeric
83
    id of the issue, optionally separated by any of the following [ :-]
84
    Eg. Bug-45453, #102, #:53
85
86
@PARAM1 String, filename of validatable file, excluding path.
87
@RETURNS String, the koha.atomicupdates.filename if the given file is considered a well formed update script.
88
                 Removes the full path if present and returns only the filename component.
89
90
@THROWS Koha::Exceptions::File, if the given file doesn't have a proper naming convention
91
92
=cut
93
94
sub _validateFilename {
95
    my ($self, $fileName) = @_;
96
97
    Koha::Exceptions::File->throw(error => __PACKAGE__."->_validateFilename():> Filename '$fileName' has unknown suffix")
98
            unless $fileName =~ /\.(sql|perl|pl)$/;  #skip other files
99
100
    $fileName = File::Basename::basename($fileName);
101
102
    return $fileName;
103
}
104
105
=head getIssueIdentifier
106
@STATIC
107
108
Extracts the unique issue identifier from the atomicupdate DB upgrade script.
109
110
@PARAM1 String, filename, excluding path.
111
        OR
112
@PARAM2 String, Git commit title.
113
@RETURNS String, The unique issue identifier
114
115
@THROWS Koha::Exceptions::Parse, if the unique identifier couldn't be parsed.
116
=cut
117
118
sub getIssueIdentifier {
119
    my ($fileName, $gitTitle) = @_;
120
121
    Koha::Exceptions::BadParameter->throw(error => "Either \$gitTitle or \$fileName must be given!") unless ($fileName || $gitTitle);
122
123
    my ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type);
124
    ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type) =
125
            getFileNameElements($fileName)
126
                if $fileName;
127
    ($prefix, $issueNumber, $followupNumber, $issueDescription, $file_type) =
128
            getGitCommitTitleElements($gitTitle)
129
                if $gitTitle;
130
131
    $prefix = uc $prefix if length $prefix <= 2;
132
    $prefix = ucfirst(lc($prefix)) if length $prefix > 2;
133
134
    my @keys = ($prefix, $issueNumber);
135
    push(@keys, $followupNumber) if $followupNumber;
136
    return join('-', @keys);
137
}
138
139
=head2 getFileNameElements
140
@STATIC
141
142
Parses the given file name for atomicupdater markers.
143
144
@PARAM1 String, base filename of the atomicupdate-file
145
@RETURNS ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType)
146
@THROWS Koha::Exceptions::Parse, if the fileName couldn't be parsed.
147
148
=cut
149
150
sub getFileNameElements {
151
    my ($fileName) = @_;
152
153
    Koha::Exceptions::File->throw(error =>
154
        __PACKAGE__."->getIssueNameElements($fileName):> \$fileName cannot contain the comment-character '\x23'.".
155
        " It will screw up the make build chain.") if $fileName =~ /\x23/;
156
157
    if ($fileName =~ /^([a-zA-Z]{1,3})(?:\W|[_])?(\d+)(?:(?:\W|[_])(\d+))?(?:(?:\W|[_])(.+?))?\.(\w{1,5})$/) {
158
        return ($1, $2, $3, $4, $5);
159
    }
160
161
    Koha::Exceptions::Parse->throw(error => __PACKAGE__."->getIssueNameElements($fileName):> Couldn't parse the given \$fileName");
162
}
163
164
=head2 getGitCommitTitleElements
165
@STATIC
166
167
Parses the given Git commit title for atomicupdater markers.
168
169
@PARAM1 String, git commit title
170
@RETURNS ($prefix, $issueNumber, $followupNumber, $issueDescription)
171
@THROWS Koha::Exceptions::Parse, if the title couldn't be parsed.
172
173
=cut
174
175
sub getGitCommitTitleElements {
176
    my ($title) = @_;
177
178
    Koha::Exceptions::File->throw(error =>
179
        __PACKAGE__."->getGitCommitTitleElements($title):> \$prefix cannot contain the comment-character '\x23'.".
180
        " It will screw up the make build chain.") if $title =~ /^.{0,2}\x23.{0,2} ?\W ?/;
181
182
    if ($title =~ /^(\w{1,3})(?: ?\W ?)(\d+)(?:(?:\W)(\d+))?(?: ?\W? ?)(.+?)$/) {
183
184
        #my ($prefix, $issueNumber, $followupNumber, $issueDescription) = ($1, $2, $3, $4);
185
        #return ($prefix, $issueNumber, $followupNumber, $issueDescription);
186
        return ($1, $2, $3, $4);
187
    }
188
189
    Koha::Exceptions::Parse->throw(error => __PACKAGE__."->getGitCommitTitleElements($title):> Couldn't parse the given \$title");
190
}
191
192
1;
(-)a/Koha/AtomicUpdater.pm (+440 lines)
Line 0 Link Here
1
package Koha::AtomicUpdater;
2
3
# Copyright Open Source Freedom Fighters
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use Modern::Perl;
21
use Carp;
22
use Scalar::Util qw(blessed);
23
use Try::Tiny;
24
use Data::Format::Pretty::Console qw(format_pretty);
25
use Git;
26
use YAML::XS;
27
use File::Slurp;
28
29
use C4::Installer;
30
31
use Koha::Database;
32
use Koha::Cache;
33
use Koha::AtomicUpdate;
34
35
use base qw(Koha::Objects);
36
37
use Koha::Exceptions::File;
38
use Koha::Exceptions::Parse;
39
use Koha::Exceptions::BadParameter;
40
use Koha::Exceptions::DuplicateObject;
41
42
sub _type {
43
    return 'Atomicupdate';
44
}
45
sub object_class {
46
    return 'Koha::AtomicUpdate';
47
}
48
sub _get_castable_unique_columns {
49
    return ['atomicupdate_id'];
50
}
51
52
=head find
53
@OVERLOADS
54
    my $$atomicUpdate = $atomicUpdater->find($issue_id || $atomicupdate_id);
55
56
@PARAM1 Scalar, issue_id or atomicupdate_id
57
@RETURNS Koha::AtomicUpdate
58
@THROWS Koha::Exceptions::BadParameter, if @PARAM1 is not a scalar
59
        Koha::Exceptions::DuplicateObject, if @PARAM1 matches both the issue_id and atomicupdate_id,
60
                                          you should change your issue naming convention.
61
=cut
62
63
sub find {
64
    my ( $self, $id ) = @_;
65
    return unless $id;
66
    if (ref($id)) {
67
        return $self->SUPER::find($id);
68
    }
69
70
    my @results = $self->_resultset()->search({'-or' => [
71
                                            {issue_id => $id},
72
                                            {atomicupdate_id => $id}
73
                                        ]});
74
    return unless @results;
75
    if (scalar(@results > 1)) {
76
        my @cc1 = caller(1);
77
        my @cc0 = caller(0);
78
        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.");
79
    }
80
81
    my $object = $self->object_class()->_new_from_dbic( $results[0] );
82
    return $object;
83
}
84
85
my $updateOrderFilename = '_updateorder';
86
87
sub new {
88
    my ($class, $params) = @_;
89
90
    my $cache = Koha::Cache->new();
91
    my $self = $cache->get_from_cache('Koha::AtomicUpdater') || {};
92
    bless($self, $class);
93
94
    $self->{verbose} = $params->{verbose} || $self->{verbose} || 0;
95
    $self->{scriptDir} = $params->{scriptDir} || $self->{scriptDir} || C4::Context->config('intranetdir') . '/installer/data/mysql/atomicupdate/';
96
    $self->{confFile} = $params->{confFile} || $self->{confFile} || C4::Context->config('intranetdir') . '/installer/data/mysql/atomicupdate.conf';
97
    $self->{gitRepo} = $params->{gitRepo} || $self->{gitRepo} || $ENV{KOHA_PATH};
98
    $self->{dryRun} = $params->{dryRun} || $self->{dryRun} || 0;
99
100
    $self->_loadConfig();
101
    return $self;
102
}
103
104
=head getAtomicUpdates
105
106
    my $atomicUpdates = $atomicUpdater->getAtomicUpdates();
107
108
Gets all the AtomicUpdate-objects in the DB. This result should be Koha::Cached.
109
@RETURNS HASHRef of Koha::AtomicUpdate-objects, keyed with the issue_id
110
=cut
111
112
sub getAtomicUpdates {
113
    my ($self) = @_;
114
115
    my @au = $self->search({});
116
    my %au; #HASHify the AtomicUpdate-objects for easy searching.
117
    foreach my $au (@au) {
118
        $au{$au->issue_id} = $au;
119
    }
120
    return \%au;
121
}
122
123
sub addAtomicUpdate {
124
    my ($self, $params) = @_;
125
    print "Adding atomicupdate '".($params->{issue_id} || $params->{filename})."'\n" if $self->{verbose} > 2;
126
127
    my $atomicupdate = Koha::AtomicUpdate->new($params);
128
    $atomicupdate->store();
129
    $atomicupdate = $self->find($atomicupdate->issue_id);
130
    return $atomicupdate;
131
}
132
133
sub removeAtomicUpdate {
134
    my ($self, $issueId) = @_;
135
    print "Deleting atomicupdate '$issueId'\n" if $self->{verbose} > 2;
136
137
    my $atomicupdate = $self->find($issueId);
138
    if ($atomicupdate) {
139
        $atomicupdate->delete;
140
        print "Deleted atomicupdate '$issueId'\n" if $self->{verbose} > 2;
141
    }
142
    else {
143
        Koha::Exceptions::BadParameter->throw(error => __PACKAGE__."->removeIssueFromLog():> No such Issue '$issueId' stored to the atomicupdates-table");
144
    }
145
}
146
147
sub listToConsole {
148
    my ($self) = @_;
149
    my @stringBuilder;
150
151
    my @atomicupdates = $self->search({});
152
    foreach my $au (@atomicupdates) {
153
        push @stringBuilder, $au->unblessed();
154
    }
155
    return Data::Format::Pretty::Console::format_pretty(\@stringBuilder);
156
}
157
158
sub listPendingToConsole {
159
    my ($self) = @_;
160
    my @stringBuilder;
161
162
    my $atomicUpdates = $self->getPendingAtomicUpdates();
163
    foreach my $key (sort keys %$atomicUpdates) {
164
        my $au = $atomicUpdates->{$key};
165
        push @stringBuilder, $au->unblessed();
166
    }
167
    return Data::Format::Pretty::Console::format_pretty(\@stringBuilder);
168
}
169
170
sub getPendingAtomicUpdates {
171
    my ($self) = @_;
172
173
    my %pendingAtomicUpdates;
174
    my $atomicupdateFiles = $self->_getValidAtomicUpdateScripts();
175
    my $atomicUpdatesDeployed = $self->getAtomicUpdates();
176
    foreach my $key (keys(%$atomicupdateFiles)) {
177
        my $au = $atomicupdateFiles->{$key};
178
        my $parsedissueId =  $self->_parseIssueIds($au->issue_id);
179
        unless ($atomicUpdatesDeployed->{$au->issue_id} || $atomicUpdatesDeployed->{$parsedissueId}) {
180
            #This script hasn't been deployed.
181
            $pendingAtomicUpdates{$au->issue_id} = $au;
182
        }
183
    }
184
    return \%pendingAtomicUpdates;
185
}
186
187
=head applyAtomicUpdates
188
189
    my $atomicUpdater = Koha::AtomicUpdater->new();
190
    my $appliedAtomicupdates = $atomicUpdater->applyAtomicUpdates();
191
192
Checks the atomicupdates/-directory for any not-applied update scripts and
193
runs them in the order specified in the _updateorder-file in atomicupdate/-directory.
194
195
@RETURNS ARRAYRef of Koha::AtomicUpdate-objects deployed on this run
196
=cut
197
198
sub applyAtomicUpdates {
199
    my ($self) = @_;
200
201
    my %appliedUpdates;
202
203
    my $atomicUpdates = $self->getPendingAtomicUpdates();
204
    my $updateOrder = $self->getUpdateOrder();
205
    foreach my $issueId ( @$updateOrder ) {
206
        my $atomicUpdate = $atomicUpdates->{$issueId};
207
        next unless $atomicUpdate; #Not each ordered Git commit necessarily have a atomicupdate-script.
208
209
        $self->applyAtomicUpdate($atomicUpdate);
210
        $appliedUpdates{$issueId} = $atomicUpdate;
211
    }
212
213
    #Check that we have actually applied all the updates.
214
    my $stillPendingAtomicUpdates = $self->getPendingAtomicUpdates();
215
    if (scalar(%$stillPendingAtomicUpdates)) {
216
        my @issueIds = sort keys %$stillPendingAtomicUpdates;
217
        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";
218
    }
219
220
    return \%appliedUpdates;
221
}
222
223
sub applyAtomicUpdate {
224
    my ($self, $atomicUpdate) = @_;
225
    #Validate params
226
    unless ($atomicUpdate) {
227
        Koha::Exceptions::BadParameter->throw(error => __PACKAGE__."->applyAtomicUpdate($atomicUpdate):> Parameter must be a Koha::AtomicUpdate-object or a path to a valid atomicupdates-script!");
228
    }
229
    if ($atomicUpdate && ref($atomicUpdate) eq '') { #We have a scalar, presumably a filepath to atomicUpdate-script.
230
        $atomicUpdate = Koha::AtomicUpdate->new({filename => $atomicUpdate});
231
    }
232
233
    #$atomicUpdate = Koha::AtomicUpdater->cast($atomicUpdate);
234
235
    my $filename = $atomicUpdate->filename;
236
    print "Applying file '$filename'\n" if $self->{verbose} > 2;
237
238
    unless ($self->{dryRun}) {
239
        my $rv;
240
        if ( $filename =~ /\.sql$/ ) {
241
            my $installer = C4::Installer->new();
242
            $rv = $installer->load_sql( $self->{scriptDir}.'/'.$filename ) ? 0 : 1;
243
        } elsif ( $filename =~ /\.(perl|pl)$/ ) {
244
            my $fileAndPath = $self->{scriptDir}.'/'.$filename;
245
            $rv = do $fileAndPath;
246
            unless ($rv) {
247
                warn "couldn't parse $fileAndPath: $@\n" if $@;
248
                warn "couldn't do $fileAndPath: $!\n"    unless defined $rv;
249
                warn "couldn't run $fileAndPath\n"       unless $rv;
250
            }
251
        }
252
        print 'AtomicUpdate '.$atomicUpdate->filename." done.\n" if $self->{verbose} > 0;
253
        $atomicUpdate->store();
254
    }
255
256
    print "File '$filename' applied\n" if $self->{verbose} > 2;
257
}
258
259
=head _getValidAtomicUpdateScripts
260
261
@RETURNS HASHRef of Koha::AtomicUpdate-objects, of all the files
262
                in the atomicupdates/-directory that can be considered valid.
263
                Validity is currently conforming to the naming convention.
264
                Keys are the issue_id of atomicupdate-scripts
265
                Eg. {'Bug8584' => Koha::AtomicUpdate,
266
                     ...
267
                    }
268
=cut
269
270
sub _getValidAtomicUpdateScripts {
271
    my ($self) = @_;
272
273
    my %atomicUpdates;
274
    opendir( my $dirh, $self->{scriptDir} );
275
    foreach my $file ( sort readdir $dirh ) {
276
        print "Looking at file '$file'\n" if $self->{verbose} > 2;
277
278
        my $atomicUpdate;
279
        try {
280
            $atomicUpdate = Koha::AtomicUpdate->new({filename => $file});
281
        } catch {
282
            if (blessed($_)) {
283
                if ($_->isa('Koha::Exceptions::File') || $_->isa('Koha::Exceptions::Parse')) {
284
                    print "File-error for file '$file': ".$_->error()." \n" if $self->{verbose} > 2;
285
                    #We can ignore filename validation issues, since the directory has
286
                    #loads of other types of files as well. Like README . ..
287
                }
288
                else {
289
                    $_->rethrow();
290
                }
291
            }
292
            else {
293
                die $_; #Rethrow the unknown Exception
294
            }
295
        };
296
        next unless $atomicUpdate;
297
298
        $atomicUpdates{$atomicUpdate->issue_id} = $atomicUpdate;
299
    }
300
    return \%atomicUpdates;
301
}
302
303
=head getUpdateOrder
304
305
    $atomicUpdater->getUpdateOrder();
306
307
@RETURNS ARRAYRef of Strings, IssueIds ordered from the earliest to the newest.
308
=cut
309
310
sub getUpdateOrder {
311
    my ($self) = @_;
312
313
    my $updateOrderFilepath = $self->{scriptDir}."/$updateOrderFilename";
314
    open(my $FH, "<:encoding(UTF-8)", $updateOrderFilepath) or die "Koha::AtomicUpdater->_saveAsUpdateOrder():> Couldn't open the updateOrderFile for reading\n$!\n";
315
    my @updateOrder = map {chomp($_); $_;} <$FH>;
316
    close $FH;
317
    return \@updateOrder;
318
}
319
320
=head
321
322
    my $issueIdOrder = Koha::AtomicUpdater->buildUpdateOrderFromGit(10000);
323
324
Creates a update order file '_updateorder' for atomicupdates to know which updates come before which.
325
This is a simple way to make sure the atomicupdates are applied in the correct order.
326
The update order file is by default in your $KOHA_PATH/installer/data/mysql/atomicupdate/_updateorder
327
328
This requires a Git repository to be in the $ENV{KOHA_PATH} to be effective.
329
330
@PARAM1 Integer, How many Git commits to include to the update order file,
331
                 10000 is a good default.
332
@RETURNS ARRAYRef of Strings, The update order of atomicupdates from oldest to newest.
333
=cut
334
335
sub buildUpdateOrderFromGit {
336
    my ($self, $gitCommitsCount) = @_;
337
338
    my %orderedCommits; #Store the commits we have ordered here, so we don't reorder any followups.
339
    my @orderedCommits;
340
341
    my $i = 0; #Index of array where we push issue_ids
342
    my $commits = $self->_getGitCommits($gitCommitsCount);
343
    foreach my $commit (reverse @$commits) {
344
345
        my ($commitHash, $commitTitle) = $self->_parseGitOneliner($commit);
346
        unless ($commitHash && $commitTitle) {
347
            next();
348
        }
349
350
        my $issueId;
351
        try {
352
            $issueId = Koha::AtomicUpdate::getIssueIdentifier(undef, $commitTitle);
353
        } catch {
354
            if (blessed($_)) {
355
                if($_->isa('Koha::Exceptions::Parse')) {
356
                    #Silently ignore parsing errors
357
                    print "Koha::AtomicUpdater->buildUpdateOrderFromGit():> Couldn't parse issue_id from Git commit title '$commitTitle'.\n"
358
                                    if $self->{verbose} > 1;
359
                }
360
                else {
361
                    $_->rethrow();
362
                }
363
            }
364
            else {
365
                die $_;
366
            }
367
        };
368
        next unless $issueId;
369
370
        if ($orderedCommits{ $issueId }) {
371
            next();
372
        }
373
        else {
374
            $orderedCommits{ $issueId } = $issueId;
375
            $orderedCommits[$i] = $issueId;
376
            $i++;
377
        }
378
    }
379
380
    $self->_saveAsUpdateOrder(\@orderedCommits);
381
    return \@orderedCommits;
382
}
383
384
sub _parseIssueIds {
385
    my ($self, $issueId) = @_;
386
387
    my @keys = split /(-)/, $issueId;
388
    delete $keys[1];
389
    @keys = grep defined, @keys;
390
391
    return join('', @keys);
392
}
393
394
sub _getGitCommits {
395
    my ($self, $count) = @_;
396
    my $repo = Git->repository(Directory => $self->{gitRepo});
397
398
    #We can read and print 10000 git commits in less than three seconds :) good Git!
399
    my @commits = $repo->command('show', '--pretty=oneline', '--no-patch', '-'.$count);
400
    return \@commits;
401
}
402
403
sub _parseGitOneliner {
404
    my ($self, $gitLiner) = @_;
405
406
    my ($commitHash, $commitTitle) = ($1, $2) if $gitLiner =~ /^(\w{40}) (.+)$/;
407
    unless ($commitHash && $commitTitle) {
408
        print "Koha::AtomicUpdater->parseGitOneliner():> Couldn't parse Git commit '$gitLiner' to hash and title.\n"
409
                        if $self->{verbose} > 1;
410
        return();
411
    }
412
    return ($commitHash, $commitTitle);
413
}
414
415
sub _saveAsUpdateOrder {
416
    my ($self, $orderedUpdates) = @_;
417
418
    my $updateOrderFilepath = $self->{scriptDir}."/$updateOrderFilename";
419
    my $text = join("\n", @$orderedUpdates);
420
    open(my $FH, ">:encoding(UTF-8)", $updateOrderFilepath) or die "Koha::AtomicUpdater->_saveAsUpdateOrder():> Couldn't open the updateOrderFile for writing\n$!\n";
421
    print $FH $text;
422
    close $FH;
423
}
424
425
=head %config
426
Package static variable to the configurations Hash.
427
=cut
428
429
my $config;
430
431
sub _loadConfig {
432
    my ($self) = @_;
433
434
    if (-e $self->{confFile}) {
435
        my $yaml = File::Slurp::read_file( $self->{confFile}, { binmode => ':utf8' } ) ;
436
        $config = YAML::XS::Load($yaml);
437
    }
438
}
439
440
1;
(-)a/Koha/Schema/Result/Atomicupdate.pm (+115 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::Atomicupdate;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::Atomicupdate
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<atomicupdates>
19
20
=cut
21
22
__PACKAGE__->table("atomicupdates");
23
24
=head1 ACCESSORS
25
26
=head2 atomicupdate_id
27
28
  data_type: 'integer'
29
  extra: {unsigned => 1}
30
  is_auto_increment: 1
31
  is_nullable: 0
32
33
=head2 issue_id
34
35
  data_type: 'varchar'
36
  is_nullable: 0
37
  size: 20
38
39
=head2 filename
40
41
  data_type: 'varchar'
42
  is_nullable: 0
43
  size: 128
44
45
=head2 modification_time
46
47
  data_type: 'timestamp'
48
  datetime_undef_if_invalid: 1
49
  default_value: current_timestamp
50
  is_nullable: 0
51
52
=cut
53
54
__PACKAGE__->add_columns(
55
  "atomicupdate_id",
56
  {
57
    data_type => "integer",
58
    extra => { unsigned => 1 },
59
    is_auto_increment => 1,
60
    is_nullable => 0,
61
  },
62
  "issue_id",
63
  { data_type => "varchar", is_nullable => 0, size => 20 },
64
  "filename",
65
  { data_type => "varchar", is_nullable => 0, size => 128 },
66
  "modification_time",
67
  {
68
    data_type => "timestamp",
69
    datetime_undef_if_invalid => 1,
70
    default_value => \"current_timestamp",
71
    is_nullable => 0,
72
  },
73
);
74
75
=head1 PRIMARY KEY
76
77
=over 4
78
79
=item * L</atomicupdate_id>
80
81
=back
82
83
=cut
84
85
__PACKAGE__->set_primary_key("atomicupdate_id");
86
87
=head1 UNIQUE CONSTRAINTS
88
89
=head2 C<atomic_issue_id>
90
91
=over 4
92
93
=item * L</issue_id>
94
95
=back
96
97
=cut
98
99
__PACKAGE__->add_unique_constraint("atomic_issue_id", ["issue_id"]);
100
101
102
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2020-03-23 14:13:35
103
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:iZ8fpCOXTPc1a2v86F6zRw
104
105
106
# You can replace this text with custom code or comments, and it will be preserved on regeneration
107
108
sub koha_objects_class {
109
    'Koha::AtomicUpdater';
110
}
111
sub koha_object_class {
112
    'Koha::AtomicUpdate';
113
}
114
115
1;
(-)a/cpanfile (+3 lines)
Lines 26-31 requires 'DBIx::RunSQL', '0.14'; Link Here
26
requires 'Data::Dumper', '2.121';
26
requires 'Data::Dumper', '2.121';
27
requires 'Data::ICal', '0.13';
27
requires 'Data::ICal', '0.13';
28
requires 'Date::Calc', '5.4';
28
requires 'Date::Calc', '5.4';
29
requires 'Data::Format::Pretty::Console', '0.34';
29
requires 'Date::Manip', '5.44';
30
requires 'Date::Manip', '5.44';
30
requires 'DateTime', '0.58';
31
requires 'DateTime', '0.58';
31
requires 'DateTime::Event::ICal', '0.08';
32
requires 'DateTime::Event::ICal', '0.08';
Lines 39-50 requires 'Email::Date', '1.103'; Link Here
39
requires 'Email::MessageID', '1.406';
40
requires 'Email::MessageID', '1.406';
40
requires 'Email::Valid', '0.190';
41
requires 'Email::Valid', '0.190';
41
requires 'Exception::Class', '1.38';
42
requires 'Exception::Class', '1.38';
43
requires 'File::Fu::File', '1';
42
requires 'File::Slurp', '9999.13';
44
requires 'File::Slurp', '9999.13';
43
requires 'Font::TTF', '0.45';
45
requires 'Font::TTF', '0.45';
44
requires 'GD', '2.39';
46
requires 'GD', '2.39';
45
requires 'GD::Barcode::UPCE', '1.1';
47
requires 'GD::Barcode::UPCE', '1.1';
46
requires 'Getopt::Long', '2.35';
48
requires 'Getopt::Long', '2.35';
47
requires 'Getopt::Std', '1.05';
49
requires 'Getopt::Std', '1.05';
50
requires 'Git', '0.41';
48
requires 'HTML::Entities', '3.69';
51
requires 'HTML::Entities', '3.69';
49
requires 'HTML::FormatText', '1.23';
52
requires 'HTML::FormatText', '1.23';
50
requires 'HTML::Scrubber', '0.08';
53
requires 'HTML::Scrubber', '0.08';
(-)a/installer/data/mysql/atomicupdate.pl (+208 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
#
3
# Copyright Vaara-kirjastot 2015
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
#
20
21
use Modern::Perl;
22
use Getopt::Long qw(:config no_ignore_case);
23
24
use C4::Context;
25
26
use Koha::AtomicUpdater;
27
28
my $verbose = 0;
29
my $help = 0;
30
my $apply = 0;
31
my $remove = '';
32
my $dryRun = 0;
33
my $insert = '';
34
my $list = 0;
35
my $pending = 0;
36
my $directory = '';
37
my $git = '';
38
my $single = '';
39
my $configurationFile = '';
40
41
GetOptions(
42
    'v|verbose:i'       => \$verbose,
43
    'h|help'            => \$help,
44
    'a|apply'           => \$apply,
45
    'D|dry-run'         => \$dryRun,
46
    'd|directory:s'     => \$directory,
47
    'r|remove:s'        => \$remove,
48
    'i|insert:s'        => \$insert,
49
    'l|list'            => \$list,
50
    'p|pending'         => \$pending,
51
    'g|git:s'           => \$git,
52
    's|single:s'        => \$single,
53
    'c|config:s'        => \$configurationFile,
54
);
55
56
my $usage = << 'ENDUSAGE';
57
58
Runs all the not-yet-applied atomicupdate-scripts and sql in the
59
atomicupdates-directory, in the order specified by the _updateorder-file.
60
61
This script uses koha.atomicupdates-table to see if the update has already been
62
applied.
63
64
Also acts as a gateway to CRUD the koha.atomicupdates-table.
65
66
Naming conventions for atomicupdate-scripts:
67
--------------------------------------------
68
All atomicupdate-scripts must follow this naming convention
69
<prefix><separator><issue_number><separator><followup_number><separator><issueDescription><file_type>
70
eg.
71
"Bug-1234-ThreeLittleMusketeers.pl"
72
"Bug:1234-1-ThreeLittleMusketeersFollowup1.pl"
73
"Bug 1234-2-ThreeLittleMusketeersFollowup2.pl"
74
"Bug-1235-FeaturelessFeature.sql"
75
"bug_7534.perl"
76
See --config for allowed prefix values.
77
78
79
    -v --verbose        Integer, 1 is not so verbose, 3 is maximally verbose.
80
81
    -D --dry-run        Flag, Run the script but don't execute any atomicupdates.
82
                        You should use --verbose 3 to see what is happening.
83
84
    -h --help           Flag, This nice help!
85
86
    -a --apply          Flag, Apply all the pending atomicupdates from the
87
                        atomicupdates-directory.
88
89
    -d --directory      Path, From which directory to look for atomicupdate-scripts.
90
                        Defaults to '$KOHA_PATH/installer/data/mysql/atomicupdate/'
91
92
    -s --single         Path, execute a single atomicupdate-script.
93
                        eg. atomicupdate/Bug01243-SingleFeature.pl
94
95
    -r --remove         String, Remove the upgrade entry from koha.atomicupdates
96
                        eg. --remove "Bug71337"
97
98
    -i --insert         Path, Add an upgrade log entry for the given atomicupdate-file.
99
                        Useful to revert an accidental --remove -operation or for
100
                        testing. Does not execute the update script, simply adds
101
                        the log entry.
102
                        eg. -i installer/data/mysql/atomicupdate/Bug5453-Example.pl
103
104
    -l --list           Flag, List all entries in the koha.atomicupdates-table.
105
                        This typically means all applied atomicupdates.
106
107
    -p --pending        Flag, List all pending atomicupdates from the
108
                        atomicupdates-directory.
109
110
    -g --git            Path, Build the update order from the Git repository given,
111
                        or default to the Git repository in $KOHA_PATH.
112
                        Eg. --git 1, to build with default values, or
113
                            --git /tmp/kohaclone/ to look for another repository
114
115
    -c --config         The configuration file to load. Defaults to
116
                        '$KOHA_PATH/installer/data/mysql/atomicupdate.conf'
117
118
                        The configuration file is an YAML-file, and must have the
119
                        following definitions:
120
121
                        "Defines the prefixes used to identify the unique issue
122
                         identifier. You can give a normalizer function to the
123
                         identifier prefix."
124
                        example:
125
                        allowedIssueIdentifierPrefixes:
126
                           Bug:
127
                              ucfirst
128
                           "#":
129
                              normal
130
                           KD:
131
                              normal
132
133
134
EXAMPLES:
135
136
    atomicupdate.pl -g 1 -a
137
138
Looks for the Git repository in $KOHA_PATH, parses the issue/commit identifiers
139
from the top 10000 commits and generates the _updateorder-file to tell in which
140
order the atomicupdates-scripts are executed.
141
Then applies all pending atomicupdate-scripts in the order (oldest to newest)
142
presented in the Git repository.
143
144
145
    atomicupdate --apply -d /home/koha/kohaclone/installer/data/mysql/atomicupdate/
146
147
Applies all pending atomicupdate-scripts from the given directory. If the file
148
'_updateorder' is not present, it must be first generated, for example with the
149
--git 1 argument.
150
151
UPDATEORDER:
152
153
When deploying more than one atomicupdate, it is imperative to know in which order
154
the updates are applied. Atomicupdates can easily depend on each other and fail in
155
very strange and hard-to-debug -ways if the prerequisite modifications are not
156
in effect.
157
The correct update order is defined in the atomicupdates/_updateorder-file. This is
158
a simple list of issue/commit identifiers, eg.
159
160
    Bug5454
161
    Bug12432
162
    Bug12432-1
163
    Bug12432-2
164
    Bug3218
165
    #45
166
167
This file is most easily generated directly from the original Git repository, since
168
the order in which the Commits have been introduced most definetely is the order
169
they should be applied.
170
When deploying the atomicupdates to production environments without the
171
Git repository, the _updateorder file must be copied along the atomicupdate-scripts.
172
173
P.S. Remember to put atomicupdate/_updateorder to your .gitignore
174
175
ENDUSAGE
176
177
if ( $help ) {
178
    print $usage;
179
    exit;
180
}
181
182
my $atomicupdater = Koha::AtomicUpdater->new({verbose => $verbose,
183
                                              scriptDir => $directory,
184
                                              gitRepo => (length($git) == 1) ? '' : $git,
185
                                              dryRun => $dryRun,}
186
                                            ,);
187
188
if ($git) {
189
    $atomicupdater->buildUpdateOrderFromGit(10000);
190
}
191
if ($remove) {
192
    $atomicupdater->removeAtomicUpdate($remove);
193
}
194
if ($insert) {
195
    $atomicupdater->addAtomicUpdate({filename => $insert});
196
}
197
if ($list) {
198
    print $atomicupdater->listToConsole();
199
}
200
if ($pending) {
201
    print $atomicupdater->listPendingToConsole();
202
}
203
if ($apply) {
204
    $atomicupdater->applyAtomicUpdates();
205
}
206
if ($single) {
207
    $atomicupdater->applyAtomicUpdate($single);
208
}
(-)a/installer/data/mysql/atomicupdate/Bug-14698-AtomicUpdater.pl (+36 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright Open Source Freedom Fighters
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, write to the Free Software Foundation, Inc.,
18
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20
use C4::Context;
21
22
my $dbh = C4::Context->dbh();
23
24
$dbh->do("
25
CREATE TABLE `atomicupdates` (
26
  `atomicupdate_id` int(11) unsigned NOT NULL auto_increment,
27
  `issue_id` varchar(20) NOT NULL,
28
  `filename` varchar(30) NOT NULL,
29
  `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
30
  PRIMARY KEY  (`atomicupdate_id`),
31
  UNIQUE KEY `origincode` (`issue_id`)
32
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
33
");
34
$dbh->do("INSERT INTO atomicupdates (issue_id, filename) VALUES ('Bug14698', 'Bug14698-AtomicUpdater.pl')");
35
36
print "Upgrade to Bug 14698 - AtomicUpdater - Keeps track of which updates have been applied to a database done\n";
(-)a/installer/data/mysql/atomicupdate/bug_14698-AtomicUpdater.perl (+20 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    # you can use $dbh here like:
4
    $dbh->do("
5
        CREATE TABLE `atomicupdates` (
6
        `atomicupdate_id` int(11) unsigned NOT NULL auto_increment,
7
        `issue_id` varchar(20) NOT NULL,
8
        `filename` varchar(128) NOT NULL,
9
        `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
10
        PRIMARY KEY  (`atomicupdate_id`),
11
        UNIQUE KEY `atomic_issue_id` (`issue_id`)
12
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
13
    ");
14
15
    $dbh->do("INSERT INTO atomicupdates (issue_id, filename) VALUES ('Bug-14698', 'bug_14698-AtomicUpdater.perl')");
16
17
    # Always end with this (adjust the bug info)
18
    SetVersion( $DBversion );
19
    print "Upgrade to $DBversion done (Bug 14698 - AtomicUpdater - Keeps track of which updates have been applied to a database done)\n";
20
}
(-)a/installer/data/mysql/kohastructure.sql (+14 lines)
Lines 15-20 Link Here
15
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
15
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
16
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
16
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
17
17
18
--
19
-- Table structure for table `atomicupdates`
20
--
21
22
DROP TABLE IF EXISTS `atomicupdates`;
23
CREATE TABLE `atomicupdates` (
24
  `atomicupdate_id` int(11) unsigned NOT NULL auto_increment,
25
  `issue_id` varchar(20) NOT NULL,
26
  `filename` varchar(128) NOT NULL,
27
  `modification_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
28
  PRIMARY KEY  (`atomicupdate_id`),
29
  UNIQUE KEY `atomic_issue_id` (`issue_id`)
30
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
31
18
--
32
--
19
-- Table structure for table `auth_header`
33
-- Table structure for table `auth_header`
20
--
34
--
(-)a/t/AtomicUpdater.t (+134 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
use Try::Tiny;
20
use Scalar::Util qw(blessed);
21
22
use Test::More;
23
24
use Koha::AtomicUpdate;
25
26
plan tests => 3;
27
28
use_ok('Koha::AtomicUpdate');
29
30
subtest "Check allowed atomicupdate file naming conventions" => sub {
31
32
    plan tests => 6;
33
34
    my ();
35
36
    my @goodTests = (
37
        [ 'Bug.1234-2:Trollollollol.perl',
38
            'Bug', '1234', '2',   'Trollollollol',   'perl', ],
39
        [ 'KD-257-Mahtava_patchi.sql',
40
            'KD',  '257',  undef, 'Mahtava_patchi',  'sql', ],
41
        [ 'bug_666_lazy_Koha_man_cant_type_proper_atomicupdate_filename.sql',
42
            'bug', '666',  undef, 'lazy_Koha_man_cant_type_proper_atomicupdate_filename', 'sql', ],
43
        [ 'G-8-Banksters.pl',
44
            'G',   '8',    undef, 'Banksters',       'pl', ],
45
        [ 'B-12-6:Is_important.cpp',
46
            'B',   '12',   '6',   'Is_important',    'cpp', ],
47
    );
48
49
    my @forbiddenCharacters = (
50
        [ '#:445-HashTagForbidden', 'Koha::Exceptions::File' ],
51
    );
52
53
    foreach my $t (@goodTests) {
54
        testName('fileName', @$t);
55
    }
56
57
    foreach my $t (@forbiddenCharacters) {
58
        my ($fileName, $exception) = @$t;
59
        try {
60
            Koha::AtomicUpdate::getFileNameElements($fileName);
61
            ok(0, "$fileName should have crashed with $exception");
62
        } catch {
63
            is(ref($_), $exception, "$fileName crashed with $exception");
64
        };
65
    }
66
67
};
68
69
70
71
subtest "Check allowed atomicupdate Git title naming conventions" => sub {
72
73
    plan tests => 7;
74
75
    my ();
76
77
    my @goodTests = (
78
        [ 'Bug 1234-2 : Trollollollol',
79
            'Bug', '1234', '2',   'Trollollollol', ],
80
        [ 'KD-257-Mahtava_patchi.sql',
81
            'KD',  '257',  undef, 'Mahtava_patchi.sql', ],
82
        [ 'G - 8 - Banksters:Hip.Top',
83
            'G',   '8',    undef, 'Banksters:Hip.Top', ],
84
        [ 'B -12- 6:Is_important.like.no.other',
85
            'B',   '12',   undef, '6:Is_important.like.no.other', ],
86
        [ 'HSH-12412-1: Remove any # of characters',
87
            'HSH', '12412', 1,   'Remove any # of characters', ],
88
    );
89
90
    my @forbiddenCharacters = (
91
        [ '#:445-HashTagForbidden', 'Koha::Exceptions::File' ],
92
        [ 'bug_666_lazy_Koha_man_cant_type_proper_atomicupdate_filename', 'Koha::Exceptions::Parse', ],
93
    );
94
95
    foreach my $t (@goodTests) {
96
        testName('git', @$t);
97
    }
98
99
    foreach my $t (@forbiddenCharacters) {
100
        my ($title, $exception) = @$t;
101
        try {
102
            Koha::AtomicUpdate::getGitCommitTitleElements($title);
103
            ok(0, "$title should have crashed with $exception");
104
        } catch {
105
            is(ref($_), $exception, "$title crashed with $exception");
106
        };
107
    }
108
109
};
110
111
###################
112
##  TEST HELPERS ##
113
114
sub testName {
115
    my ($type, $nameTitle, $e_prefix, $e_issueNumber, $e_followupNumber, $e_issueDescription, $e_fileType) = @_;
116
117
    subtest "testName($nameTitle)" => sub {
118
119
    my ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType);
120
    ($prefix, $issueNumber, $followupNumber, $issueDescription, $fileType) =
121
            Koha::AtomicUpdate::getFileNameElements($nameTitle)
122
                if $type eq 'fileName';
123
    ($prefix, $issueNumber, $followupNumber, $issueDescription) =
124
            Koha::AtomicUpdate::getGitCommitTitleElements($nameTitle)
125
                if $type eq 'git';
126
127
    is($prefix, $e_prefix, 'prefix');
128
    is($issueNumber, $e_issueNumber, 'issue number');
129
    is($followupNumber, $e_followupNumber, 'followup number');
130
    is($issueDescription, $e_issueDescription, 'issue description');
131
    is($fileType, $e_fileType, 'file type') if $type eq 'fileName';
132
133
    };
134
}
(-)a/t/db_dependent/Koha/AtomicUpdater.t (-1 / +287 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2015 Open Source Freedom Fighters
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use Test::More;
22
use Try::Tiny;
23
use Encode;
24
use File::Spec;
25
use File::Path;
26
use File::Fu::File;
27
28
use Koha::Database;
29
use Koha::AtomicUpdater;
30
31
use t::lib::TestBuilder;
32
33
plan tests => 8;
34
35
my $schema  = Koha::Database->new->schema;
36
$schema->storage->txn_begin;
37
38
my $builder = t::lib::TestBuilder->new;
39
40
#Create the _updateorder-file to a temp directory.
41
my $test_file = create_file({
42
   filepath => 'atomicupdate/',
43
   filename => '_updateorder',
44
   content  => ''
45
});
46
47
use_ok('Koha::AtomicUpdater');
48
49
my $atomicupdate1 = Koha::AtomicUpdate->new({filename => 'Bug-12-WatchExMachinaYoullLikeIt.pl'})->store;
50
my $atomicupdate2 = Koha::AtomicUpdate->new({filename => 'Bug-14-ReturnOfZorro.perl'})->store;
51
my $atomicupdate3 = Koha::AtomicUpdate->new({filename => 'KD-14-RobotronInDanger.sql'})->store;
52
my $atomicupdate4 = Koha::AtomicUpdate->new({filename => 'KD-15-ILikedPrometheusButAlienWasBetter.pl'})->store;
53
54
#Make sure we get the correct update order, otherwise we get unpredictable results.
55
{ #Overload existing subroutines to provide a Mock implementation
56
   no warnings 'redefine';
57
   package Koha::AtomicUpdater;
58
   sub _getGitCommits { #instead of requiring a Git repository, we just mock the input.
59
      return [#Newest commit
60
               '2e8a39762b506738195f21c8ff67e4e7bfe6dbba Bug_01243-SingleUpdate',
61
               '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab KD:55 : Fiftyfive',
62
               '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab KD-54 - KohaCon in Finland next year',
63
               'b447b595acacb0c4823582acf9d8a08902118e59 KD-53 - Place to be.pl',
64
               '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab bug 112 - Lapinlahden linnut',
65
               '5ac7101d4071fe11f7a5d1445bb97ed1a603a9b5 Bug:911 - What are you going to do?',
66
               '1d54601b9cac0bd75ee97e071cf52ed49daef8bd KD-911 - Who are you going to call',
67
               '1d54601b9cac0bd75ee97e071cf52ed49daef8bd bug 30 - Feature Yes yes',
68
               '5ac7101d4071fe11f7a5d1445bb97ed1a603a9b5 KD-29 - Bug squashable',
69
               '2e8a39762b506738195f21c8ff67e4e7bfe6d7ab Bug : 28 - Feature Squash',
70
               'b447b595acacb0c4823582acf9d8a08902118e59 BUG 27 - Bug help',
71
               #Oldest commit
72
               ];
73
   }
74
}
75
76
subtest "Followup naming convention" => sub {
77
78
   plan tests => 1;
79
80
   my $au = Koha::AtomicUpdate->new({filename => "Bug-535455-1-TestingFollowups.pl"});
81
   is($au->issue_id, "Bug-535455-1", "Followup Bug-535455-1 recognized");
82
83
};
84
85
86
subtest "Create update order from Git repository" => sub {
87
   plan tests => 8;
88
89
   #Instantiate the AtomicUpdater to operate on a temp directory.
90
   my $atomicUpdater = Koha::AtomicUpdater->new({
91
      scriptDir => $test_file->dirname(),
92
   });
93
94
   #Start real testing.
95
   my $issueIds = $atomicUpdater->buildUpdateOrderFromGit(4);
96
   is($issueIds->[0], 'Bug-27', "First atomicupdate to deploy");
97
   is($issueIds->[1], 'Bug-28', "Second atomicupdate to deploy");
98
   is($issueIds->[2], 'KD-29', "Third atomicupdate to deploy");
99
   is($issueIds->[3], 'Bug-30', "Last atomicupdate to deploy");
100
101
   #Testing file access
102
   $issueIds = $atomicUpdater->getUpdateOrder();
103
   is($issueIds->[0], 'Bug-27', "First atomicupdate to deploy, from _updateorder");
104
   is($issueIds->[1], 'Bug-28', "Second atomicupdate to deploy, from _updateorder");
105
   is($issueIds->[2], 'KD-29', "Third atomicupdate to deploy, from _updateorder");
106
   is($issueIds->[3], 'Bug-30', "Last atomicupdate to deploy, from _updateorder");
107
108
};
109
110
subtest "List all deployed atomicupdates" => sub {
111
112
   plan tests => 4;
113
114
   my $atomicUpdater = Koha::AtomicUpdater->new();
115
   my $text = $atomicUpdater->listToConsole();
116
   print $text;
117
118
   ok($text =~ m/Bug-12-WatchExMachinaYoullLik/, "Bug12-WatchExMachinaYoullLikeIt");
119
   ok($text =~ m/Bug-14-ReturnOfZorro.perl/, "Bug14-ReturnOfZorro");
120
   ok($text =~ m/KD-14-RobotronInDanger.sql/, "KD-14-RobotronInDanger");
121
   ok($text =~ m/KD-15-ILikedPrometheusButAli/, "KD-15-ILikedPrometheusButAlienWasBetter");
122
123
};
124
125
subtest "Delete an atomicupdate entry" => sub {
126
127
   plan tests => 2;
128
129
   my $atomicUpdater = Koha::AtomicUpdater->new();
130
   my $atomicupdates = $atomicUpdater->search();
131
   ok($atomicupdate1->id, "AtomicUpdate '".$atomicupdate1->issue_id."' exists prior to deletion");
132
133
   $atomicUpdater->removeAtomicUpdate($atomicupdate1->issue_id);
134
   my $atomicupdate = $atomicUpdater->find($atomicupdate1->id);
135
   ok(not($atomicupdate), "AtomicUpdate '".$atomicupdate1->issue_id."' deleted");
136
137
};
138
139
subtest "Insert an atomicupdate entry" => sub {
140
141
   plan tests => 2;
142
143
   my $atomicUpdater = Koha::AtomicUpdater->new();
144
   $atomicUpdater->addAtomicUpdate({filename => "Bug-15-Inserted.pl"});
145
146
   my $atomicupdate = $atomicUpdater->find('Bug-15');
147
   ok($atomicupdate, "Bug-15-Inserted.pl inserted");
148
149
   $atomicUpdater->removeAtomicUpdate($atomicupdate->issue_id);
150
   $atomicupdate = $atomicUpdater->find('Bug-15');
151
   ok(not($atomicupdate), "Bug-15-Inserted.pl deleted");
152
153
};
154
155
subtest "List pending atomicupdates" => sub {
156
157
   plan tests => 13;
158
159
   ##Test adding update scripts and deploy them, confirm that no pending scripts detected
160
   my $test_file1 = create_file({
161
      filepath => 'atomicupdate/',
162
      filename => 'KD-911-WhoYouGonnaCall.pl',
163
      content  => '$ENV{ATOMICUPDATE_TESTS} = 1;',
164
   });
165
166
   my $test_file2 =create_file({
167
      filepath => 'atomicupdate/',
168
      filename => 'Bug-911-WhatchaGonnaDo.pl',
169
      content  => '$ENV{ATOMICUPDATE_TESTS}++;',
170
   });
171
172
   my $test_file3 = create_file({
173
      filepath => 'atomicupdate/',
174
      filename => 'Bug-112-LapinlahdenLinnut.pl',
175
      content  => '$ENV{ATOMICUPDATE_TESTS}++;',
176
   });
177
178
   my $atomicUpdater = Koha::AtomicUpdater->new({
179
      scriptDir => $test_file->dirname()
180
   });
181
182
   my $text = $atomicUpdater->listPendingToConsole();
183
   ok($text =~ m/KD-911-WhoYouGonnaCall.pl/, "KD-911-WhoYouGonnaCall is pending");
184
   ok($text =~ m/Bug-911-WhatchaGonnaDo.pl/, "Bug-911-WhatchaGonnaDo is pending");
185
   ok($text =~ m/Bug-112-LapinlahdenLinnut.pl/, 'Bug-112-LapinlahdenLinnut is pending');
186
187
   my $atomicupdates = $atomicUpdater->applyAtomicUpdates();
188
189
   is($atomicupdates->{'KD-911'}->issue_id, 'KD-911', "KD-911-WhoYouGonnaCall.pl deployed");
190
   is($atomicupdates->{'Bug-112'}->issue_id, 'Bug-112', 'Bug-112-LapinlahdenLinnut.pl deployed');
191
   is($atomicupdates->{'Bug-911'}->issue_id, 'Bug-911', "Bug-911-WhatchaGonnaDo.pl deployed");
192
193
   ##Test adding scripts to the atomicupdates directory and how we deal with such change.
194
   my $test_file4 = create_file({
195
      filepath => 'atomicupdate/',
196
      filename => 'KD-53-PlaceToBe.pl',
197
      content  => '$ENV{ATOMICUPDATE_TESTS}++;',
198
   });
199
200
   my $test_file5 = create_file({
201
      filepath => 'atomicupdate/',
202
      filename => 'KD-54-KohaConInFinlandNextYear.pl',
203
      content  => '$ENV{ATOMICUPDATE_TESTS}++;',
204
   });
205
206
   my $test_file6 = create_file({
207
      filepath => 'atomicupdate/',
208
      filename => 'KD-55-Fiftyfive.pl',
209
      content  => '$ENV{ATOMICUPDATE_TESTS}++;',
210
   });
211
212
   $text = $atomicUpdater->listPendingToConsole();
213
   print $text;
214
215
   ok($text =~ m/KD-53-PlaceToBe.pl/, "KD-53-PlaceToBe.pl is pending");
216
   ok($text =~ m/KD-54-KohaConInFinlandNextYear.pl/, "KD-54-KohaConInFinlandNextYear.pl is pending");
217
   ok($text =~ m/KD-55-Fiftyfive.pl/u, 'KD-55-Fiftyfive.pl is pending');
218
219
   $atomicupdates = $atomicUpdater->applyAtomicUpdates();
220
221
   is($atomicupdates->{'KD-53'}->issue_id, 'KD-53', "KD-53-PlaceToBe.pl deployed");
222
   is($atomicupdates->{'KD-54'}->issue_id, 'KD-54', 'KD-54-KohaConInFinlandNextYear.pl deployed');
223
   is($atomicupdates->{'KD-55'}->issue_id, 'KD-55', "KD-55-Fiftyfive.pl deployed");
224
225
   is($ENV{ATOMICUPDATE_TESTS}, 6, "All configured AtomicUpdates deployed");
226
227
   $test_file1->remove;
228
   $test_file2->remove;
229
   $test_file3->remove;
230
   $test_file4->remove;
231
   $test_file5->remove;
232
   $test_file6->remove;
233
234
};
235
236
237
subtest "Apply single atomicupdate from file" => sub {
238
239
   plan tests => 4;
240
241
   my $test_file = create_file({
242
      filepath => 'atomicupdate/',
243
      filename => 'Bug_01243-SingleUpdate.pl',
244
      content  => '$ENV{ATOMICUPDATE_TESTS_2} = 10;',
245
   });
246
247
   ###  Try first as a dry-run  ###
248
   my $atomicUpdater = Koha::AtomicUpdater->new({
249
      scriptDir => $test_file->dirname(),
250
      dryRun => 1,
251
   });
252
253
   $atomicUpdater->applyAtomicUpdate($test_file->stringify);
254
   my $atomicUpdate = $atomicUpdater->find('Bug-01243');
255
256
   ok(not($atomicUpdate), "--dry-run doesn't add anything");
257
   is($ENV{ATOMICUPDATE_TESTS_2}, undef, "--dry-run doesn't execute anything");
258
259
   ###  Make a change!  ###
260
   $atomicUpdater = Koha::AtomicUpdater->new({
261
      scriptDir => $test_file->dirname(),
262
   });
263
264
   $atomicUpdater->applyAtomicUpdate($test_file->stringify);
265
   $atomicUpdate = $atomicUpdater->find('Bug-01243');
266
267
   is($atomicUpdate->filename, "Bug_01243-SingleUpdate.pl", "Bug_01243-SingleUpdate.pl added to DB");
268
   is($ENV{ATOMICUPDATE_TESTS_2}, 10, "Bug_01243-SingleUpdate.pl executed");
269
270
   $test_file->remove;
271
272
};
273
274
$test_file->remove;
275
$schema->storage->txn_rollback;
276
277
sub create_file {
278
   my ($file) = @_;
279
   my $tmpdir = File::Spec->tmpdir();
280
   my $path = $tmpdir.'/'.$file->{filepath};
281
   File::Path::make_path($path);
282
   my $test_file = File::Fu::File->new($path.'/'.$file->{filename});
283
284
   $test_file->write($file->{content}) if $file->{content};
285
286
   return $test_file;
287
}

Return to bug 14698