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

(-)a/t/db_dependent/HoldsQueue/TransportCostOptimizations.t (-1 / +406 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Test C4::HoldsQueue::CreateQueue() for optimal allocation using the
4
# transport cost matrix.
5
#
6
# Wraps tests in transaction that's rolled back, so no data is destroyed
7
# MySQL WARNING: This makes sense only if your tables are InnoDB, otherwise
8
# transactions are not supported and mess is left behind
9
10
use Modern::Perl;
11
12
use Test::More tests => 117;
13
use Data::Dumper;
14
15
use C4::Context;
16
use Koha::Database;
17
use C4::Reserves qw( AddReserve );
18
use C4::HoldsQueue;
19
20
use t::lib::TestBuilder;
21
use t::lib::Mocks;
22
23
sub mock_cost_matrix {
24
    my $matrix  = shift;
25
    my $builder = shift;
26
    my $schema  = shift;
27
28
    my $rows = scalar(@$matrix);
29
    my $cols = $rows > 0 ? scalar( @{ $matrix->[0] } ) : 0;
30
31
    my $max = $rows >= $cols ? $rows : $cols;
32
33
    my @libraries = ();
34
35
    for ( my $i = 0 ; $i < $max ; $i++ ) {
36
        push @libraries, $builder->build(
37
            {
38
                source => 'Branch',
39
            }
40
        );
41
    }
42
43
    for ( my $i = 0 ; $i < scalar(@$matrix) ; $i++ ) {
44
        for ( my $j = 0 ; $j < scalar( @{ $matrix->[$i] } ) ; $j++ ) {
45
            if ( $i != $j ) {
46
                my $tc_rs = $schema->resultset('TransportCost');
47
                my $cost  = $matrix->[$i]->[$j];
48
                $tc_rs->create(
49
                    {
50
                        frombranch       => $libraries[$i]->{branchcode},
51
                        tobranch         => $libraries[$j]->{branchcode},
52
                        cost             => $cost,
53
                        disable_transfer => ( $cost < 0 ? 1 : 0 )
54
                    }
55
                );
56
            }
57
        }
58
    }
59
60
    return \@libraries;
61
}
62
63
sub mock_scenario {
64
    my $libraries           = shift;
65
    my $items_at_library    = shift;
66
    my $reserves_at_library = shift;
67
    my $builder             = shift;
68
69
    my @borrowers = ();
70
71
    for ( my $i = 0 ; $i < scalar(@$libraries) ; $i++ ) {
72
        my @at_library = ();
73
        for ( my $j = 0 ; $j < $reserves_at_library->[$i] ; $j++ ) {
74
            push @at_library, $builder->build(
75
                {
76
                    source => 'Borrower',
77
                    value  => {
78
                        branchcode => $libraries->[$i]->{branchcode},
79
                    }
80
                }
81
            );
82
        }
83
        push @borrowers, \@at_library;
84
    }
85
86
    my $biblio = $builder->build_sample_biblio();
87
88
    my @items = ();
89
90
    for ( my $i = 0 ; $i < scalar(@$libraries) ; $i++ ) {
91
        for ( my $j = 0 ; $j < $items_at_library->[$i] ; $j++ ) {
92
            push @items, $builder->build_sample_item(
93
                {
94
                    biblionumber  => $biblio->biblionumber,
95
                    holdingbranch => $libraries->[$i]->{branchcode},
96
                    homebranch    => $libraries->[$i]->{branchcode},
97
                }
98
            );
99
        }
100
    }
101
102
    for ( my $i = 0 ; $i < scalar(@$libraries) ; $i++ ) {
103
        for ( my $j = 0 ; $j < $reserves_at_library->[$i] ; $j++ ) {
104
            my $borrower = $borrowers[$i]->[$j];
105
            AddReserve(
106
                {
107
                    branchcode     => $borrower->{branchcode},
108
                    borrowernumber => $borrower->{borrowernumber},
109
                    biblionumber   => $biblio->biblionumber,
110
                    priority       => 1 + $j + $i * scalar(@$libraries),
111
                }
112
            );
113
        }
114
    }
115
}
116
117
sub library_index {
118
    my ( $branchcode, $libraries ) = @_;
119
120
    for ( my $i = 0 ; $i < scalar(@$libraries) ; $i++ ) {
121
        if ( $libraries->[$i]->{branchcode} eq $branchcode ) {
122
            return $i;
123
        }
124
    }
125
126
    return -1;
127
}
128
129
sub allocation_indices {
130
    my ( $libraries, $allocations ) = @_;
131
132
    my @allocations = ();
133
134
    for ( my $i = 0 ; $i < scalar(@$allocations) ; $i++ ) {
135
        my $pick    = library_index( $allocations->[$i]->{pickbranch},    $libraries );
136
        my $holding = library_index( $allocations->[$i]->{holdingbranch}, $libraries );
137
        push @allocations, [ $holding, $pick ];
138
    }
139
140
    return @allocations;
141
}
142
143
sub total_cost {
144
    my ( $matrix, $libraries, $allocations ) = @_;
145
146
    my @allocations = allocation_indices( $libraries, $allocations );
147
    my $total       = 0;
148
149
    for my $allocation (@allocations) {
150
        my $cost = $matrix->[ $allocation->[0] ]->[ $allocation->[1] ];
151
        $total += $cost;
152
    }
153
154
    return $total;
155
}
156
157
# Mock a scenario for reservations on a biblio.
158
#
159
# Parameters:
160
# $label               - Label for scenario
161
# $matrix              - Transport cost matrix (array of arrays of numbers).
162
# $items_at_library    - Array specifying the number of items for the biblio at each library.
163
#                        Indices correspond to the indices in the transport cost matrix.
164
# $reserves_at_library - Array specifying the number of holds on the biblio at each library.
165
#                        Indices correspond to the indices in the transport cost matrix.
166
# $expected_allocation - Array of tuples specifying expected allocation on the form
167
#                        [<index of holding library>, <index of of pick library>].  Specify
168
#                        undefined if optimal allocation is ambiguous
169
# $expected_total_cost - The expected total cost of the allocation.
170
sub test_allocation {
171
    my ( $label, $matrix, $items_at_library, $reserves_at_library, $expected_allocation, $expected_total_cost ) = @_;
172
173
    my $schema = Koha::Database->schema;
174
    $schema->storage->txn_begin;
175
    my $dbh = C4::Context->dbh;
176
177
    my $builder = t::lib::TestBuilder->new;
178
179
    t::lib::Mocks::mock_preference( 'UseBranchTransferLimits',  '0' );
180
    t::lib::Mocks::mock_preference( 'BranchTransferLimitsType', 'itemtype' );
181
    t::lib::Mocks::mock_preference( 'UseTransportCostMatrix',   '1' );
182
183
    my $libraries = mock_cost_matrix( $matrix, $builder, $schema );
184
185
    mock_scenario( $libraries, $items_at_library, $reserves_at_library, $builder );
186
187
    C4::HoldsQueue::CreateQueue();
188
189
    my $holds_queue = $dbh->selectall_arrayref( "SELECT * FROM tmp_holdsqueue", { Slice => {} } );
190
191
    if ($expected_allocation) {
192
        my @indices = allocation_indices( $libraries, $holds_queue );
193
194
        is( scalar(@indices), scalar(@$expected_allocation), "$label correct number of allocations" );
195
196
        # print STDERR Dumper(\@indices);
197
198
        while ( my $expected = shift @$expected_allocation ) {
199
            my $found = 0;
200
            for ( my $i = 0 ; $i < scalar(@indices) ; $i++ ) {
201
                if (   $expected->[0] == $indices[$i]->[0]
202
                    && $expected->[1] == $indices[$i]->[1] )
203
                {
204
                    $found = 1;
205
                    $indices[$i] = [ -1, -1 ];
206
                    last;
207
                }
208
            }
209
            ok( $found, "$label - allocation contained [" . $expected->[0] . ", " . $expected->[1] . "]" );
210
        }
211
    }
212
213
    is( total_cost( $matrix, $libraries, $holds_queue ), $expected_total_cost, "$label the total cost is as expected" );
214
215
    $schema->txn_rollback;
216
}
217
218
test_allocation(
219
    "trivial case",
220
    [],
221
    [],
222
    [],
223
    [],
224
    0
225
);
226
227
test_allocation(
228
    "unit case",
229
    [ [0] ],
230
    [1],
231
    [1],
232
    [ [ 0, 0 ] ],
233
    0
234
);
235
236
test_allocation(
237
    "all local allocations",
238
    [
239
        [ 0, 1, 1 ],
240
        [ 1, 0, 1 ],
241
        [ 1, 1, 0 ]
242
    ],
243
    [ 1, 1, 1 ],
244
    [ 1, 1, 1 ],
245
    undef,
246
    0
247
);
248
249
test_allocation(
250
    "some non-local allocations",
251
    [
252
        [ 0, 1, 1 ],
253
        [ 1, 0, 1 ],
254
        [ 1, 1, 0 ]
255
    ],
256
    [ 3,        0,        0 ],
257
    [ 1,        1,        1 ],
258
    [ [ 0, 0 ], [ 0, 1 ], [ 0, 2 ] ],
259
    2
260
);
261
262
test_allocation(
263
    "different costs 1",
264
    [
265
        [ 0,  2,  2 ],
266
        [ 1,  0,  1 ],
267
        [ -1, -1, 0 ]
268
    ],
269
    [ 3,        3,        0 ],
270
    [ 0,        0,        3 ],
271
    [ [ 1, 2 ], [ 1, 2 ], [ 1, 2 ] ],
272
    3
273
);
274
275
test_allocation(
276
    "different costs 2",
277
    [
278
        [ 0,  2,  2 ],
279
        [ 1,  0,  1 ],
280
        [ -1, -1, 0 ]
281
    ],
282
    [ 2,        2,        0 ],
283
    [ 0,        0,        3 ],
284
    [ [ 1, 2 ], [ 1, 2 ], [ 0, 2 ] ],
285
    4
286
);
287
288
test_allocation(
289
    "allocation prohibited",
290
    [
291
        [ 0,  -1 ],
292
        [ -1, 0 ],
293
    ],
294
    [ 2, 0 ],
295
    [ 0, 2 ],
296
    [],
297
    0
298
);
299
300
test_allocation(
301
    "some allocation prohibited",
302
    [
303
        [ 0,  -1, -1 ],
304
        [ -1, 0,  1 ],
305
        [ -1, -1, 0 ]
306
    ],
307
    [ 2,        2, 0 ],
308
    [ 0,        0, 4 ],
309
    [ [ 1, 2 ], [ 1, 2 ] ],
310
    2
311
);
312
313
test_allocation(
314
    "local allocation suboptimal",
315
    [
316
        [ 0, 1, 1 ],
317
        [ 1, 0, 10 ],
318
        [ 1, 1, 0 ]
319
    ],
320
    [ 1,        2, 0 ],
321
    [ 1,        0, 1 ],
322
    [ [ 0, 2 ], [ 1, 0 ] ],
323
    2
324
);
325
326
test_allocation(
327
    "prefer fewer fails",
328
    [
329
        [ 0,  1,  1,  1,  1 ],
330
        [ -1, 0,  -1, -1, 1 ],
331
        [ -1, -1, 0,  -1, -1 ],
332
        [ -1, -1, -1, 0,  -1 ],
333
        [ -1, -1, -1, -1, 0 ]
334
    ],
335
    [ 1,        1, 0, 0, 0 ],
336
    [ 0,        0, 1, 1, 1 ],
337
    [ [ 0, 2 ], [ 1, 4 ] ],
338
    2
339
);
340
341
test_allocation(
342
    "large volume",
343
    [
344
        [ (0) x 38, 23, 8 ],
345
        [ (0) x 38, 31, 29 ],
346
        [ (0) x 38, 10, 21 ],
347
        [ (0) x 38, 98, 58 ],
348
        [ (0) x 38, 24, 93 ],
349
        [ (0) x 38, 62, 71 ],
350
        [ (0) x 38, 75, 3 ],
351
        [ (0) x 38, 86, 32 ],
352
        [ (0) x 38, 45, 38 ],
353
        [ (0) x 38, 83, 75 ],
354
        [ (0) x 38, 45, 16 ],
355
        [ (0) x 38, 60, 7 ],
356
        [ (0) x 38, 83, 79 ],
357
        [ (0) x 38, 54, 59 ],
358
        [ (0) x 38, 78, 44 ],
359
        [ (0) x 38, 77, 49 ],
360
        [ (0) x 38, 9,  8 ],
361
        [ (0) x 38, 13, 63 ],
362
        [ (0) x 38, 82, 6 ],
363
        [ (0) x 38, 62, 9 ],
364
        [ (0) x 38, 22, 94 ],
365
        [ (0) x 38, 58, 8 ],
366
        [ (0) x 38, 39, 1 ],
367
        [ (0) x 38, 69, 71 ],
368
        [ (0) x 38, 13, 26 ],
369
        [ (0) x 38, 3,  67 ],
370
        [ (0) x 38, 39, 33 ],
371
        [ (0) x 38, 60, 91 ],
372
        [ (0) x 38, 46, 44 ],
373
        [ (0) x 38, 71, 59 ],
374
        [ (0) x 38, 66, 93 ],
375
        [ (0) x 38, 38, 92 ],
376
        [ (0) x 38, 72, 71 ],
377
        [ (0) x 38, 0,  7 ],
378
        [ (0) x 38, 44, 87 ],
379
        [ (0) x 38, 74, 41 ],
380
        [ (0) x 38, 21, 78 ],
381
        [ (0) x 38, 90, 68 ],
382
        [ (0) x 38, 96, 18 ],
383
        [ (0) x 38, 47, 68 ],
384
    ],
385
    [ (5) x 38, (0) x 2 ],
386
    [ (0) x 38, (40) x 2 ],
387
    [
388
        ( [ 33, 38 ] ) x 5,    # cost 0
389
        ( [ 22, 39 ] ) x 5,    # cost 1
390
        ( [ 6,  39 ] ) x 5,    # cost 3
391
        ( [ 25, 38 ] ) x 5,    # cost 3
392
        ( [ 18, 39 ] ) x 5,    # cost 6
393
        ( [ 11, 39 ] ) x 5,    # cost 7
394
        ( [ 0,  39 ] ) x 5,    # cost 8
395
        ( [ 21, 39 ] ) x 5,    # cost 8
396
        ( [ 16, 38 ] ) x 5,    # cost 9
397
        ( [ 19, 39 ] ) x 5,    # cost 9
398
        ( [ 2,  38 ] ) x 5,    # cost 10
399
        ( [ 17, 38 ] ) x 5,    # cost 13
400
        ( [ 24, 38 ] ) x 5,    # cost 13
401
        ( [ 10, 39 ] ) x 5,    # cost 16
402
        ( [ 36, 38 ] ) x 5,    # cost 21
403
        ( [ 20, 38 ] ) x 5,    # cost 22
404
    ],
405
    745
406
);

Return to bug 35826