source: trunk/src/allmydata/test/test_happiness.py

Last change on this file was 1cfe843d, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-22T23:40:25Z

more python2 removal

  • Property mode set to 100644
File size: 17.2 KB
Line 
1# -*- coding: utf-8 -*-
2"""
3Tests for allmydata.immutable.happiness_upload and
4allmydata.util.happinessutil.
5
6Ported to Python 3.
7"""
8
9from twisted.trial import unittest
10from hypothesis import given
11from hypothesis.strategies import text, sets
12
13from allmydata.immutable import happiness_upload
14from allmydata.util.happinessutil import servers_of_happiness, \
15    shares_by_server, merge_servers
16from allmydata.test.common import ShouldFailMixin
17
18
19class 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
54class 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
233class 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
285class 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
293class 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)
Note: See TracBrowser for help on using the repository browser.