1 | # -*- coding: utf-8 -*- |
---|
2 | """ |
---|
3 | Tests for allmydata.immutable.happiness_upload and |
---|
4 | allmydata.util.happinessutil. |
---|
5 | |
---|
6 | Ported to Python 3. |
---|
7 | """ |
---|
8 | |
---|
9 | from twisted.trial import unittest |
---|
10 | from hypothesis import given |
---|
11 | from hypothesis.strategies import text, sets |
---|
12 | |
---|
13 | from allmydata.immutable import happiness_upload |
---|
14 | from allmydata.util.happinessutil import servers_of_happiness, \ |
---|
15 | shares_by_server, merge_servers |
---|
16 | from allmydata.test.common import ShouldFailMixin |
---|
17 | |
---|
18 | |
---|
19 | class HappinessUploadUtils(unittest.TestCase): |
---|
20 | """ |
---|
21 | test-cases for happiness_upload utility functions augmenting_path_for and |
---|
22 | residual_network. |
---|
23 | """ |
---|
24 | |
---|
25 | def test_residual_0(self): |
---|
26 | graph = happiness_upload._servermap_flow_graph( |
---|
27 | ['peer0'], |
---|
28 | ['share0'], |
---|
29 | servermap={ |
---|
30 | 'peer0': ['share0'], |
---|
31 | } |
---|
32 | ) |
---|
33 | flow = [[0 for _ in graph] for _ in graph] |
---|
34 | |
---|
35 | residual, capacity = happiness_upload.residual_network(graph, flow) |
---|
36 | |
---|
37 | # XXX no idea if these are right; hand-verify |
---|
38 | self.assertEqual(residual, [[1], [2], [3], []]) |
---|
39 | self.assertEqual(capacity, [[0, 1, 0, 0], [-1, 0, 1, 0], [0, -1, 0, 1], [0, 0, -1, 0]]) |
---|
40 | |
---|
41 | def test_trivial_maximum_graph(self): |
---|
42 | self.assertEqual( |
---|
43 | {}, |
---|
44 | happiness_upload._compute_maximum_graph([], {}) |
---|
45 | ) |
---|
46 | |
---|
47 | def test_trivial_flow_graph(self): |
---|
48 | self.assertEqual( |
---|
49 | [], |
---|
50 | happiness_upload._servermap_flow_graph(set(), set(), {}) |
---|
51 | ) |
---|
52 | |
---|
53 | |
---|
54 | class Happiness(unittest.TestCase): |
---|
55 | |
---|
56 | def test_placement_simple(self): |
---|
57 | |
---|
58 | shares = {'share0', 'share1', 'share2'} |
---|
59 | peers = {'peer0', 'peer1'} |
---|
60 | readonly_peers = {'peer0'} |
---|
61 | peers_to_shares = { |
---|
62 | 'peer0': {'share2'}, |
---|
63 | 'peer1': [], |
---|
64 | } |
---|
65 | |
---|
66 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
67 | |
---|
68 | self.assertEqual( |
---|
69 | places, |
---|
70 | { |
---|
71 | 'share0': 'peer1', |
---|
72 | 'share1': 'peer1', |
---|
73 | 'share2': 'peer0', |
---|
74 | } |
---|
75 | ) |
---|
76 | |
---|
77 | def test_placement_1(self): |
---|
78 | |
---|
79 | shares = { |
---|
80 | 'share0', 'share1', 'share2', |
---|
81 | 'share3', 'share4', 'share5', |
---|
82 | 'share6', 'share7', 'share8', |
---|
83 | 'share9', |
---|
84 | } |
---|
85 | peers = { |
---|
86 | 'peer0', 'peer1', 'peer2', 'peer3', |
---|
87 | 'peer4', 'peer5', 'peer6', 'peer7', |
---|
88 | 'peer8', 'peer9', 'peerA', 'peerB', |
---|
89 | } |
---|
90 | readonly_peers = {'peer0', 'peer1', 'peer2', 'peer3'} |
---|
91 | peers_to_shares = { |
---|
92 | 'peer0': {'share0'}, |
---|
93 | 'peer1': {'share1'}, |
---|
94 | 'peer2': {'share2'}, |
---|
95 | 'peer3': {'share3'}, |
---|
96 | 'peer4': {'share4'}, |
---|
97 | 'peer5': {'share5'}, |
---|
98 | 'peer6': {'share6'}, |
---|
99 | 'peer7': {'share7'}, |
---|
100 | 'peer8': {'share8'}, |
---|
101 | 'peer9': {'share9'}, |
---|
102 | 'peerA': set(), |
---|
103 | 'peerB': set(), |
---|
104 | } |
---|
105 | |
---|
106 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
107 | |
---|
108 | # actually many valid answers for this, so long as peer's 0, |
---|
109 | # 1, 2, 3 all have share 0, 1, 2 3. |
---|
110 | |
---|
111 | # share N maps to peer N |
---|
112 | # i.e. this says that share0 should be on peer0, share1 should |
---|
113 | # be on peer1, etc. |
---|
114 | expected = { |
---|
115 | 'share{}'.format(i): 'peer{}'.format(i) |
---|
116 | for i in range(10) |
---|
117 | } |
---|
118 | self.assertEqual(expected, places) |
---|
119 | |
---|
120 | def test_unhappy(self): |
---|
121 | shares = { |
---|
122 | 'share1', 'share2', 'share3', 'share4', 'share5', |
---|
123 | } |
---|
124 | peers = { |
---|
125 | 'peer1', 'peer2', 'peer3', 'peer4', |
---|
126 | } |
---|
127 | readonly_peers = set() |
---|
128 | peers_to_shares = {} |
---|
129 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
130 | happiness = happiness_upload.calculate_happiness(places) |
---|
131 | self.assertEqual(4, happiness) |
---|
132 | |
---|
133 | def test_hypothesis0(self): |
---|
134 | peers={u'0', u'00'} |
---|
135 | shares={u'0', u'1'} |
---|
136 | readonly_peers = set() |
---|
137 | peers_to_shares = dict() |
---|
138 | |
---|
139 | #h = happiness_upload.HappinessUpload(peers, readonly_peers, shares, peers_to_shares) |
---|
140 | #places = h.generate_mappings() |
---|
141 | #happiness = h.happiness() |
---|
142 | |
---|
143 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
144 | happiness = happiness_upload.calculate_happiness(places) |
---|
145 | |
---|
146 | self.assertEqual(2, happiness) |
---|
147 | |
---|
148 | def test_100(self): |
---|
149 | peers = set(['peer{}'.format(x) for x in range(100)]) |
---|
150 | shares = set(['share{}'.format(x) for x in range(100)]) |
---|
151 | readonly_peers = set() |
---|
152 | peers_to_shares = dict() |
---|
153 | |
---|
154 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
155 | happiness = happiness_upload.calculate_happiness(places) |
---|
156 | |
---|
157 | self.assertEqual(100, happiness) |
---|
158 | |
---|
159 | def test_redistribute(self): |
---|
160 | """ |
---|
161 | with existing shares 0, 3 on a single servers we can achieve |
---|
162 | higher happiness by moving one of those shares to a new server |
---|
163 | """ |
---|
164 | peers = {'a', 'b', 'c', 'd'} |
---|
165 | shares = {'0', '1', '2', '3'} |
---|
166 | readonly_peers = set() |
---|
167 | peers_to_shares = { |
---|
168 | 'a': set(['0']), |
---|
169 | 'b': set(['1']), |
---|
170 | 'c': set(['2', '3']), |
---|
171 | } |
---|
172 | # we can achieve more happiness by moving "2" or "3" to server "d" |
---|
173 | |
---|
174 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
175 | #print("places %s" % places) |
---|
176 | #places = happiness_upload.slow_share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
177 | #print("places %s" % places) |
---|
178 | |
---|
179 | happiness = happiness_upload.calculate_happiness(places) |
---|
180 | self.assertEqual(4, happiness) |
---|
181 | |
---|
182 | def test_calc_happy(self): |
---|
183 | # share -> server |
---|
184 | share_placements = { |
---|
185 | 0: "\x0e\xd6\xb3>\xd6\x85\x9d\x94')'\xf03:R\x88\xf1\x04\x1b\xa4", |
---|
186 | 1: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
187 | 2: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
188 | 3: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
189 | 4: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
190 | 5: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
191 | 6: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
192 | 7: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
193 | 8: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
194 | 9: '\xb9\xa3N\x80u\x9c_\xf7\x97FSS\xa7\xbd\x02\xf9f$:\t', |
---|
195 | } |
---|
196 | happy = happiness_upload.calculate_happiness(share_placements) |
---|
197 | self.assertEqual(2, happy) |
---|
198 | |
---|
199 | def test_hypothesis_0(self): |
---|
200 | """ |
---|
201 | an error-case Hypothesis found |
---|
202 | """ |
---|
203 | peers={u'0'} |
---|
204 | shares={u'0', u'1'} |
---|
205 | |
---|
206 | places = happiness_upload.share_placement(peers, set(), shares, {}) |
---|
207 | happiness = happiness_upload.calculate_happiness(places) |
---|
208 | |
---|
209 | assert set(places.values()).issubset(peers) |
---|
210 | assert happiness == min(len(peers), len(shares)) |
---|
211 | |
---|
212 | def test_hypothesis_1(self): |
---|
213 | """ |
---|
214 | an error-case Hypothesis found |
---|
215 | """ |
---|
216 | peers = {u'0', u'1', u'2', u'3'} |
---|
217 | shares = {u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8'} |
---|
218 | |
---|
219 | places = happiness_upload.share_placement(peers, set(), shares, {}) |
---|
220 | happiness = happiness_upload.calculate_happiness(places) |
---|
221 | |
---|
222 | assert set(places.values()).issubset(peers) |
---|
223 | assert happiness == min(len(peers), len(shares)) |
---|
224 | |
---|
225 | def test_everything_broken(self): |
---|
226 | peers = set() |
---|
227 | shares = {u'0', u'1', u'2', u'3'} |
---|
228 | |
---|
229 | places = happiness_upload.share_placement(peers, set(), shares, {}) |
---|
230 | self.assertEqual(places, dict()) |
---|
231 | |
---|
232 | |
---|
233 | class PlacementTests(unittest.TestCase): |
---|
234 | |
---|
235 | @given( |
---|
236 | sets(elements=text(min_size=1, max_size=30), min_size=4, max_size=4), |
---|
237 | sets(elements=text(min_size=1, max_size=30), min_size=4), |
---|
238 | ) |
---|
239 | def test_hypothesis_unhappy(self, peers, shares): |
---|
240 | """ |
---|
241 | similar to test_unhappy we test that the resulting happiness is |
---|
242 | always 4 since the size of peers is 4. |
---|
243 | """ |
---|
244 | # https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.sets |
---|
245 | # hypothesis.strategies.sets(elements=None, min_size=None, average_size=None, max_size=None)[source] |
---|
246 | readonly_peers = set() |
---|
247 | peers_to_shares = {} |
---|
248 | places = happiness_upload.share_placement(peers, readonly_peers, shares, peers_to_shares) |
---|
249 | happiness = happiness_upload.calculate_happiness(places) |
---|
250 | assert set(places.keys()) == shares |
---|
251 | assert happiness == 4 |
---|
252 | |
---|
253 | @given( |
---|
254 | sets(elements=text(min_size=1, max_size=30), min_size=1, max_size=10), |
---|
255 | # can we make a readonly_peers that's a subset of ^ |
---|
256 | sets(elements=text(min_size=1, max_size=30), min_size=1, max_size=20), |
---|
257 | ) |
---|
258 | def test_more_hypothesis(self, peers, shares): |
---|
259 | """ |
---|
260 | similar to test_unhappy we test that the resulting happiness is |
---|
261 | always either the number of peers or the number of shares |
---|
262 | whichever is smaller. |
---|
263 | """ |
---|
264 | # https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.sets |
---|
265 | # hypothesis.strategies.sets(elements=None, min_size=None, average_size=None, max_size=None)[source] |
---|
266 | # XXX would be nice to paramaterize these by hypothesis too |
---|
267 | readonly_peers = set() |
---|
268 | peers_to_shares = {} |
---|
269 | |
---|
270 | places = happiness_upload.share_placement(peers, readonly_peers, set(list(shares)), peers_to_shares) |
---|
271 | happiness = happiness_upload.calculate_happiness(places) |
---|
272 | |
---|
273 | # every share should get placed |
---|
274 | assert set(places.keys()) == shares |
---|
275 | |
---|
276 | # we should only use peers that exist |
---|
277 | assert set(places.values()).issubset(peers) |
---|
278 | |
---|
279 | # if we have more shares than peers, happiness is at most # of |
---|
280 | # peers; if we have fewer shares than peers happiness is capped at |
---|
281 | # # of peers. |
---|
282 | assert happiness == min(len(peers), len(shares)) |
---|
283 | |
---|
284 | |
---|
285 | class FakeServerTracker(object): |
---|
286 | def __init__(self, serverid, buckets): |
---|
287 | self._serverid = serverid |
---|
288 | self.buckets = buckets |
---|
289 | def get_serverid(self): |
---|
290 | return self._serverid |
---|
291 | |
---|
292 | |
---|
293 | class HappinessUtilTests(unittest.TestCase, ShouldFailMixin): |
---|
294 | """Tests for happinesutil.py.""" |
---|
295 | |
---|
296 | def test_merge_servers(self): |
---|
297 | # merge_servers merges a list of upload_servers and a dict of |
---|
298 | # shareid -> serverid mappings. |
---|
299 | shares = { |
---|
300 | 1 : set(["server1"]), |
---|
301 | 2 : set(["server2"]), |
---|
302 | 3 : set(["server3"]), |
---|
303 | 4 : set(["server4", "server5"]), |
---|
304 | 5 : set(["server1", "server2"]), |
---|
305 | } |
---|
306 | # if not provided with a upload_servers argument, it should just |
---|
307 | # return the first argument unchanged. |
---|
308 | self.failUnlessEqual(shares, merge_servers(shares, set([]))) |
---|
309 | trackers = [] |
---|
310 | for (i, server) in [(i, "server%d" % i) for i in range(5, 9)]: |
---|
311 | t = FakeServerTracker(server, [i]) |
---|
312 | trackers.append(t) |
---|
313 | expected = { |
---|
314 | 1 : set(["server1"]), |
---|
315 | 2 : set(["server2"]), |
---|
316 | 3 : set(["server3"]), |
---|
317 | 4 : set(["server4", "server5"]), |
---|
318 | 5 : set(["server1", "server2", "server5"]), |
---|
319 | 6 : set(["server6"]), |
---|
320 | 7 : set(["server7"]), |
---|
321 | 8 : set(["server8"]), |
---|
322 | } |
---|
323 | self.failUnlessEqual(expected, merge_servers(shares, set(trackers))) |
---|
324 | shares2 = {} |
---|
325 | expected = { |
---|
326 | 5 : set(["server5"]), |
---|
327 | 6 : set(["server6"]), |
---|
328 | 7 : set(["server7"]), |
---|
329 | 8 : set(["server8"]), |
---|
330 | } |
---|
331 | self.failUnlessEqual(expected, merge_servers(shares2, set(trackers))) |
---|
332 | shares3 = {} |
---|
333 | trackers = [] |
---|
334 | expected = {} |
---|
335 | for (i, server) in [(i, "server%d" % i) for i in range(10)]: |
---|
336 | shares3[i] = set([server]) |
---|
337 | t = FakeServerTracker(server, [i]) |
---|
338 | trackers.append(t) |
---|
339 | expected[i] = set([server]) |
---|
340 | self.failUnlessEqual(expected, merge_servers(shares3, set(trackers))) |
---|
341 | |
---|
342 | |
---|
343 | def test_servers_of_happiness_utility_function(self): |
---|
344 | # These tests are concerned with the servers_of_happiness() |
---|
345 | # utility function, and its underlying matching algorithm. Other |
---|
346 | # aspects of the servers_of_happiness behavior are tested |
---|
347 | # elsehwere These tests exist to ensure that |
---|
348 | # servers_of_happiness doesn't under or overcount the happiness |
---|
349 | # value for given inputs. |
---|
350 | |
---|
351 | # servers_of_happiness expects a dict of |
---|
352 | # shnum => set(serverids) as a preexisting shares argument. |
---|
353 | test1 = { |
---|
354 | 1 : set(["server1"]), |
---|
355 | 2 : set(["server2"]), |
---|
356 | 3 : set(["server3"]), |
---|
357 | 4 : set(["server4"]) |
---|
358 | } |
---|
359 | happy = servers_of_happiness(test1) |
---|
360 | self.failUnlessEqual(4, happy) |
---|
361 | test1[4] = set(["server1"]) |
---|
362 | # We've added a duplicate server, so now servers_of_happiness |
---|
363 | # should be 3 instead of 4. |
---|
364 | happy = servers_of_happiness(test1) |
---|
365 | self.failUnlessEqual(3, happy) |
---|
366 | # The second argument of merge_servers should be a set of objects with |
---|
367 | # serverid and buckets as attributes. In actual use, these will be |
---|
368 | # ServerTracker instances, but for testing it is fine to make a |
---|
369 | # FakeServerTracker whose job is to hold those instance variables to |
---|
370 | # test that part. |
---|
371 | trackers = [] |
---|
372 | for (i, server) in [(i, "server%d" % i) for i in range(5, 9)]: |
---|
373 | t = FakeServerTracker(server, [i]) |
---|
374 | trackers.append(t) |
---|
375 | # Recall that test1 is a server layout with servers_of_happiness |
---|
376 | # = 3. Since there isn't any overlap between the shnum -> |
---|
377 | # set([serverid]) correspondences in test1 and those in trackers, |
---|
378 | # the result here should be 7. |
---|
379 | test2 = merge_servers(test1, set(trackers)) |
---|
380 | happy = servers_of_happiness(test2) |
---|
381 | self.failUnlessEqual(7, happy) |
---|
382 | # Now add an overlapping server to trackers. This is redundant, |
---|
383 | # so it should not cause the previously reported happiness value |
---|
384 | # to change. |
---|
385 | t = FakeServerTracker("server1", [1]) |
---|
386 | trackers.append(t) |
---|
387 | test2 = merge_servers(test1, set(trackers)) |
---|
388 | happy = servers_of_happiness(test2) |
---|
389 | self.failUnlessEqual(7, happy) |
---|
390 | test = {} |
---|
391 | happy = servers_of_happiness(test) |
---|
392 | self.failUnlessEqual(0, happy) |
---|
393 | # Test a more substantial overlap between the trackers and the |
---|
394 | # existing assignments. |
---|
395 | test = { |
---|
396 | 1 : set(['server1']), |
---|
397 | 2 : set(['server2']), |
---|
398 | 3 : set(['server3']), |
---|
399 | 4 : set(['server4']), |
---|
400 | } |
---|
401 | trackers = [] |
---|
402 | t = FakeServerTracker('server5', [4]) |
---|
403 | trackers.append(t) |
---|
404 | t = FakeServerTracker('server6', [3, 5]) |
---|
405 | trackers.append(t) |
---|
406 | # The value returned by servers_of_happiness is the size |
---|
407 | # of a maximum matching in the bipartite graph that |
---|
408 | # servers_of_happiness() makes between serverids and share |
---|
409 | # numbers. It should find something like this: |
---|
410 | # (server 1, share 1) |
---|
411 | # (server 2, share 2) |
---|
412 | # (server 3, share 3) |
---|
413 | # (server 5, share 4) |
---|
414 | # (server 6, share 5) |
---|
415 | # |
---|
416 | # and, since there are 5 edges in this matching, it should |
---|
417 | # return 5. |
---|
418 | test2 = merge_servers(test, set(trackers)) |
---|
419 | happy = servers_of_happiness(test2) |
---|
420 | self.failUnlessEqual(5, happy) |
---|
421 | # Zooko's first puzzle: |
---|
422 | # (from http://allmydata.org/trac/tahoe-lafs/ticket/778#comment:156) |
---|
423 | # |
---|
424 | # server 1: shares 0, 1 |
---|
425 | # server 2: shares 1, 2 |
---|
426 | # server 3: share 2 |
---|
427 | # |
---|
428 | # This should yield happiness of 3. |
---|
429 | test = { |
---|
430 | 0 : set(['server1']), |
---|
431 | 1 : set(['server1', 'server2']), |
---|
432 | 2 : set(['server2', 'server3']), |
---|
433 | } |
---|
434 | self.failUnlessEqual(3, servers_of_happiness(test)) |
---|
435 | # Zooko's second puzzle: |
---|
436 | # (from http://allmydata.org/trac/tahoe-lafs/ticket/778#comment:158) |
---|
437 | # |
---|
438 | # server 1: shares 0, 1 |
---|
439 | # server 2: share 1 |
---|
440 | # |
---|
441 | # This should yield happiness of 2. |
---|
442 | test = { |
---|
443 | 0 : set(['server1']), |
---|
444 | 1 : set(['server1', 'server2']), |
---|
445 | } |
---|
446 | self.failUnlessEqual(2, servers_of_happiness(test)) |
---|
447 | |
---|
448 | |
---|
449 | def test_shares_by_server(self): |
---|
450 | test = dict([(i, set(["server%d" % i])) for i in range(1, 5)]) |
---|
451 | sbs = shares_by_server(test) |
---|
452 | self.failUnlessEqual(set([1]), sbs["server1"]) |
---|
453 | self.failUnlessEqual(set([2]), sbs["server2"]) |
---|
454 | self.failUnlessEqual(set([3]), sbs["server3"]) |
---|
455 | self.failUnlessEqual(set([4]), sbs["server4"]) |
---|
456 | test1 = { |
---|
457 | 1 : set(["server1"]), |
---|
458 | 2 : set(["server1"]), |
---|
459 | 3 : set(["server1"]), |
---|
460 | 4 : set(["server2"]), |
---|
461 | 5 : set(["server2"]) |
---|
462 | } |
---|
463 | sbs = shares_by_server(test1) |
---|
464 | self.failUnlessEqual(set([1, 2, 3]), sbs["server1"]) |
---|
465 | self.failUnlessEqual(set([4, 5]), sbs["server2"]) |
---|
466 | # This should fail unless the serverid part of the mapping is a set |
---|
467 | test2 = {1: "server1"} |
---|
468 | self.shouldFail(AssertionError, |
---|
469 | "test_shares_by_server", |
---|
470 | "", |
---|
471 | shares_by_server, test2) |
---|