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 |
); |