Ticket #778: tests2.txt

File tests2.txt, 113.7 KB (added by kevan, at 2010-05-07T22:34:20Z)
Line 
1Sat Oct 17 18:30:13 PDT 2009  Kevan Carstensen <kevan@isnotajoke.com>
2  * Alter NoNetworkGrid to allow the creation of readonly servers for testing purposes.
3
4Fri Oct 30 02:19:08 PDT 2009  "Kevan Carstensen" <kevan@isnotajoke.com>
5  * Refactor some behavior into a mixin, and add tests for the behavior described in #778
6
7Tue Nov  3 19:36:02 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
8  * Alter tests to use the new form of set_shareholders
9
10Tue Nov  3 19:42:32 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
11  * Minor tweak to an existing test -- make the first server read-write, instead of read-only
12
13Wed Nov  4 03:13:24 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
14  * Add a test for upload.shares_by_server
15
16Wed Nov  4 03:28:49 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
17  * Add more tests for comment:53 in ticket #778
18
19Sun Nov  8 16:37:35 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
20  * Test Tahoe2PeerSelector to make sure that it recognizeses existing shares on readonly servers
21
22Mon Nov 16 11:23:34 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
23  * Re-work 'test_upload.py' to be more readable; add more tests for #778
24
25Sun Nov 22 17:20:08 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
26  * Add tests for the behavior described in #834.
27
28Fri Dec  4 20:34:53 PST 2009  Kevan Carstensen <kevan@isnotajoke.com>
29  * Replace "UploadHappinessError" with "UploadUnhappinessError" in tests.
30
31Thu Jan  7 10:13:25 PST 2010  Kevan Carstensen <kevan@isnotajoke.com>
32  * Alter various unit tests to work with the new happy behavior
33
34Fri May  7 15:00:51 PDT 2010  Kevan Carstensen <kevan@isnotajoke.com>
35  * Revisions of the #778 tests, per reviewers' comments
36 
37  - Fix comments and confusing naming.
38  - Add tests for the new error messages suggested by David-Sarah
39    and Zooko.
40  - Alter existing tests for new error messages.
41  - Make sure that the tests continue to work with the trunk.
42  - Add a test for a mutual disjointedness assertion that I added to
43    upload.servers_of_happiness.
44  - Fix the comments to correctly reflect read-onlyness
45  - Add a test for an edge case in should_add_server
46  - Add an assertion to make sure that share redistribution works as it
47    should
48  - Alter tests to work with revised servers_of_happiness semantics
49  - Remove tests for should_add_server, since that function no longer exists.
50  - Alter tests to know about merge_peers, and to use it before calling
51    servers_of_happiness.
52  - Add tests for merge_peers.
53  - Add Zooko's puzzles to the tests.
54  - Edit encoding tests to expect the new kind of failure message.
55 
56
57New patches:
58
59[Alter NoNetworkGrid to allow the creation of readonly servers for testing purposes.
60Kevan Carstensen <kevan@isnotajoke.com>**20091018013013
61 Ignore-this: e12cd7c4ddeb65305c5a7e08df57c754
62] {
63hunk ./src/allmydata/test/no_network.py 219
64             c.setServiceParent(self)
65             self.clients.append(c)
66 
67-    def make_server(self, i):
68+    def make_server(self, i, readonly=False):
69         serverid = hashutil.tagged_hash("serverid", str(i))[:20]
70         serverdir = os.path.join(self.basedir, "servers",
71                                  idlib.shortnodeid_b2a(serverid))
72hunk ./src/allmydata/test/no_network.py 224
73         fileutil.make_dirs(serverdir)
74-        ss = StorageServer(serverdir, serverid, stats_provider=SimpleStats())
75+        ss = StorageServer(serverdir, serverid, stats_provider=SimpleStats(),
76+                           readonly_storage=readonly)
77         return ss
78 
79     def add_server(self, i, ss):
80}
81[Refactor some behavior into a mixin, and add tests for the behavior described in #778
82"Kevan Carstensen" <kevan@isnotajoke.com>**20091030091908
83 Ignore-this: a6f9797057ca135579b249af3b2b66ac
84] {
85hunk ./src/allmydata/test/test_upload.py 2
86 
87-import os
88+import os, shutil
89 from cStringIO import StringIO
90 from twisted.trial import unittest
91 from twisted.python.failure import Failure
92hunk ./src/allmydata/test/test_upload.py 12
93 
94 import allmydata # for __full_version__
95 from allmydata import uri, monitor, client
96-from allmydata.immutable import upload
97+from allmydata.immutable import upload, encode
98 from allmydata.interfaces import FileTooLargeError, NoSharesError, \
99      NotEnoughSharesError
100 from allmydata.util.assertutil import precondition
101hunk ./src/allmydata/test/test_upload.py 20
102 from no_network import GridTestMixin
103 from common_util import ShouldFailMixin
104 from allmydata.storage_client import StorageFarmBroker
105+from allmydata.storage.server import storage_index_to_dir
106 
107 MiB = 1024*1024
108 
109hunk ./src/allmydata/test/test_upload.py 91
110 class ServerError(Exception):
111     pass
112 
113+class SetDEPMixin:
114+    def set_encoding_parameters(self, k, happy, n, max_segsize=1*MiB):
115+        p = {"k": k,
116+             "happy": happy,
117+             "n": n,
118+             "max_segment_size": max_segsize,
119+             }
120+        self.node.DEFAULT_ENCODING_PARAMETERS = p
121+
122 class FakeStorageServer:
123     def __init__(self, mode):
124         self.mode = mode
125hunk ./src/allmydata/test/test_upload.py 247
126     u = upload.FileHandle(fh, convergence=None)
127     return uploader.upload(u)
128 
129-class GoodServer(unittest.TestCase, ShouldFailMixin):
130+class GoodServer(unittest.TestCase, ShouldFailMixin, SetDEPMixin):
131     def setUp(self):
132         self.node = FakeClient(mode="good")
133         self.u = upload.Uploader()
134hunk ./src/allmydata/test/test_upload.py 254
135         self.u.running = True
136         self.u.parent = self.node
137 
138-    def set_encoding_parameters(self, k, happy, n, max_segsize=1*MiB):
139-        p = {"k": k,
140-             "happy": happy,
141-             "n": n,
142-             "max_segment_size": max_segsize,
143-             }
144-        self.node.DEFAULT_ENCODING_PARAMETERS = p
145-
146     def _check_small(self, newuri, size):
147         u = uri.from_string(newuri)
148         self.failUnless(isinstance(u, uri.LiteralFileURI))
149hunk ./src/allmydata/test/test_upload.py 377
150         d.addCallback(self._check_large, SIZE_LARGE)
151         return d
152 
153-class ServerErrors(unittest.TestCase, ShouldFailMixin):
154+class ServerErrors(unittest.TestCase, ShouldFailMixin, SetDEPMixin):
155     def make_node(self, mode, num_servers=10):
156         self.node = FakeClient(mode, num_servers)
157         self.u = upload.Uploader()
158hunk ./src/allmydata/test/test_upload.py 677
159         d.addCallback(_done)
160         return d
161 
162-class EncodingParameters(GridTestMixin, unittest.TestCase):
163+class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
164+    ShouldFailMixin):
165+    def _do_upload_with_broken_servers(self, servers_to_break):
166+        """
167+        I act like a normal upload, but before I send the results of
168+        Tahoe2PeerSelector to the Encoder, I break the first servers_to_break
169+        PeerTrackers in the used_peers part of the return result.
170+        """
171+        assert self.g, "I tried to find a grid at self.g, but failed"
172+        broker = self.g.clients[0].storage_broker
173+        sh     = self.g.clients[0]._secret_holder
174+        data = upload.Data("data" * 10000, convergence="")
175+        data.encoding_param_k = 3
176+        data.encoding_param_happy = 4
177+        data.encoding_param_n = 10
178+        uploadable = upload.EncryptAnUploadable(data)
179+        encoder = encode.Encoder()
180+        encoder.set_encrypted_uploadable(uploadable)
181+        status = upload.UploadStatus()
182+        selector = upload.Tahoe2PeerSelector("dglev", "test", status)
183+        storage_index = encoder.get_param("storage_index")
184+        share_size = encoder.get_param("share_size")
185+        block_size = encoder.get_param("block_size")
186+        num_segments = encoder.get_param("num_segments")
187+        d = selector.get_shareholders(broker, sh, storage_index,
188+                                      share_size, block_size, num_segments,
189+                                      10, 4)
190+        def _have_shareholders((used_peers, already_peers)):
191+            assert servers_to_break <= len(used_peers)
192+            for index in xrange(servers_to_break):
193+                server = list(used_peers)[index]
194+                for share in server.buckets.keys():
195+                    server.buckets[share].abort()
196+            buckets = {}
197+            for peer in used_peers:
198+                buckets.update(peer.buckets)
199+            encoder.set_shareholders(buckets)
200+            d = encoder.start()
201+            return d
202+        d.addCallback(_have_shareholders)
203+        return d
204+
205+    def _add_server_with_share(self, server_number, share_number=None,
206+                               readonly=False):
207+        assert self.g, "I tried to find a grid at self.g, but failed"
208+        assert self.shares, "I tried to find shares at self.shares, but failed"
209+        ss = self.g.make_server(server_number, readonly)
210+        self.g.add_server(server_number, ss)
211+        if share_number:
212+            # Copy share i from the directory associated with the first
213+            # storage server to the directory associated with this one.
214+            old_share_location = self.shares[share_number][2]
215+            new_share_location = os.path.join(ss.storedir, "shares")
216+            si = uri.from_string(self.uri).get_storage_index()
217+            new_share_location = os.path.join(new_share_location,
218+                                              storage_index_to_dir(si))
219+            if not os.path.exists(new_share_location):
220+                os.makedirs(new_share_location)
221+            new_share_location = os.path.join(new_share_location,
222+                                              str(share_number))
223+            shutil.copy(old_share_location, new_share_location)
224+            shares = self.find_shares(self.uri)
225+            # Make sure that the storage server has the share.
226+            self.failUnless((share_number, ss.my_nodeid, new_share_location)
227+                            in shares)
228+
229+    def _setup_and_upload(self):
230+        """
231+        I set up a NoNetworkGrid with a single server and client,
232+        upload a file to it, store its uri in self.uri, and store its
233+        sharedata in self.shares.
234+        """
235+        self.set_up_grid(num_clients=1, num_servers=1)
236+        client = self.g.clients[0]
237+        client.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
238+        data = upload.Data("data" * 10000, convergence="")
239+        self.data = data
240+        d = client.upload(data)
241+        def _store_uri(ur):
242+            self.uri = ur.uri
243+        d.addCallback(_store_uri)
244+        d.addCallback(lambda ign:
245+            self.find_shares(self.uri))
246+        def _store_shares(shares):
247+            self.shares = shares
248+        d.addCallback(_store_shares)
249+        return d
250+
251     def test_configure_parameters(self):
252         self.basedir = self.mktemp()
253         hooks = {0: self._set_up_nodes_extra_config}
254hunk ./src/allmydata/test/test_upload.py 784
255         d.addCallback(_check)
256         return d
257 
258+    def _setUp(self, ns):
259+        # Used by test_happy_semantics and test_prexisting_share_behavior
260+        # to set up the grid.
261+        self.node = FakeClient(mode="good", num_servers=ns)
262+        self.u = upload.Uploader()
263+        self.u.running = True
264+        self.u.parent = self.node
265+
266+    def test_happy_semantics(self):
267+        self._setUp(2)
268+        DATA = upload.Data("kittens" * 10000, convergence="")
269+        # These parameters are unsatisfiable with the client that we've made
270+        # -- we'll use them to test that the semnatics work correctly.
271+        self.set_encoding_parameters(k=3, happy=5, n=10)
272+        d = self.shouldFail(NotEnoughSharesError, "test_happy_semantics",
273+                            "shares could only be placed on 2 servers "
274+                            "(5 were requested)",
275+                            self.u.upload, DATA)
276+        # Let's reset the client to have 10 servers
277+        d.addCallback(lambda ign:
278+            self._setUp(10))
279+        # These parameters are satisfiable with the client we've made.
280+        d.addCallback(lambda ign:
281+            self.set_encoding_parameters(k=3, happy=5, n=10))
282+        # this should work
283+        d.addCallback(lambda ign:
284+            self.u.upload(DATA))
285+        # Let's reset the client to have 7 servers
286+        # (this is less than n, but more than h)
287+        d.addCallback(lambda ign:
288+            self._setUp(7))
289+        # These encoding parameters should still be satisfiable with our
290+        # client setup
291+        d.addCallback(lambda ign:
292+            self.set_encoding_parameters(k=3, happy=5, n=10))
293+        # This, then, should work.
294+        d.addCallback(lambda ign:
295+            self.u.upload(DATA))
296+        return d
297+
298+    def test_problem_layouts(self):
299+        self.basedir = self.mktemp()
300+        # This scenario is at
301+        # http://allmydata.org/trac/tahoe/ticket/778#comment:52
302+        #
303+        # The scenario in comment:52 proposes that we have a layout
304+        # like:
305+        # server 1: share 1
306+        # server 2: share 1
307+        # server 3: share 1
308+        # server 4: shares 2 - 10
309+        # To get access to the shares, we will first upload to one
310+        # server, which will then have shares 1 - 10. We'll then
311+        # add three new servers, configure them to not accept any new
312+        # shares, then write share 1 directly into the serverdir of each.
313+        # Then each of servers 1 - 3 will report that they have share 1,
314+        # and will not accept any new share, while server 4 will report that
315+        # it has shares 2 - 10 and will accept new shares.
316+        # We'll then set 'happy' = 4, and see that an upload fails
317+        # (as it should)
318+        d = self._setup_and_upload()
319+        d.addCallback(lambda ign:
320+            self._add_server_with_share(1, 0, True))
321+        d.addCallback(lambda ign:
322+            self._add_server_with_share(2, 0, True))
323+        d.addCallback(lambda ign:
324+            self._add_server_with_share(3, 0, True))
325+        # Remove the first share from server 0.
326+        def _remove_share_0():
327+            share_location = self.shares[0][2]
328+            os.remove(share_location)
329+        d.addCallback(lambda ign:
330+            _remove_share_0())
331+        # Set happy = 4 in the client.
332+        def _prepare():
333+            client = self.g.clients[0]
334+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
335+            return client
336+        d.addCallback(lambda ign:
337+            _prepare())
338+        # Uploading data should fail
339+        d.addCallback(lambda client:
340+            self.shouldFail(NotEnoughSharesError, "test_happy_semantics",
341+                            "shares could only be placed on 1 servers "
342+                            "(4 were requested)",
343+                            client.upload, upload.Data("data" * 10000,
344+                                                       convergence="")))
345+
346+
347+        # This scenario is at
348+        # http://allmydata.org/trac/tahoe/ticket/778#comment:53
349+        #
350+        # Set up the grid to have one server
351+        def _change_basedir(ign):
352+            self.basedir = self.mktemp()
353+        d.addCallback(_change_basedir)
354+        d.addCallback(lambda ign:
355+            self._setup_and_upload())
356+        # We want to have a layout like this:
357+        # server 1: share 1
358+        # server 2: share 2
359+        # server 3: share 3
360+        # server 4: shares 1 - 10
361+        # (this is an expansion of Zooko's example because it is easier
362+        #  to code, but it will fail in the same way)
363+        # To start, we'll create a server with shares 1-10 of the data
364+        # we're about to upload.
365+        # Next, we'll add three new servers to our NoNetworkGrid. We'll add
366+        # one share from our initial upload to each of these.
367+        # The counterintuitive ordering of the share numbers is to deal with
368+        # the permuting of these servers -- distributing the shares this
369+        # way ensures that the Tahoe2PeerSelector sees them in the order
370+        # described above.
371+        d.addCallback(lambda ign:
372+            self._add_server_with_share(server_number=1, share_number=2))
373+        d.addCallback(lambda ign:
374+            self._add_server_with_share(server_number=2, share_number=0))
375+        d.addCallback(lambda ign:
376+            self._add_server_with_share(server_number=3, share_number=1))
377+        # So, we now have the following layout:
378+        # server 0: shares 1 - 10
379+        # server 1: share 0
380+        # server 2: share 1
381+        # server 3: share 2
382+        # We want to change the 'happy' parameter in the client to 4.
383+        # We then want to feed the upload process a list of peers that
384+        # server 0 is at the front of, so we trigger Zooko's scenario.
385+        # Ideally, a reupload of our original data should work.
386+        def _reset_encoding_parameters(ign):
387+            client = self.g.clients[0]
388+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
389+            return client
390+        d.addCallback(_reset_encoding_parameters)
391+        # We need this to get around the fact that the old Data
392+        # instance already has a happy parameter set.
393+        d.addCallback(lambda client:
394+            client.upload(upload.Data("data" * 10000, convergence="")))
395+        return d
396+
397+
398+    def test_dropped_servers_in_encoder(self):
399+        def _set_basedir(ign=None):
400+            self.basedir = self.mktemp()
401+        _set_basedir()
402+        d = self._setup_and_upload();
403+        # Add 5 servers, with one share each from the original
404+        # Add a readonly server
405+        def _do_server_setup(ign):
406+            self._add_server_with_share(1, 1, True)
407+            self._add_server_with_share(2)
408+            self._add_server_with_share(3)
409+            self._add_server_with_share(4)
410+            self._add_server_with_share(5)
411+        d.addCallback(_do_server_setup)
412+        # remove the original server
413+        # (necessary to ensure that the Tahoe2PeerSelector will distribute
414+        #  all the shares)
415+        def _remove_server(ign):
416+            server = self.g.servers_by_number[0]
417+            self.g.remove_server(server.my_nodeid)
418+        d.addCallback(_remove_server)
419+        # This should succeed.
420+        d.addCallback(lambda ign:
421+            self._do_upload_with_broken_servers(1))
422+        # Now, do the same thing over again, but drop 2 servers instead
423+        # of 1. This should fail.
424+        d.addCallback(_set_basedir)
425+        d.addCallback(lambda ign:
426+            self._setup_and_upload())
427+        d.addCallback(_do_server_setup)
428+        d.addCallback(_remove_server)
429+        d.addCallback(lambda ign:
430+            self.shouldFail(NotEnoughSharesError,
431+                            "test_dropped_server_in_encoder", "",
432+                            self._do_upload_with_broken_servers, 2))
433+        return d
434+
435+
436+    def test_servers_with_unique_shares(self):
437+        # servers_with_unique_shares expects a dict of
438+        # shnum => peerid as a preexisting shares argument.
439+        test1 = {
440+                 1 : "server1",
441+                 2 : "server2",
442+                 3 : "server3",
443+                 4 : "server4"
444+                }
445+        unique_servers = upload.servers_with_unique_shares(test1)
446+        self.failUnlessEqual(4, len(unique_servers))
447+        for server in ["server1", "server2", "server3", "server4"]:
448+            self.failUnlessIn(server, unique_servers)
449+        test1[4] = "server1"
450+        # Now there should only be 3 unique servers.
451+        unique_servers = upload.servers_with_unique_shares(test1)
452+        self.failUnlessEqual(3, len(unique_servers))
453+        for server in ["server1", "server2", "server3"]:
454+            self.failUnlessIn(server, unique_servers)
455+        # servers_with_unique_shares expects a set of PeerTracker
456+        # instances as a used_peers argument, but only uses the peerid
457+        # instance variable to assess uniqueness. So we feed it some fake
458+        # PeerTrackers whose only important characteristic is that they
459+        # have peerid set to something.
460+        class FakePeerTracker:
461+            pass
462+        trackers = []
463+        for server in ["server5", "server6", "server7", "server8"]:
464+            t = FakePeerTracker()
465+            t.peerid = server
466+            trackers.append(t)
467+        # Recall that there are 3 unique servers in test1. Since none of
468+        # those overlap with the ones in trackers, we should get 7 back
469+        unique_servers = upload.servers_with_unique_shares(test1, set(trackers))
470+        self.failUnlessEqual(7, len(unique_servers))
471+        expected_servers = ["server" + str(i) for i in xrange(1, 9)]
472+        expected_servers.remove("server4")
473+        for server in expected_servers:
474+            self.failUnlessIn(server, unique_servers)
475+        # Now add an overlapping server to trackers.
476+        t = FakePeerTracker()
477+        t.peerid = "server1"
478+        trackers.append(t)
479+        unique_servers = upload.servers_with_unique_shares(test1, set(trackers))
480+        self.failUnlessEqual(7, len(unique_servers))
481+        for server in expected_servers:
482+            self.failUnlessIn(server, unique_servers)
483+
484+
485     def _set_up_nodes_extra_config(self, clientdir):
486         cfgfn = os.path.join(clientdir, "tahoe.cfg")
487         oldcfg = open(cfgfn, "r").read()
488}
489[Alter tests to use the new form of set_shareholders
490Kevan Carstensen <kevan@isnotajoke.com>**20091104033602
491 Ignore-this: 3deac11fc831618d11441317463ef830
492] {
493hunk ./src/allmydata/test/test_encode.py 301
494                      (NUM_SEGMENTS-1)*segsize, len(data), NUM_SEGMENTS*segsize)
495 
496             shareholders = {}
497+            servermap = {}
498             for shnum in range(NUM_SHARES):
499                 peer = FakeBucketReaderWriterProxy()
500                 shareholders[shnum] = peer
501hunk ./src/allmydata/test/test_encode.py 305
502+                servermap[shnum] = str(shnum)
503                 all_shareholders.append(peer)
504hunk ./src/allmydata/test/test_encode.py 307
505-            e.set_shareholders(shareholders)
506+            e.set_shareholders(shareholders, servermap)
507             return e.start()
508         d.addCallback(_ready)
509 
510merger 0.0 (
511hunk ./src/allmydata/test/test_encode.py 462
512-            all_peers = []
513hunk ./src/allmydata/test/test_encode.py 463
514+            servermap = {}
515)
516hunk ./src/allmydata/test/test_encode.py 467
517                 mode = bucket_modes.get(shnum, "good")
518                 peer = FakeBucketReaderWriterProxy(mode)
519                 shareholders[shnum] = peer
520-            e.set_shareholders(shareholders)
521+                servermap[shnum] = str(shnum)
522+            e.set_shareholders(shareholders, servermap)
523             return e.start()
524         d.addCallback(_ready)
525         def _sent(res):
526hunk ./src/allmydata/test/test_upload.py 711
527                 for share in server.buckets.keys():
528                     server.buckets[share].abort()
529             buckets = {}
530+            servermap = already_peers.copy()
531             for peer in used_peers:
532                 buckets.update(peer.buckets)
533hunk ./src/allmydata/test/test_upload.py 714
534-            encoder.set_shareholders(buckets)
535+                for bucket in peer.buckets:
536+                    servermap[bucket] = peer.peerid
537+            encoder.set_shareholders(buckets, servermap)
538             d = encoder.start()
539             return d
540         d.addCallback(_have_shareholders)
541hunk ./src/allmydata/test/test_upload.py 933
542         _set_basedir()
543         d = self._setup_and_upload();
544         # Add 5 servers, with one share each from the original
545-        # Add a readonly server
546         def _do_server_setup(ign):
547             self._add_server_with_share(1, 1, True)
548             self._add_server_with_share(2)
549}
550[Minor tweak to an existing test -- make the first server read-write, instead of read-only
551Kevan Carstensen <kevan@isnotajoke.com>**20091104034232
552 Ignore-this: a951a46c93f7f58dd44d93d8623b2aee
553] hunk ./src/allmydata/test/test_upload.py 934
554         d = self._setup_and_upload();
555         # Add 5 servers, with one share each from the original
556         def _do_server_setup(ign):
557-            self._add_server_with_share(1, 1, True)
558+            self._add_server_with_share(1, 1)
559             self._add_server_with_share(2)
560             self._add_server_with_share(3)
561             self._add_server_with_share(4)
562[Add a test for upload.shares_by_server
563Kevan Carstensen <kevan@isnotajoke.com>**20091104111324
564 Ignore-this: f9802e82d6982a93e00f92e0b276f018
565] hunk ./src/allmydata/test/test_upload.py 1013
566             self.failUnlessIn(server, unique_servers)
567 
568 
569+    def test_shares_by_server(self):
570+        test = {
571+                    1 : "server1",
572+                    2 : "server2",
573+                    3 : "server3",
574+                    4 : "server4"
575+               }
576+        shares_by_server = upload.shares_by_server(test)
577+        self.failUnlessEqual(set([1]), shares_by_server["server1"])
578+        self.failUnlessEqual(set([2]), shares_by_server["server2"])
579+        self.failUnlessEqual(set([3]), shares_by_server["server3"])
580+        self.failUnlessEqual(set([4]), shares_by_server["server4"])
581+        test1 = {
582+                    1 : "server1",
583+                    2 : "server1",
584+                    3 : "server1",
585+                    4 : "server2",
586+                    5 : "server2"
587+                }
588+        shares_by_server = upload.shares_by_server(test1)
589+        self.failUnlessEqual(set([1, 2, 3]), shares_by_server["server1"])
590+        self.failUnlessEqual(set([4, 5]), shares_by_server["server2"])
591+
592+
593     def _set_up_nodes_extra_config(self, clientdir):
594         cfgfn = os.path.join(clientdir, "tahoe.cfg")
595         oldcfg = open(cfgfn, "r").read()
596[Add more tests for comment:53 in ticket #778
597Kevan Carstensen <kevan@isnotajoke.com>**20091104112849
598 Ignore-this: 3bb2edd299a944cc9586e14d5d83ec8c
599] {
600hunk ./src/allmydata/test/test_upload.py 722
601         d.addCallback(_have_shareholders)
602         return d
603 
604-    def _add_server_with_share(self, server_number, share_number=None,
605-                               readonly=False):
606+    def _add_server(self, server_number, readonly=False):
607         assert self.g, "I tried to find a grid at self.g, but failed"
608         assert self.shares, "I tried to find shares at self.shares, but failed"
609         ss = self.g.make_server(server_number, readonly)
610hunk ./src/allmydata/test/test_upload.py 727
611         self.g.add_server(server_number, ss)
612+
613+    def _add_server_with_share(self, server_number, share_number=None,
614+                               readonly=False):
615+        self._add_server(server_number, readonly)
616         if share_number:
617hunk ./src/allmydata/test/test_upload.py 732
618-            # Copy share i from the directory associated with the first
619-            # storage server to the directory associated with this one.
620-            old_share_location = self.shares[share_number][2]
621-            new_share_location = os.path.join(ss.storedir, "shares")
622-            si = uri.from_string(self.uri).get_storage_index()
623-            new_share_location = os.path.join(new_share_location,
624-                                              storage_index_to_dir(si))
625-            if not os.path.exists(new_share_location):
626-                os.makedirs(new_share_location)
627-            new_share_location = os.path.join(new_share_location,
628-                                              str(share_number))
629-            shutil.copy(old_share_location, new_share_location)
630-            shares = self.find_shares(self.uri)
631-            # Make sure that the storage server has the share.
632-            self.failUnless((share_number, ss.my_nodeid, new_share_location)
633-                            in shares)
634+            self._copy_share_to_server(share_number, server_number)
635+
636+    def _copy_share_to_server(self, share_number, server_number):
637+        ss = self.g.servers_by_number[server_number]
638+        # Copy share i from the directory associated with the first
639+        # storage server to the directory associated with this one.
640+        assert self.g, "I tried to find a grid at self.g, but failed"
641+        assert self.shares, "I tried to find shares at self.shares, but failed"
642+        old_share_location = self.shares[share_number][2]
643+        new_share_location = os.path.join(ss.storedir, "shares")
644+        si = uri.from_string(self.uri).get_storage_index()
645+        new_share_location = os.path.join(new_share_location,
646+                                          storage_index_to_dir(si))
647+        if not os.path.exists(new_share_location):
648+            os.makedirs(new_share_location)
649+        new_share_location = os.path.join(new_share_location,
650+                                          str(share_number))
651+        shutil.copy(old_share_location, new_share_location)
652+        shares = self.find_shares(self.uri)
653+        # Make sure that the storage server has the share.
654+        self.failUnless((share_number, ss.my_nodeid, new_share_location)
655+                        in shares)
656+
657 
658     def _setup_and_upload(self):
659         """
660hunk ./src/allmydata/test/test_upload.py 917
661         d.addCallback(lambda ign:
662             self._add_server_with_share(server_number=3, share_number=1))
663         # So, we now have the following layout:
664-        # server 0: shares 1 - 10
665+        # server 0: shares 0 - 9
666         # server 1: share 0
667         # server 2: share 1
668         # server 3: share 2
669hunk ./src/allmydata/test/test_upload.py 934
670         # instance already has a happy parameter set.
671         d.addCallback(lambda client:
672             client.upload(upload.Data("data" * 10000, convergence="")))
673+
674+
675+        # This scenario is basically comment:53, but with the order reversed;
676+        # this means that the Tahoe2PeerSelector sees
677+        # server 0: shares 1-10
678+        # server 1: share 1
679+        # server 2: share 2
680+        # server 3: share 3
681+        d.addCallback(_change_basedir)
682+        d.addCallback(lambda ign:
683+            self._setup_and_upload())
684+        d.addCallback(lambda ign:
685+            self._add_server_with_share(server_number=2, share_number=0))
686+        d.addCallback(lambda ign:
687+            self._add_server_with_share(server_number=3, share_number=1))
688+        d.addCallback(lambda ign:
689+            self._add_server_with_share(server_number=1, share_number=2))
690+        # Copy all of the other shares to server number 2
691+        def _copy_shares(ign):
692+            for i in xrange(1, 10):
693+                self._copy_share_to_server(i, 2)
694+        d.addCallback(_copy_shares)
695+        # Remove the first server, and add a placeholder with share 0
696+        d.addCallback(lambda ign:
697+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
698+        d.addCallback(lambda ign:
699+            self._add_server_with_share(server_number=0, share_number=0))
700+        # Now try uploading.
701+        d.addCallback(_reset_encoding_parameters)
702+        d.addCallback(lambda client:
703+            client.upload(upload.Data("data" * 10000, convergence="")))
704+        # Try the same thing, but with empty servers after the first one
705+        # We want to make sure that Tahoe2PeerSelector will redistribute
706+        # shares as necessary, not simply discover an existing layout.
707+        d.addCallback(_change_basedir)
708+        d.addCallback(lambda ign:
709+            self._setup_and_upload())
710+        d.addCallback(lambda ign:
711+            self._add_server(server_number=2))
712+        d.addCallback(lambda ign:
713+            self._add_server(server_number=3))
714+        d.addCallback(lambda ign:
715+            self._add_server(server_number=1))
716+        d.addCallback(_copy_shares)
717+        d.addCallback(lambda ign:
718+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
719+        d.addCallback(lambda ign:
720+            self._add_server(server_number=0))
721+        d.addCallback(_reset_encoding_parameters)
722+        d.addCallback(lambda client:
723+            client.upload(upload.Data("data" * 10000, convergence="")))
724+        # Try the following layout
725+        # server 0: shares 1-10
726+        # server 1: share 1, read-only
727+        # server 2: share 2, read-only
728+        # server 3: share 3, read-only
729+        d.addCallback(_change_basedir)
730+        d.addCallback(lambda ign:
731+            self._setup_and_upload())
732+        d.addCallback(lambda ign:
733+            self._add_server_with_share(server_number=2, share_number=0))
734+        d.addCallback(lambda ign:
735+            self._add_server_with_share(server_number=3, share_number=1,
736+                                        readonly=True))
737+        d.addCallback(lambda ign:
738+            self._add_server_with_share(server_number=1, share_number=2,
739+                                        readonly=True))
740+        # Copy all of the other shares to server number 2
741+        d.addCallback(_copy_shares)
742+        # Remove server 0, and add another in its place
743+        d.addCallback(lambda ign:
744+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
745+        d.addCallback(lambda ign:
746+            self._add_server_with_share(server_number=0, share_number=0,
747+                                        readonly=True))
748+        d.addCallback(_reset_encoding_parameters)
749+        d.addCallback(lambda client:
750+            client.upload(upload.Data("data" * 10000, convergence="")))
751         return d
752 
753 
754}
755[Test Tahoe2PeerSelector to make sure that it recognizeses existing shares on readonly servers
756Kevan Carstensen <kevan@isnotajoke.com>**20091109003735
757 Ignore-this: 12f9b4cff5752fca7ed32a6ebcff6446
758] hunk ./src/allmydata/test/test_upload.py 1125
759         self.failUnlessEqual(set([4, 5]), shares_by_server["server2"])
760 
761 
762+    def test_existing_share_detection(self):
763+        self.basedir = self.mktemp()
764+        d = self._setup_and_upload()
765+        # Our final setup should look like this:
766+        # server 1: shares 1 - 10, read-only
767+        # server 2: empty
768+        # server 3: empty
769+        # server 4: empty
770+        # The purpose of this test is to make sure that the peer selector
771+        # knows about the shares on server 1, even though it is read-only.
772+        # It used to simply filter these out, which would cause the test
773+        # to fail when servers_of_happiness = 4.
774+        d.addCallback(lambda ign:
775+            self._add_server_with_share(1, 0, True))
776+        d.addCallback(lambda ign:
777+            self._add_server_with_share(2))
778+        d.addCallback(lambda ign:
779+            self._add_server_with_share(3))
780+        d.addCallback(lambda ign:
781+            self._add_server_with_share(4))
782+        def _copy_shares(ign):
783+            for i in xrange(1, 10):
784+                self._copy_share_to_server(i, 1)
785+        d.addCallback(_copy_shares)
786+        d.addCallback(lambda ign:
787+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
788+        def _prepare_client(ign):
789+            client = self.g.clients[0]
790+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
791+            return client
792+        d.addCallback(_prepare_client)
793+        d.addCallback(lambda client:
794+            client.upload(upload.Data("data" * 10000, convergence="")))
795+        return d
796+
797+
798     def _set_up_nodes_extra_config(self, clientdir):
799         cfgfn = os.path.join(clientdir, "tahoe.cfg")
800         oldcfg = open(cfgfn, "r").read()
801[Re-work 'test_upload.py' to be more readable; add more tests for #778
802Kevan Carstensen <kevan@isnotajoke.com>**20091116192334
803 Ignore-this: 7e8565f92fe51dece5ae28daf442d659
804] {
805hunk ./src/allmydata/test/test_upload.py 722
806         d.addCallback(_have_shareholders)
807         return d
808 
809+
810     def _add_server(self, server_number, readonly=False):
811         assert self.g, "I tried to find a grid at self.g, but failed"
812         assert self.shares, "I tried to find shares at self.shares, but failed"
813hunk ./src/allmydata/test/test_upload.py 729
814         ss = self.g.make_server(server_number, readonly)
815         self.g.add_server(server_number, ss)
816 
817+
818     def _add_server_with_share(self, server_number, share_number=None,
819                                readonly=False):
820         self._add_server(server_number, readonly)
821hunk ./src/allmydata/test/test_upload.py 733
822-        if share_number:
823+        if share_number is not None:
824             self._copy_share_to_server(share_number, server_number)
825 
826hunk ./src/allmydata/test/test_upload.py 736
827+
828     def _copy_share_to_server(self, share_number, server_number):
829         ss = self.g.servers_by_number[server_number]
830         # Copy share i from the directory associated with the first
831hunk ./src/allmydata/test/test_upload.py 752
832             os.makedirs(new_share_location)
833         new_share_location = os.path.join(new_share_location,
834                                           str(share_number))
835-        shutil.copy(old_share_location, new_share_location)
836+        if old_share_location != new_share_location:
837+            shutil.copy(old_share_location, new_share_location)
838         shares = self.find_shares(self.uri)
839         # Make sure that the storage server has the share.
840         self.failUnless((share_number, ss.my_nodeid, new_share_location)
841hunk ./src/allmydata/test/test_upload.py 782
842         d.addCallback(_store_shares)
843         return d
844 
845+
846     def test_configure_parameters(self):
847         self.basedir = self.mktemp()
848         hooks = {0: self._set_up_nodes_extra_config}
849hunk ./src/allmydata/test/test_upload.py 802
850         d.addCallback(_check)
851         return d
852 
853+
854     def _setUp(self, ns):
855         # Used by test_happy_semantics and test_prexisting_share_behavior
856         # to set up the grid.
857hunk ./src/allmydata/test/test_upload.py 811
858         self.u.running = True
859         self.u.parent = self.node
860 
861+
862     def test_happy_semantics(self):
863         self._setUp(2)
864         DATA = upload.Data("kittens" * 10000, convergence="")
865hunk ./src/allmydata/test/test_upload.py 844
866             self.u.upload(DATA))
867         return d
868 
869-    def test_problem_layouts(self):
870-        self.basedir = self.mktemp()
871+
872+    def test_problem_layout_comment_52(self):
873+        def _basedir():
874+            self.basedir = self.mktemp()
875+        _basedir()
876         # This scenario is at
877         # http://allmydata.org/trac/tahoe/ticket/778#comment:52
878         #
879hunk ./src/allmydata/test/test_upload.py 890
880         # Uploading data should fail
881         d.addCallback(lambda client:
882             self.shouldFail(NotEnoughSharesError, "test_happy_semantics",
883-                            "shares could only be placed on 1 servers "
884+                            "shares could only be placed on 2 servers "
885                             "(4 were requested)",
886                             client.upload, upload.Data("data" * 10000,
887                                                        convergence="")))
888hunk ./src/allmydata/test/test_upload.py 895
889 
890+        # Do comment:52, but like this:
891+        # server 2: empty
892+        # server 3: share 0, read-only
893+        # server 1: share 0, read-only
894+        # server 0: shares 0-9
895+        d.addCallback(lambda ign:
896+            _basedir())
897+        d.addCallback(lambda ign:
898+            self._setup_and_upload())
899+        d.addCallback(lambda ign:
900+            self._add_server_with_share(server_number=2))
901+        d.addCallback(lambda ign:
902+            self._add_server_with_share(server_number=3, share_number=0,
903+                                        readonly=True))
904+        d.addCallback(lambda ign:
905+            self._add_server_with_share(server_number=1, share_number=0,
906+                                        readonly=True))
907+        def _prepare2():
908+            client = self.g.clients[0]
909+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 3
910+            return client
911+        d.addCallback(lambda ign:
912+            _prepare2())
913+        d.addCallback(lambda client:
914+            self.shouldFail(NotEnoughSharesError, "test_happy_sematics",
915+                            "shares could only be placed on 2 servers "
916+                            "(3 were requested)",
917+                            client.upload, upload.Data("data" * 10000,
918+                                                       convergence="")))
919+        return d
920+
921 
922hunk ./src/allmydata/test/test_upload.py 927
923+    def test_problem_layout_comment_53(self):
924         # This scenario is at
925         # http://allmydata.org/trac/tahoe/ticket/778#comment:53
926         #
927hunk ./src/allmydata/test/test_upload.py 934
928         # Set up the grid to have one server
929         def _change_basedir(ign):
930             self.basedir = self.mktemp()
931-        d.addCallback(_change_basedir)
932-        d.addCallback(lambda ign:
933-            self._setup_and_upload())
934-        # We want to have a layout like this:
935-        # server 1: share 1
936-        # server 2: share 2
937-        # server 3: share 3
938-        # server 4: shares 1 - 10
939-        # (this is an expansion of Zooko's example because it is easier
940-        #  to code, but it will fail in the same way)
941-        # To start, we'll create a server with shares 1-10 of the data
942-        # we're about to upload.
943+        _change_basedir(None)
944+        d = self._setup_and_upload()
945+        # We start by uploading all of the shares to one server (which has
946+        # already been done above).
947         # Next, we'll add three new servers to our NoNetworkGrid. We'll add
948         # one share from our initial upload to each of these.
949         # The counterintuitive ordering of the share numbers is to deal with
950hunk ./src/allmydata/test/test_upload.py 952
951             self._add_server_with_share(server_number=3, share_number=1))
952         # So, we now have the following layout:
953         # server 0: shares 0 - 9
954-        # server 1: share 0
955-        # server 2: share 1
956-        # server 3: share 2
957+        # server 1: share 2
958+        # server 2: share 0
959+        # server 3: share 1
960         # We want to change the 'happy' parameter in the client to 4.
961hunk ./src/allmydata/test/test_upload.py 956
962-        # We then want to feed the upload process a list of peers that
963-        # server 0 is at the front of, so we trigger Zooko's scenario.
964+        # The Tahoe2PeerSelector will see the peers permuted as:
965+        # 2, 3, 1, 0
966         # Ideally, a reupload of our original data should work.
967hunk ./src/allmydata/test/test_upload.py 959
968-        def _reset_encoding_parameters(ign):
969+        def _reset_encoding_parameters(ign, happy=4):
970             client = self.g.clients[0]
971hunk ./src/allmydata/test/test_upload.py 961
972-            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
973+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
974             return client
975         d.addCallback(_reset_encoding_parameters)
976hunk ./src/allmydata/test/test_upload.py 964
977-        # We need this to get around the fact that the old Data
978-        # instance already has a happy parameter set.
979         d.addCallback(lambda client:
980             client.upload(upload.Data("data" * 10000, convergence="")))
981 
982hunk ./src/allmydata/test/test_upload.py 970
983 
984         # This scenario is basically comment:53, but with the order reversed;
985         # this means that the Tahoe2PeerSelector sees
986-        # server 0: shares 1-10
987-        # server 1: share 1
988-        # server 2: share 2
989-        # server 3: share 3
990+        # server 2: shares 1-10
991+        # server 3: share 1
992+        # server 1: share 2
993+        # server 4: share 3
994         d.addCallback(_change_basedir)
995         d.addCallback(lambda ign:
996             self._setup_and_upload())
997hunk ./src/allmydata/test/test_upload.py 992
998         d.addCallback(lambda ign:
999             self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1000         d.addCallback(lambda ign:
1001-            self._add_server_with_share(server_number=0, share_number=0))
1002+            self._add_server_with_share(server_number=4, share_number=0))
1003         # Now try uploading.
1004         d.addCallback(_reset_encoding_parameters)
1005         d.addCallback(lambda client:
1006hunk ./src/allmydata/test/test_upload.py 1013
1007         d.addCallback(lambda ign:
1008             self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1009         d.addCallback(lambda ign:
1010-            self._add_server(server_number=0))
1011+            self._add_server(server_number=4))
1012         d.addCallback(_reset_encoding_parameters)
1013         d.addCallback(lambda client:
1014             client.upload(upload.Data("data" * 10000, convergence="")))
1015hunk ./src/allmydata/test/test_upload.py 1017
1016+        return d
1017+
1018+
1019+    def test_happiness_with_some_readonly_peers(self):
1020         # Try the following layout
1021hunk ./src/allmydata/test/test_upload.py 1022
1022-        # server 0: shares 1-10
1023-        # server 1: share 1, read-only
1024-        # server 2: share 2, read-only
1025-        # server 3: share 3, read-only
1026-        d.addCallback(_change_basedir)
1027-        d.addCallback(lambda ign:
1028-            self._setup_and_upload())
1029+        # server 2: shares 0-9
1030+        # server 4: share 0, read-only
1031+        # server 3: share 1, read-only
1032+        # server 1: share 2, read-only
1033+        self.basedir = self.mktemp()
1034+        d = self._setup_and_upload()
1035         d.addCallback(lambda ign:
1036             self._add_server_with_share(server_number=2, share_number=0))
1037         d.addCallback(lambda ign:
1038hunk ./src/allmydata/test/test_upload.py 1037
1039             self._add_server_with_share(server_number=1, share_number=2,
1040                                         readonly=True))
1041         # Copy all of the other shares to server number 2
1042+        def _copy_shares(ign):
1043+            for i in xrange(1, 10):
1044+                self._copy_share_to_server(i, 2)
1045         d.addCallback(_copy_shares)
1046         # Remove server 0, and add another in its place
1047         d.addCallback(lambda ign:
1048hunk ./src/allmydata/test/test_upload.py 1045
1049             self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1050         d.addCallback(lambda ign:
1051-            self._add_server_with_share(server_number=0, share_number=0,
1052+            self._add_server_with_share(server_number=4, share_number=0,
1053                                         readonly=True))
1054hunk ./src/allmydata/test/test_upload.py 1047
1055+        def _reset_encoding_parameters(ign, happy=4):
1056+            client = self.g.clients[0]
1057+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
1058+            return client
1059+        d.addCallback(_reset_encoding_parameters)
1060+        d.addCallback(lambda client:
1061+            client.upload(upload.Data("data" * 10000, convergence="")))
1062+        return d
1063+
1064+
1065+    def test_happiness_with_all_readonly_peers(self):
1066+        # server 3: share 1, read-only
1067+        # server 1: share 2, read-only
1068+        # server 2: shares 0-9, read-only
1069+        # server 4: share 0, read-only
1070+        # The idea with this test is to make sure that the survey of
1071+        # read-only peers doesn't undercount servers of happiness
1072+        self.basedir = self.mktemp()
1073+        d = self._setup_and_upload()
1074+        d.addCallback(lambda ign:
1075+            self._add_server_with_share(server_number=4, share_number=0,
1076+                                        readonly=True))
1077+        d.addCallback(lambda ign:
1078+            self._add_server_with_share(server_number=3, share_number=1,
1079+                                        readonly=True))
1080+        d.addCallback(lambda ign:
1081+            self._add_server_with_share(server_number=1, share_number=2,
1082+                                        readonly=True))
1083+        d.addCallback(lambda ign:
1084+            self._add_server_with_share(server_number=2, share_number=0,
1085+                                        readonly=True))
1086+        def _copy_shares(ign):
1087+            for i in xrange(1, 10):
1088+                self._copy_share_to_server(i, 2)
1089+        d.addCallback(_copy_shares)
1090+        d.addCallback(lambda ign:
1091+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1092+        def _reset_encoding_parameters(ign, happy=4):
1093+            client = self.g.clients[0]
1094+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
1095+            return client
1096         d.addCallback(_reset_encoding_parameters)
1097         d.addCallback(lambda client:
1098             client.upload(upload.Data("data" * 10000, convergence="")))
1099hunk ./src/allmydata/test/test_upload.py 1099
1100             self.basedir = self.mktemp()
1101         _set_basedir()
1102         d = self._setup_and_upload();
1103-        # Add 5 servers, with one share each from the original
1104+        # Add 5 servers
1105         def _do_server_setup(ign):
1106hunk ./src/allmydata/test/test_upload.py 1101
1107-            self._add_server_with_share(1, 1)
1108+            self._add_server_with_share(1)
1109             self._add_server_with_share(2)
1110             self._add_server_with_share(3)
1111             self._add_server_with_share(4)
1112hunk ./src/allmydata/test/test_upload.py 1126
1113         d.addCallback(_remove_server)
1114         d.addCallback(lambda ign:
1115             self.shouldFail(NotEnoughSharesError,
1116-                            "test_dropped_server_in_encoder", "",
1117+                            "test_dropped_servers_in_encoder",
1118+                            "lost too many servers during upload "
1119+                            "(still have 3, want 4)",
1120+                            self._do_upload_with_broken_servers, 2))
1121+        # Now do the same thing over again, but make some of the servers
1122+        # readonly, break some of the ones that aren't, and make sure that
1123+        # happiness accounting is preserved.
1124+        d.addCallback(_set_basedir)
1125+        d.addCallback(lambda ign:
1126+            self._setup_and_upload())
1127+        def _do_server_setup_2(ign):
1128+            self._add_server_with_share(1)
1129+            self._add_server_with_share(2)
1130+            self._add_server_with_share(3)
1131+            self._add_server_with_share(4, 7, readonly=True)
1132+            self._add_server_with_share(5, 8, readonly=True)
1133+        d.addCallback(_do_server_setup_2)
1134+        d.addCallback(_remove_server)
1135+        d.addCallback(lambda ign:
1136+            self._do_upload_with_broken_servers(1))
1137+        d.addCallback(_set_basedir)
1138+        d.addCallback(lambda ign:
1139+            self._setup_and_upload())
1140+        d.addCallback(_do_server_setup_2)
1141+        d.addCallback(_remove_server)
1142+        d.addCallback(lambda ign:
1143+            self.shouldFail(NotEnoughSharesError,
1144+                            "test_dropped_servers_in_encoder",
1145+                            "lost too many servers during upload "
1146+                            "(still have 3, want 4)",
1147                             self._do_upload_with_broken_servers, 2))
1148         return d
1149 
1150hunk ./src/allmydata/test/test_upload.py 1179
1151         self.failUnlessEqual(3, len(unique_servers))
1152         for server in ["server1", "server2", "server3"]:
1153             self.failUnlessIn(server, unique_servers)
1154-        # servers_with_unique_shares expects a set of PeerTracker
1155-        # instances as a used_peers argument, but only uses the peerid
1156-        # instance variable to assess uniqueness. So we feed it some fake
1157-        # PeerTrackers whose only important characteristic is that they
1158-        # have peerid set to something.
1159+        # servers_with_unique_shares expects to receive some object with
1160+        # a peerid attribute. So we make a FakePeerTracker whose only
1161+        # job is to have a peerid attribute.
1162         class FakePeerTracker:
1163             pass
1164         trackers = []
1165hunk ./src/allmydata/test/test_upload.py 1185
1166-        for server in ["server5", "server6", "server7", "server8"]:
1167+        for (i, server) in [(i, "server%d" % i) for i in xrange(5, 9)]:
1168             t = FakePeerTracker()
1169             t.peerid = server
1170hunk ./src/allmydata/test/test_upload.py 1188
1171+            t.buckets = [i]
1172             trackers.append(t)
1173         # Recall that there are 3 unique servers in test1. Since none of
1174         # those overlap with the ones in trackers, we should get 7 back
1175hunk ./src/allmydata/test/test_upload.py 1201
1176         # Now add an overlapping server to trackers.
1177         t = FakePeerTracker()
1178         t.peerid = "server1"
1179+        t.buckets = [1]
1180         trackers.append(t)
1181         unique_servers = upload.servers_with_unique_shares(test1, set(trackers))
1182         self.failUnlessEqual(7, len(unique_servers))
1183hunk ./src/allmydata/test/test_upload.py 1207
1184         for server in expected_servers:
1185             self.failUnlessIn(server, unique_servers)
1186+        test = {}
1187+        unique_servers = upload.servers_with_unique_shares(test)
1188+        self.failUnlessEqual(0, len(test))
1189 
1190 
1191     def test_shares_by_server(self):
1192hunk ./src/allmydata/test/test_upload.py 1213
1193-        test = {
1194-                    1 : "server1",
1195-                    2 : "server2",
1196-                    3 : "server3",
1197-                    4 : "server4"
1198-               }
1199+        test = dict([(i, "server%d" % i) for i in xrange(1, 5)])
1200         shares_by_server = upload.shares_by_server(test)
1201         self.failUnlessEqual(set([1]), shares_by_server["server1"])
1202         self.failUnlessEqual(set([2]), shares_by_server["server2"])
1203hunk ./src/allmydata/test/test_upload.py 1267
1204         return d
1205 
1206 
1207+    def test_should_add_server(self):
1208+        shares = dict([(i, "server%d" % i) for i in xrange(10)])
1209+        self.failIf(upload.should_add_server(shares, "server1", 4))
1210+        shares[4] = "server1"
1211+        self.failUnless(upload.should_add_server(shares, "server4", 4))
1212+        shares = {}
1213+        self.failUnless(upload.should_add_server(shares, "server1", 1))
1214+
1215+
1216     def _set_up_nodes_extra_config(self, clientdir):
1217         cfgfn = os.path.join(clientdir, "tahoe.cfg")
1218         oldcfg = open(cfgfn, "r").read()
1219}
1220[Add tests for the behavior described in #834.
1221Kevan Carstensen <kevan@isnotajoke.com>**20091123012008
1222 Ignore-this: d8e0aa0f3f7965ce9b5cea843c6d6f9f
1223] {
1224hunk ./src/allmydata/test/test_encode.py 12
1225 from allmydata.util.assertutil import _assert
1226 from allmydata.util.consumer import MemoryConsumer
1227 from allmydata.interfaces import IStorageBucketWriter, IStorageBucketReader, \
1228-     NotEnoughSharesError, IStorageBroker
1229+     NotEnoughSharesError, IStorageBroker, UploadHappinessError
1230 from allmydata.monitor import Monitor
1231 import common_util as testutil
1232 
1233hunk ./src/allmydata/test/test_encode.py 794
1234         d = self.send_and_recover((4,8,10), bucket_modes=modemap)
1235         def _done(res):
1236             self.failUnless(isinstance(res, Failure))
1237-            self.failUnless(res.check(NotEnoughSharesError), res)
1238+            self.failUnless(res.check(UploadHappinessError), res)
1239         d.addBoth(_done)
1240         return d
1241 
1242hunk ./src/allmydata/test/test_encode.py 805
1243         d = self.send_and_recover((4,8,10), bucket_modes=modemap)
1244         def _done(res):
1245             self.failUnless(isinstance(res, Failure))
1246-            self.failUnless(res.check(NotEnoughSharesError))
1247+            self.failUnless(res.check(UploadHappinessError))
1248         d.addBoth(_done)
1249         return d
1250hunk ./src/allmydata/test/test_upload.py 13
1251 import allmydata # for __full_version__
1252 from allmydata import uri, monitor, client
1253 from allmydata.immutable import upload, encode
1254-from allmydata.interfaces import FileTooLargeError, NoSharesError, \
1255-     NotEnoughSharesError
1256+from allmydata.interfaces import FileTooLargeError, UploadHappinessError
1257 from allmydata.util.assertutil import precondition
1258 from allmydata.util.deferredutil import DeferredListShouldSucceed
1259 from no_network import GridTestMixin
1260hunk ./src/allmydata/test/test_upload.py 402
1261 
1262     def test_first_error_all(self):
1263         self.make_node("first-fail")
1264-        d = self.shouldFail(NoSharesError, "first_error_all",
1265+        d = self.shouldFail(UploadHappinessError, "first_error_all",
1266                             "peer selection failed",
1267                             upload_data, self.u, DATA)
1268         def _check((f,)):
1269hunk ./src/allmydata/test/test_upload.py 434
1270 
1271     def test_second_error_all(self):
1272         self.make_node("second-fail")
1273-        d = self.shouldFail(NotEnoughSharesError, "second_error_all",
1274+        d = self.shouldFail(UploadHappinessError, "second_error_all",
1275                             "peer selection failed",
1276                             upload_data, self.u, DATA)
1277         def _check((f,)):
1278hunk ./src/allmydata/test/test_upload.py 452
1279         self.u.parent = self.node
1280 
1281     def _should_fail(self, f):
1282-        self.failUnless(isinstance(f, Failure) and f.check(NoSharesError), f)
1283+        self.failUnless(isinstance(f, Failure) and f.check(UploadHappinessError), f)
1284 
1285     def test_data_large(self):
1286         data = DATA
1287hunk ./src/allmydata/test/test_upload.py 817
1288         # These parameters are unsatisfiable with the client that we've made
1289         # -- we'll use them to test that the semnatics work correctly.
1290         self.set_encoding_parameters(k=3, happy=5, n=10)
1291-        d = self.shouldFail(NotEnoughSharesError, "test_happy_semantics",
1292+        d = self.shouldFail(UploadHappinessError, "test_happy_semantics",
1293                             "shares could only be placed on 2 servers "
1294                             "(5 were requested)",
1295                             self.u.upload, DATA)
1296hunk ./src/allmydata/test/test_upload.py 888
1297             _prepare())
1298         # Uploading data should fail
1299         d.addCallback(lambda client:
1300-            self.shouldFail(NotEnoughSharesError, "test_happy_semantics",
1301+            self.shouldFail(UploadHappinessError, "test_happy_semantics",
1302                             "shares could only be placed on 2 servers "
1303                             "(4 were requested)",
1304                             client.upload, upload.Data("data" * 10000,
1305hunk ./src/allmydata/test/test_upload.py 918
1306         d.addCallback(lambda ign:
1307             _prepare2())
1308         d.addCallback(lambda client:
1309-            self.shouldFail(NotEnoughSharesError, "test_happy_sematics",
1310+            self.shouldFail(UploadHappinessError, "test_happy_sematics",
1311                             "shares could only be placed on 2 servers "
1312                             "(3 were requested)",
1313                             client.upload, upload.Data("data" * 10000,
1314hunk ./src/allmydata/test/test_upload.py 1124
1315         d.addCallback(_do_server_setup)
1316         d.addCallback(_remove_server)
1317         d.addCallback(lambda ign:
1318-            self.shouldFail(NotEnoughSharesError,
1319+            self.shouldFail(UploadHappinessError,
1320                             "test_dropped_servers_in_encoder",
1321                             "lost too many servers during upload "
1322                             "(still have 3, want 4)",
1323hunk ./src/allmydata/test/test_upload.py 1151
1324         d.addCallback(_do_server_setup_2)
1325         d.addCallback(_remove_server)
1326         d.addCallback(lambda ign:
1327-            self.shouldFail(NotEnoughSharesError,
1328+            self.shouldFail(UploadHappinessError,
1329                             "test_dropped_servers_in_encoder",
1330                             "lost too many servers during upload "
1331                             "(still have 3, want 4)",
1332hunk ./src/allmydata/test/test_upload.py 1275
1333         self.failUnless(upload.should_add_server(shares, "server1", 1))
1334 
1335 
1336+    def test_exception_messages_during_peer_selection(self):
1337+        # server 1: readonly, no shares
1338+        # server 2: readonly, no shares
1339+        # server 3: readonly, no shares
1340+        # server 4: readonly, no shares
1341+        # server 5: readonly, no shares
1342+        # This will fail, but we want to make sure that the log messages
1343+        # are informative about why it has failed.
1344+        self.basedir = self.mktemp()
1345+        d = self._setup_and_upload()
1346+        d.addCallback(lambda ign:
1347+            self._add_server_with_share(server_number=1, readonly=True))
1348+        d.addCallback(lambda ign:
1349+            self._add_server_with_share(server_number=2, readonly=True))
1350+        d.addCallback(lambda ign:
1351+            self._add_server_with_share(server_number=3, readonly=True))
1352+        d.addCallback(lambda ign:
1353+            self._add_server_with_share(server_number=4, readonly=True))
1354+        d.addCallback(lambda ign:
1355+            self._add_server_with_share(server_number=5, readonly=True))
1356+        d.addCallback(lambda ign:
1357+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1358+        def _reset_encoding_parameters(ign):
1359+            client = self.g.clients[0]
1360+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
1361+            return client
1362+        d.addCallback(_reset_encoding_parameters)
1363+        d.addCallback(lambda client:
1364+            self.shouldFail(UploadHappinessError, "test_selection_exceptions",
1365+                            "peer selection failed for <Tahoe2PeerSelector "
1366+                            "for upload dglev>: placed 0 shares out of 10 "
1367+                            "total (10 homeless), want to place on 4 servers,"
1368+                            " sent 5 queries to 5 peers, 0 queries placed "
1369+                            "some shares, 5 placed none "
1370+                            "(of which 5 placed none due to the server being "
1371+                            "full and 0 placed none due to an error)",
1372+                            client.upload,
1373+                            upload.Data("data" * 10000, convergence="")))
1374+
1375+
1376+        # server 1: readonly, no shares
1377+        # server 2: broken, no shares
1378+        # server 3: readonly, no shares
1379+        # server 4: readonly, no shares
1380+        # server 5: readonly, no shares
1381+        def _reset(ign):
1382+            self.basedir = self.mktemp()
1383+        d.addCallback(_reset)
1384+        d.addCallback(lambda ign:
1385+            self._setup_and_upload())
1386+        d.addCallback(lambda ign:
1387+            self._add_server_with_share(server_number=1, readonly=True))
1388+        d.addCallback(lambda ign:
1389+            self._add_server_with_share(server_number=2))
1390+        def _break_server_2(ign):
1391+            server = self.g.servers_by_number[2].my_nodeid
1392+            # We have to break the server in servers_by_id,
1393+            # because the ones in servers_by_number isn't wrapped,
1394+            # and doesn't look at its broken attribute
1395+            self.g.servers_by_id[server].broken = True
1396+        d.addCallback(_break_server_2)
1397+        d.addCallback(lambda ign:
1398+            self._add_server_with_share(server_number=3, readonly=True))
1399+        d.addCallback(lambda ign:
1400+            self._add_server_with_share(server_number=4, readonly=True))
1401+        d.addCallback(lambda ign:
1402+            self._add_server_with_share(server_number=5, readonly=True))
1403+        d.addCallback(lambda ign:
1404+            self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1405+        def _reset_encoding_parameters(ign):
1406+            client = self.g.clients[0]
1407+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
1408+            return client
1409+        d.addCallback(_reset_encoding_parameters)
1410+        d.addCallback(lambda client:
1411+            self.shouldFail(UploadHappinessError, "test_selection_exceptions",
1412+                            "peer selection failed for <Tahoe2PeerSelector "
1413+                            "for upload dglev>: placed 0 shares out of 10 "
1414+                            "total (10 homeless), want to place on 4 servers,"
1415+                            " sent 5 queries to 5 peers, 0 queries placed "
1416+                            "some shares, 5 placed none "
1417+                            "(of which 4 placed none due to the server being "
1418+                            "full and 1 placed none due to an error)",
1419+                            client.upload,
1420+                            upload.Data("data" * 10000, convergence="")))
1421+        return d
1422+
1423+
1424     def _set_up_nodes_extra_config(self, clientdir):
1425         cfgfn = os.path.join(clientdir, "tahoe.cfg")
1426         oldcfg = open(cfgfn, "r").read()
1427}
1428[Replace "UploadHappinessError" with "UploadUnhappinessError" in tests.
1429Kevan Carstensen <kevan@isnotajoke.com>**20091205043453
1430 Ignore-this: 83f4bc50c697d21b5f4e2a4cd91862ca
1431] {
1432replace ./src/allmydata/test/test_encode.py [A-Za-z_0-9] UploadHappinessError UploadUnhappinessError
1433replace ./src/allmydata/test/test_upload.py [A-Za-z_0-9] UploadHappinessError UploadUnhappinessError
1434}
1435[Alter various unit tests to work with the new happy behavior
1436Kevan Carstensen <kevan@isnotajoke.com>**20100107181325
1437 Ignore-this: 132032bbf865e63a079f869b663be34a
1438] {
1439hunk ./src/allmydata/test/common.py 941
1440             # We need multiple segments to test crypttext hash trees that are
1441             # non-trivial (i.e. they have more than just one hash in them).
1442             cl0.DEFAULT_ENCODING_PARAMETERS['max_segment_size'] = 12
1443+            # Tests that need to test servers of happiness using this should
1444+            # set their own value for happy -- the default (7) breaks stuff.
1445+            cl0.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
1446             d2 = cl0.upload(immutable.upload.Data(TEST_DATA, convergence=""))
1447             def _after_upload(u):
1448                 filecap = u.uri
1449hunk ./src/allmydata/test/test_checker.py 283
1450         self.basedir = "checker/AddLease/875"
1451         self.set_up_grid(num_servers=1)
1452         c0 = self.g.clients[0]
1453+        c0.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
1454         self.uris = {}
1455         DATA = "data" * 100
1456         d = c0.upload(Data(DATA, convergence=""))
1457hunk ./src/allmydata/test/test_system.py 93
1458         d = self.set_up_nodes()
1459         def _check_connections(res):
1460             for c in self.clients:
1461+                c.DEFAULT_ENCODING_PARAMETERS['happy'] = 5
1462                 all_peerids = c.get_storage_broker().get_all_serverids()
1463                 self.failUnlessEqual(len(all_peerids), self.numclients)
1464                 sb = c.storage_broker
1465hunk ./src/allmydata/test/test_system.py 205
1466                                                       add_to_sparent=True))
1467         def _added(extra_node):
1468             self.extra_node = extra_node
1469+            self.extra_node.DEFAULT_ENCODING_PARAMETERS['happy'] = 5
1470         d.addCallback(_added)
1471 
1472         HELPER_DATA = "Data that needs help to upload" * 1000
1473hunk ./src/allmydata/test/test_system.py 705
1474         self.basedir = "system/SystemTest/test_filesystem"
1475         self.data = LARGE_DATA
1476         d = self.set_up_nodes(use_stats_gatherer=True)
1477+        def _new_happy_semantics(ign):
1478+            for c in self.clients:
1479+                c.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
1480+        d.addCallback(_new_happy_semantics)
1481         d.addCallback(self._test_introweb)
1482         d.addCallback(self.log, "starting publish")
1483         d.addCallback(self._do_publish1)
1484hunk ./src/allmydata/test/test_system.py 1129
1485         d.addCallback(self.failUnlessEqual, "new.txt contents")
1486         # and again with something large enough to use multiple segments,
1487         # and hopefully trigger pauseProducing too
1488+        def _new_happy_semantics(ign):
1489+            for c in self.clients:
1490+                # these get reset somewhere? Whatever.
1491+                c.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
1492+        d.addCallback(_new_happy_semantics)
1493         d.addCallback(lambda res: self.PUT(public + "/subdir3/big.txt",
1494                                            "big" * 500000)) # 1.5MB
1495         d.addCallback(lambda res: self.GET(public + "/subdir3/big.txt"))
1496hunk ./src/allmydata/test/test_upload.py 178
1497 
1498 class FakeClient:
1499     DEFAULT_ENCODING_PARAMETERS = {"k":25,
1500-                                   "happy": 75,
1501+                                   "happy": 25,
1502                                    "n": 100,
1503                                    "max_segment_size": 1*MiB,
1504                                    }
1505hunk ./src/allmydata/test/test_upload.py 316
1506         data = self.get_data(SIZE_LARGE)
1507         segsize = int(SIZE_LARGE / 2.5)
1508         # we want 3 segments, since that's not a power of two
1509-        self.set_encoding_parameters(25, 75, 100, segsize)
1510+        self.set_encoding_parameters(25, 25, 100, segsize)
1511         d = upload_data(self.u, data)
1512         d.addCallback(extract_uri)
1513         d.addCallback(self._check_large, SIZE_LARGE)
1514hunk ./src/allmydata/test/test_upload.py 395
1515     def test_first_error(self):
1516         mode = dict([(0,"good")] + [(i,"first-fail") for i in range(1,10)])
1517         self.make_node(mode)
1518+        self.set_encoding_parameters(k=25, happy=1, n=50)
1519         d = upload_data(self.u, DATA)
1520         d.addCallback(extract_uri)
1521         d.addCallback(self._check_large, SIZE_LARGE)
1522hunk ./src/allmydata/test/test_upload.py 513
1523 
1524         self.make_client()
1525         data = self.get_data(SIZE_LARGE)
1526-        self.set_encoding_parameters(50, 75, 100)
1527+        # if there are 50 peers, then happy needs to be <= 50
1528+        self.set_encoding_parameters(50, 50, 100)
1529         d = upload_data(self.u, data)
1530         d.addCallback(extract_uri)
1531         d.addCallback(self._check_large, SIZE_LARGE)
1532hunk ./src/allmydata/test/test_upload.py 560
1533 
1534         self.make_client()
1535         data = self.get_data(SIZE_LARGE)
1536-        self.set_encoding_parameters(100, 150, 200)
1537+        # if there are 50 peers, then happy should be no more than 50 if
1538+        # we want this to work.
1539+        self.set_encoding_parameters(100, 50, 200)
1540         d = upload_data(self.u, data)
1541         d.addCallback(extract_uri)
1542         d.addCallback(self._check_large, SIZE_LARGE)
1543hunk ./src/allmydata/test/test_upload.py 580
1544 
1545         self.make_client(3)
1546         data = self.get_data(SIZE_LARGE)
1547-        self.set_encoding_parameters(3, 5, 10)
1548+        self.set_encoding_parameters(3, 3, 10)
1549         d = upload_data(self.u, data)
1550         d.addCallback(extract_uri)
1551         d.addCallback(self._check_large, SIZE_LARGE)
1552hunk ./src/allmydata/test/test_web.py 4073
1553         self.basedir = "web/Grid/exceptions"
1554         self.set_up_grid(num_clients=1, num_servers=2)
1555         c0 = self.g.clients[0]
1556+        c0.DEFAULT_ENCODING_PARAMETERS['happy'] = 2
1557         self.fileurls = {}
1558         DATA = "data" * 100
1559         d = c0.create_dirnode()
1560}
1561[Revisions of the #778 tests, per reviewers' comments
1562Kevan Carstensen <kevan@isnotajoke.com>**20100507220051
1563 Ignore-this: c15abb92ad0176037ff612d758c8f3ce
1564 
1565 - Fix comments and confusing naming.
1566 - Add tests for the new error messages suggested by David-Sarah
1567   and Zooko.
1568 - Alter existing tests for new error messages.
1569 - Make sure that the tests continue to work with the trunk.
1570 - Add a test for a mutual disjointedness assertion that I added to
1571   upload.servers_of_happiness.
1572 - Fix the comments to correctly reflect read-onlyness
1573 - Add a test for an edge case in should_add_server
1574 - Add an assertion to make sure that share redistribution works as it
1575   should
1576 - Alter tests to work with revised servers_of_happiness semantics
1577 - Remove tests for should_add_server, since that function no longer exists.
1578 - Alter tests to know about merge_peers, and to use it before calling
1579   servers_of_happiness.
1580 - Add tests for merge_peers.
1581 - Add Zooko's puzzles to the tests.
1582 - Edit encoding tests to expect the new kind of failure message.
1583 
1584] {
1585hunk ./src/allmydata/test/test_encode.py 28
1586 class FakeBucketReaderWriterProxy:
1587     implements(IStorageBucketWriter, IStorageBucketReader)
1588     # these are used for both reading and writing
1589-    def __init__(self, mode="good"):
1590+    def __init__(self, mode="good", peerid="peer"):
1591         self.mode = mode
1592         self.blocks = {}
1593         self.plaintext_hashes = []
1594hunk ./src/allmydata/test/test_encode.py 36
1595         self.block_hashes = None
1596         self.share_hashes = None
1597         self.closed = False
1598+        self.peerid = peerid
1599 
1600     def get_peerid(self):
1601hunk ./src/allmydata/test/test_encode.py 39
1602-        return "peerid"
1603+        return self.peerid
1604 
1605     def _start(self):
1606         if self.mode == "lost-early":
1607hunk ./src/allmydata/test/test_encode.py 306
1608             for shnum in range(NUM_SHARES):
1609                 peer = FakeBucketReaderWriterProxy()
1610                 shareholders[shnum] = peer
1611-                servermap[shnum] = str(shnum)
1612+                servermap.setdefault(shnum, set()).add(peer.get_peerid())
1613                 all_shareholders.append(peer)
1614             e.set_shareholders(shareholders, servermap)
1615             return e.start()
1616hunk ./src/allmydata/test/test_encode.py 463
1617         def _ready(res):
1618             k,happy,n = e.get_param("share_counts")
1619             assert n == NUM_SHARES # else we'll be completely confused
1620-            all_peers = []
1621+            servermap = {}
1622             for shnum in range(NUM_SHARES):
1623                 mode = bucket_modes.get(shnum, "good")
1624hunk ./src/allmydata/test/test_encode.py 466
1625-                peer = FakeBucketReaderWriterProxy(mode)
1626+                peer = FakeBucketReaderWriterProxy(mode, "peer%d" % shnum)
1627                 shareholders[shnum] = peer
1628hunk ./src/allmydata/test/test_encode.py 468
1629-                servermap[shnum] = str(shnum)
1630+                servermap.setdefault(shnum, set()).add(peer.get_peerid())
1631             e.set_shareholders(shareholders, servermap)
1632             return e.start()
1633         d.addCallback(_ready)
1634hunk ./src/allmydata/test/test_upload.py 16
1635 from allmydata.interfaces import FileTooLargeError, UploadUnhappinessError
1636 from allmydata.util.assertutil import precondition
1637 from allmydata.util.deferredutil import DeferredListShouldSucceed
1638+from allmydata.util.happinessutil import servers_of_happiness, \
1639+                                         shares_by_server, merge_peers
1640 from no_network import GridTestMixin
1641 from common_util import ShouldFailMixin
1642 from allmydata.storage_client import StorageFarmBroker
1643hunk ./src/allmydata/test/test_upload.py 708
1644         num_segments = encoder.get_param("num_segments")
1645         d = selector.get_shareholders(broker, sh, storage_index,
1646                                       share_size, block_size, num_segments,
1647-                                      10, 4)
1648+                                      10, 3, 4)
1649         def _have_shareholders((used_peers, already_peers)):
1650             assert servers_to_break <= len(used_peers)
1651             for index in xrange(servers_to_break):
1652hunk ./src/allmydata/test/test_upload.py 720
1653             for peer in used_peers:
1654                 buckets.update(peer.buckets)
1655                 for bucket in peer.buckets:
1656-                    servermap[bucket] = peer.peerid
1657+                    servermap.setdefault(bucket, set()).add(peer.peerid)
1658             encoder.set_shareholders(buckets, servermap)
1659             d = encoder.start()
1660             return d
1661hunk ./src/allmydata/test/test_upload.py 764
1662         self.failUnless((share_number, ss.my_nodeid, new_share_location)
1663                         in shares)
1664 
1665+    def _setup_grid(self):
1666+        """
1667+        I set up a NoNetworkGrid with a single server and client.
1668+        """
1669+        self.set_up_grid(num_clients=1, num_servers=1)
1670 
1671     def _setup_and_upload(self):
1672         """
1673hunk ./src/allmydata/test/test_upload.py 776
1674         upload a file to it, store its uri in self.uri, and store its
1675         sharedata in self.shares.
1676         """
1677-        self.set_up_grid(num_clients=1, num_servers=1)
1678+        self._setup_grid()
1679         client = self.g.clients[0]
1680         client.DEFAULT_ENCODING_PARAMETERS['happy'] = 1
1681         data = upload.Data("data" * 10000, convergence="")
1682hunk ./src/allmydata/test/test_upload.py 814
1683 
1684 
1685     def _setUp(self, ns):
1686-        # Used by test_happy_semantics and test_prexisting_share_behavior
1687+        # Used by test_happy_semantics and test_preexisting_share_behavior
1688         # to set up the grid.
1689         self.node = FakeClient(mode="good", num_servers=ns)
1690         self.u = upload.Uploader()
1691hunk ./src/allmydata/test/test_upload.py 825
1692     def test_happy_semantics(self):
1693         self._setUp(2)
1694         DATA = upload.Data("kittens" * 10000, convergence="")
1695-        # These parameters are unsatisfiable with the client that we've made
1696-        # -- we'll use them to test that the semnatics work correctly.
1697+        # These parameters are unsatisfiable with only 2 servers.
1698         self.set_encoding_parameters(k=3, happy=5, n=10)
1699         d = self.shouldFail(UploadUnhappinessError, "test_happy_semantics",
1700hunk ./src/allmydata/test/test_upload.py 828
1701-                            "shares could only be placed on 2 servers "
1702-                            "(5 were requested)",
1703+                            "shares could only be placed or found on 2 "
1704+                            "server(s). We were asked to place shares on "
1705+                            "at least 5 server(s) such that any 3 of them "
1706+                            "have enough shares to recover the file",
1707                             self.u.upload, DATA)
1708         # Let's reset the client to have 10 servers
1709         d.addCallback(lambda ign:
1710hunk ./src/allmydata/test/test_upload.py 836
1711             self._setUp(10))
1712-        # These parameters are satisfiable with the client we've made.
1713+        # These parameters are satisfiable with 10 servers.
1714         d.addCallback(lambda ign:
1715             self.set_encoding_parameters(k=3, happy=5, n=10))
1716hunk ./src/allmydata/test/test_upload.py 839
1717-        # this should work
1718         d.addCallback(lambda ign:
1719             self.u.upload(DATA))
1720         # Let's reset the client to have 7 servers
1721hunk ./src/allmydata/test/test_upload.py 845
1722         # (this is less than n, but more than h)
1723         d.addCallback(lambda ign:
1724             self._setUp(7))
1725-        # These encoding parameters should still be satisfiable with our
1726-        # client setup
1727+        # These parameters are satisfiable with 7 servers.
1728         d.addCallback(lambda ign:
1729             self.set_encoding_parameters(k=3, happy=5, n=10))
1730hunk ./src/allmydata/test/test_upload.py 848
1731-        # This, then, should work.
1732         d.addCallback(lambda ign:
1733             self.u.upload(DATA))
1734         return d
1735hunk ./src/allmydata/test/test_upload.py 862
1736         #
1737         # The scenario in comment:52 proposes that we have a layout
1738         # like:
1739-        # server 1: share 1
1740-        # server 2: share 1
1741-        # server 3: share 1
1742-        # server 4: shares 2 - 10
1743+        # server 0: shares 1 - 9
1744+        # server 1: share 0, read-only
1745+        # server 2: share 0, read-only
1746+        # server 3: share 0, read-only
1747         # To get access to the shares, we will first upload to one
1748hunk ./src/allmydata/test/test_upload.py 867
1749-        # server, which will then have shares 1 - 10. We'll then
1750+        # server, which will then have shares 0 - 9. We'll then
1751         # add three new servers, configure them to not accept any new
1752hunk ./src/allmydata/test/test_upload.py 869
1753-        # shares, then write share 1 directly into the serverdir of each.
1754-        # Then each of servers 1 - 3 will report that they have share 1,
1755-        # and will not accept any new share, while server 4 will report that
1756-        # it has shares 2 - 10 and will accept new shares.
1757+        # shares, then write share 0 directly into the serverdir of each,
1758+        # and then remove share 0 from server 0 in the same way.
1759+        # Then each of servers 1 - 3 will report that they have share 0,
1760+        # and will not accept any new share, while server 0 will report that
1761+        # it has shares 1 - 9 and will accept new shares.
1762         # We'll then set 'happy' = 4, and see that an upload fails
1763         # (as it should)
1764         d = self._setup_and_upload()
1765hunk ./src/allmydata/test/test_upload.py 878
1766         d.addCallback(lambda ign:
1767-            self._add_server_with_share(1, 0, True))
1768+            self._add_server_with_share(server_number=1, share_number=0,
1769+                                        readonly=True))
1770         d.addCallback(lambda ign:
1771hunk ./src/allmydata/test/test_upload.py 881
1772-            self._add_server_with_share(2, 0, True))
1773+            self._add_server_with_share(server_number=2, share_number=0,
1774+                                        readonly=True))
1775         d.addCallback(lambda ign:
1776hunk ./src/allmydata/test/test_upload.py 884
1777-            self._add_server_with_share(3, 0, True))
1778+            self._add_server_with_share(server_number=3, share_number=0,
1779+                                        readonly=True))
1780         # Remove the first share from server 0.
1781hunk ./src/allmydata/test/test_upload.py 887
1782-        def _remove_share_0():
1783+        def _remove_share_0_from_server_0():
1784             share_location = self.shares[0][2]
1785             os.remove(share_location)
1786         d.addCallback(lambda ign:
1787hunk ./src/allmydata/test/test_upload.py 891
1788-            _remove_share_0())
1789+            _remove_share_0_from_server_0())
1790         # Set happy = 4 in the client.
1791         def _prepare():
1792             client = self.g.clients[0]
1793hunk ./src/allmydata/test/test_upload.py 901
1794             _prepare())
1795         # Uploading data should fail
1796         d.addCallback(lambda client:
1797-            self.shouldFail(UploadUnhappinessError, "test_happy_semantics",
1798-                            "shares could only be placed on 2 servers "
1799-                            "(4 were requested)",
1800+            self.shouldFail(UploadUnhappinessError,
1801+                            "test_problem_layout_comment_52_test_1",
1802+                            "shares could be placed or found on 4 server(s), "
1803+                            "but they are not spread out evenly enough to "
1804+                            "ensure that any 3 of these servers would have "
1805+                            "enough shares to recover the file. "
1806+                            "We were asked to place shares on at "
1807+                            "least 4 servers such that any 3 of them have "
1808+                            "enough shares to recover the file",
1809                             client.upload, upload.Data("data" * 10000,
1810                                                        convergence="")))
1811 
1812hunk ./src/allmydata/test/test_upload.py 932
1813                                         readonly=True))
1814         def _prepare2():
1815             client = self.g.clients[0]
1816-            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 3
1817+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
1818             return client
1819         d.addCallback(lambda ign:
1820             _prepare2())
1821hunk ./src/allmydata/test/test_upload.py 937
1822         d.addCallback(lambda client:
1823-            self.shouldFail(UploadUnhappinessError, "test_happy_sematics",
1824-                            "shares could only be placed on 2 servers "
1825-                            "(3 were requested)",
1826+            self.shouldFail(UploadUnhappinessError,
1827+                            "test_problem_layout_comment_52_test_2",
1828+                            "shares could only be placed on 3 server(s) such "
1829+                            "that any 3 of them have enough shares to recover "
1830+                            "the file, but we were asked to place shares on "
1831+                            "at least 4 such servers.",
1832                             client.upload, upload.Data("data" * 10000,
1833                                                        convergence="")))
1834         return d
1835hunk ./src/allmydata/test/test_upload.py 956
1836         def _change_basedir(ign):
1837             self.basedir = self.mktemp()
1838         _change_basedir(None)
1839-        d = self._setup_and_upload()
1840-        # We start by uploading all of the shares to one server (which has
1841-        # already been done above).
1842+        # We start by uploading all of the shares to one server.
1843         # Next, we'll add three new servers to our NoNetworkGrid. We'll add
1844         # one share from our initial upload to each of these.
1845         # The counterintuitive ordering of the share numbers is to deal with
1846hunk ./src/allmydata/test/test_upload.py 962
1847         # the permuting of these servers -- distributing the shares this
1848         # way ensures that the Tahoe2PeerSelector sees them in the order
1849-        # described above.
1850+        # described below.
1851+        d = self._setup_and_upload()
1852         d.addCallback(lambda ign:
1853             self._add_server_with_share(server_number=1, share_number=2))
1854         d.addCallback(lambda ign:
1855hunk ./src/allmydata/test/test_upload.py 975
1856         # server 1: share 2
1857         # server 2: share 0
1858         # server 3: share 1
1859-        # We want to change the 'happy' parameter in the client to 4.
1860+        # We change the 'happy' parameter in the client to 4.
1861         # The Tahoe2PeerSelector will see the peers permuted as:
1862         # 2, 3, 1, 0
1863         # Ideally, a reupload of our original data should work.
1864hunk ./src/allmydata/test/test_upload.py 988
1865             client.upload(upload.Data("data" * 10000, convergence="")))
1866 
1867 
1868-        # This scenario is basically comment:53, but with the order reversed;
1869-        # this means that the Tahoe2PeerSelector sees
1870-        # server 2: shares 1-10
1871-        # server 3: share 1
1872-        # server 1: share 2
1873-        # server 4: share 3
1874+        # This scenario is basically comment:53, but changed so that the
1875+        # Tahoe2PeerSelector sees the server with all of the shares before
1876+        # any of the other servers.
1877+        # The layout is:
1878+        # server 2: shares 0 - 9
1879+        # server 3: share 0
1880+        # server 1: share 1
1881+        # server 4: share 2
1882+        # The Tahoe2PeerSelector sees the peers permuted as:
1883+        # 2, 3, 1, 4
1884+        # Note that server 0 has been replaced by server 4; this makes it
1885+        # easier to ensure that the last server seen by Tahoe2PeerSelector
1886+        # has only one share.
1887         d.addCallback(_change_basedir)
1888         d.addCallback(lambda ign:
1889             self._setup_and_upload())
1890hunk ./src/allmydata/test/test_upload.py 1012
1891             self._add_server_with_share(server_number=1, share_number=2))
1892         # Copy all of the other shares to server number 2
1893         def _copy_shares(ign):
1894-            for i in xrange(1, 10):
1895+            for i in xrange(0, 10):
1896                 self._copy_share_to_server(i, 2)
1897         d.addCallback(_copy_shares)
1898         # Remove the first server, and add a placeholder with share 0
1899hunk ./src/allmydata/test/test_upload.py 1024
1900         d.addCallback(_reset_encoding_parameters)
1901         d.addCallback(lambda client:
1902             client.upload(upload.Data("data" * 10000, convergence="")))
1903+
1904+
1905         # Try the same thing, but with empty servers after the first one
1906         # We want to make sure that Tahoe2PeerSelector will redistribute
1907         # shares as necessary, not simply discover an existing layout.
1908hunk ./src/allmydata/test/test_upload.py 1029
1909+        # The layout is:
1910+        # server 2: shares 0 - 9
1911+        # server 3: empty
1912+        # server 1: empty
1913+        # server 4: empty
1914         d.addCallback(_change_basedir)
1915         d.addCallback(lambda ign:
1916             self._setup_and_upload())
1917hunk ./src/allmydata/test/test_upload.py 1043
1918             self._add_server(server_number=3))
1919         d.addCallback(lambda ign:
1920             self._add_server(server_number=1))
1921+        d.addCallback(lambda ign:
1922+            self._add_server(server_number=4))
1923         d.addCallback(_copy_shares)
1924         d.addCallback(lambda ign:
1925             self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
1926hunk ./src/allmydata/test/test_upload.py 1048
1927-        d.addCallback(lambda ign:
1928-            self._add_server(server_number=4))
1929         d.addCallback(_reset_encoding_parameters)
1930         d.addCallback(lambda client:
1931             client.upload(upload.Data("data" * 10000, convergence="")))
1932hunk ./src/allmydata/test/test_upload.py 1051
1933+        # Make sure that only as many shares as necessary to satisfy
1934+        # servers of happiness were pushed.
1935+        d.addCallback(lambda results:
1936+            self.failUnlessEqual(results.pushed_shares, 3))
1937         return d
1938 
1939 
1940hunk ./src/allmydata/test/test_upload.py 1133
1941 
1942 
1943     def test_dropped_servers_in_encoder(self):
1944+        # The Encoder does its own "servers_of_happiness" check if it
1945+        # happens to lose a bucket during an upload (it assumes that
1946+        # the layout presented to it satisfies "servers_of_happiness"
1947+        # until a failure occurs)
1948+        #
1949+        # This test simulates an upload where servers break after peer
1950+        # selection, but before they are written to.
1951         def _set_basedir(ign=None):
1952             self.basedir = self.mktemp()
1953         _set_basedir()
1954hunk ./src/allmydata/test/test_upload.py 1146
1955         d = self._setup_and_upload();
1956         # Add 5 servers
1957         def _do_server_setup(ign):
1958-            self._add_server_with_share(1)
1959-            self._add_server_with_share(2)
1960-            self._add_server_with_share(3)
1961-            self._add_server_with_share(4)
1962-            self._add_server_with_share(5)
1963+            self._add_server_with_share(server_number=1)
1964+            self._add_server_with_share(server_number=2)
1965+            self._add_server_with_share(server_number=3)
1966+            self._add_server_with_share(server_number=4)
1967+            self._add_server_with_share(server_number=5)
1968         d.addCallback(_do_server_setup)
1969         # remove the original server
1970         # (necessary to ensure that the Tahoe2PeerSelector will distribute
1971hunk ./src/allmydata/test/test_upload.py 1159
1972             server = self.g.servers_by_number[0]
1973             self.g.remove_server(server.my_nodeid)
1974         d.addCallback(_remove_server)
1975-        # This should succeed.
1976+        # This should succeed; we still have 4 servers, and the
1977+        # happiness of the upload is 4.
1978         d.addCallback(lambda ign:
1979             self._do_upload_with_broken_servers(1))
1980         # Now, do the same thing over again, but drop 2 servers instead
1981hunk ./src/allmydata/test/test_upload.py 1164
1982-        # of 1. This should fail.
1983+        # of 1. This should fail, because servers_of_happiness is 4 and
1984+        # we can't satisfy that.
1985         d.addCallback(_set_basedir)
1986         d.addCallback(lambda ign:
1987             self._setup_and_upload())
1988hunk ./src/allmydata/test/test_upload.py 1174
1989         d.addCallback(lambda ign:
1990             self.shouldFail(UploadUnhappinessError,
1991                             "test_dropped_servers_in_encoder",
1992-                            "lost too many servers during upload "
1993-                            "(still have 3, want 4)",
1994+                            "shares could only be placed on 3 server(s) "
1995+                            "such that any 3 of them have enough shares to "
1996+                            "recover the file, but we were asked to place "
1997+                            "shares on at least 4",
1998                             self._do_upload_with_broken_servers, 2))
1999         # Now do the same thing over again, but make some of the servers
2000         # readonly, break some of the ones that aren't, and make sure that
2001hunk ./src/allmydata/test/test_upload.py 1203
2002         d.addCallback(lambda ign:
2003             self.shouldFail(UploadUnhappinessError,
2004                             "test_dropped_servers_in_encoder",
2005-                            "lost too many servers during upload "
2006-                            "(still have 3, want 4)",
2007+                            "shares could only be placed on 3 server(s) "
2008+                            "such that any 3 of them have enough shares to "
2009+                            "recover the file, but we were asked to place "
2010+                            "shares on at least 4",
2011                             self._do_upload_with_broken_servers, 2))
2012         return d
2013 
2014hunk ./src/allmydata/test/test_upload.py 1211
2015 
2016-    def test_servers_with_unique_shares(self):
2017-        # servers_with_unique_shares expects a dict of
2018-        # shnum => peerid as a preexisting shares argument.
2019+    def test_merge_peers(self):
2020+        # merge_peers merges a list of used_peers and a dict of
2021+        # shareid -> peerid mappings.
2022+        shares = {
2023+                    1 : set(["server1"]),
2024+                    2 : set(["server2"]),
2025+                    3 : set(["server3"]),
2026+                    4 : set(["server4", "server5"]),
2027+                    5 : set(["server1", "server2"]),
2028+                 }
2029+        # if not provided with a used_peers argument, it should just
2030+        # return the first argument unchanged.
2031+        self.failUnlessEqual(shares, merge_peers(shares, set([])))
2032+        class FakePeerTracker:
2033+            pass
2034+        trackers = []
2035+        for (i, server) in [(i, "server%d" % i) for i in xrange(5, 9)]:
2036+            t = FakePeerTracker()
2037+            t.peerid = server
2038+            t.buckets = [i]
2039+            trackers.append(t)
2040+        expected = {
2041+                    1 : set(["server1"]),
2042+                    2 : set(["server2"]),
2043+                    3 : set(["server3"]),
2044+                    4 : set(["server4", "server5"]),
2045+                    5 : set(["server1", "server2", "server5"]),
2046+                    6 : set(["server6"]),
2047+                    7 : set(["server7"]),
2048+                    8 : set(["server8"]),
2049+                   }
2050+        self.failUnlessEqual(expected, merge_peers(shares, set(trackers)))
2051+        shares2 = {}
2052+        expected = {
2053+                    5 : set(["server5"]),
2054+                    6 : set(["server6"]),
2055+                    7 : set(["server7"]),
2056+                    8 : set(["server8"]),
2057+                   }
2058+        self.failUnlessEqual(expected, merge_peers(shares2, set(trackers)))
2059+        shares3 = {}
2060+        trackers = []
2061+        expected = {}
2062+        for (i, server) in [(i, "server%d" % i) for i in xrange(10)]:
2063+            shares3[i] = set([server])
2064+            t = FakePeerTracker()
2065+            t.peerid = server
2066+            t.buckets = [i]
2067+            trackers.append(t)
2068+            expected[i] = set([server])
2069+        self.failUnlessEqual(expected, merge_peers(shares3, set(trackers)))
2070+
2071+
2072+    def test_servers_of_happiness_utility_function(self):
2073+        # These tests are concerned with the servers_of_happiness()
2074+        # utility function, and its underlying matching algorithm. Other
2075+        # aspects of the servers_of_happiness behavior are tested
2076+        # elsehwere These tests exist to ensure that
2077+        # servers_of_happiness doesn't under or overcount the happiness
2078+        # value for given inputs.
2079+
2080+        # servers_of_happiness expects a dict of
2081+        # shnum => set(peerids) as a preexisting shares argument.
2082         test1 = {
2083hunk ./src/allmydata/test/test_upload.py 1275
2084-                 1 : "server1",
2085-                 2 : "server2",
2086-                 3 : "server3",
2087-                 4 : "server4"
2088+                 1 : set(["server1"]),
2089+                 2 : set(["server2"]),
2090+                 3 : set(["server3"]),
2091+                 4 : set(["server4"])
2092                 }
2093hunk ./src/allmydata/test/test_upload.py 1280
2094-        unique_servers = upload.servers_with_unique_shares(test1)
2095-        self.failUnlessEqual(4, len(unique_servers))
2096-        for server in ["server1", "server2", "server3", "server4"]:
2097-            self.failUnlessIn(server, unique_servers)
2098-        test1[4] = "server1"
2099-        # Now there should only be 3 unique servers.
2100-        unique_servers = upload.servers_with_unique_shares(test1)
2101-        self.failUnlessEqual(3, len(unique_servers))
2102-        for server in ["server1", "server2", "server3"]:
2103-            self.failUnlessIn(server, unique_servers)
2104-        # servers_with_unique_shares expects to receive some object with
2105-        # a peerid attribute. So we make a FakePeerTracker whose only
2106-        # job is to have a peerid attribute.
2107+        happy = servers_of_happiness(test1)
2108+        self.failUnlessEqual(4, happy)
2109+        test1[4] = set(["server1"])
2110+        # We've added a duplicate server, so now servers_of_happiness
2111+        # should be 3 instead of 4.
2112+        happy = servers_of_happiness(test1)
2113+        self.failUnlessEqual(3, happy)
2114+        # The second argument of merge_peers should be a set of
2115+        # objects with peerid and buckets as attributes. In actual use,
2116+        # these will be PeerTracker instances, but for testing it is fine
2117+        # to make a FakePeerTracker whose job is to hold those instance
2118+        # variables to test that part.
2119         class FakePeerTracker:
2120             pass
2121         trackers = []
2122hunk ./src/allmydata/test/test_upload.py 1300
2123             t.peerid = server
2124             t.buckets = [i]
2125             trackers.append(t)
2126-        # Recall that there are 3 unique servers in test1. Since none of
2127-        # those overlap with the ones in trackers, we should get 7 back
2128-        unique_servers = upload.servers_with_unique_shares(test1, set(trackers))
2129-        self.failUnlessEqual(7, len(unique_servers))
2130-        expected_servers = ["server" + str(i) for i in xrange(1, 9)]
2131-        expected_servers.remove("server4")
2132-        for server in expected_servers:
2133-            self.failUnlessIn(server, unique_servers)
2134-        # Now add an overlapping server to trackers.
2135+        # Recall that test1 is a server layout with servers_of_happiness
2136+        # = 3.  Since there isn't any overlap between the shnum ->
2137+        # set([peerid]) correspondences in test1 and those in trackers,
2138+        # the result here should be 7.
2139+        test2 = merge_peers(test1, set(trackers))
2140+        happy = servers_of_happiness(test2)
2141+        self.failUnlessEqual(7, happy)
2142+        # Now add an overlapping server to trackers. This is redundant,
2143+        # so it should not cause the previously reported happiness value
2144+        # to change.
2145         t = FakePeerTracker()
2146         t.peerid = "server1"
2147         t.buckets = [1]
2148hunk ./src/allmydata/test/test_upload.py 1314
2149         trackers.append(t)
2150-        unique_servers = upload.servers_with_unique_shares(test1, set(trackers))
2151-        self.failUnlessEqual(7, len(unique_servers))
2152-        for server in expected_servers:
2153-            self.failUnlessIn(server, unique_servers)
2154+        test2 = merge_peers(test1, set(trackers))
2155+        happy = servers_of_happiness(test2)
2156+        self.failUnlessEqual(7, happy)
2157         test = {}
2158hunk ./src/allmydata/test/test_upload.py 1318
2159-        unique_servers = upload.servers_with_unique_shares(test)
2160-        self.failUnlessEqual(0, len(test))
2161+        happy = servers_of_happiness(test)
2162+        self.failUnlessEqual(0, happy)
2163+        # Test a more substantial overlap between the trackers and the
2164+        # existing assignments.
2165+        test = {
2166+            1 : set(['server1']),
2167+            2 : set(['server2']),
2168+            3 : set(['server3']),
2169+            4 : set(['server4']),
2170+        }
2171+        trackers = []
2172+        t = FakePeerTracker()
2173+        t.peerid = 'server5'
2174+        t.buckets = [4]
2175+        trackers.append(t)
2176+        t = FakePeerTracker()
2177+        t.peerid = 'server6'
2178+        t.buckets = [3, 5]
2179+        trackers.append(t)
2180+        # The value returned by servers_of_happiness is the size
2181+        # of a maximum matching in the bipartite graph that
2182+        # servers_of_happiness() makes between peerids and share
2183+        # numbers. It should find something like this:
2184+        # (server 1, share 1)
2185+        # (server 2, share 2)
2186+        # (server 3, share 3)
2187+        # (server 5, share 4)
2188+        # (server 6, share 5)
2189+        #
2190+        # and, since there are 5 edges in this matching, it should
2191+        # return 5.
2192+        test2 = merge_peers(test, set(trackers))
2193+        happy = servers_of_happiness(test2)
2194+        self.failUnlessEqual(5, happy)
2195+        # Zooko's first puzzle:
2196+        # (from http://allmydata.org/trac/tahoe-lafs/ticket/778#comment:156)
2197+        #
2198+        # server 1: shares 0, 1
2199+        # server 2: shares 1, 2
2200+        # server 3: share 2
2201+        #
2202+        # This should yield happiness of 3.
2203+        test = {
2204+            0 : set(['server1']),
2205+            1 : set(['server1', 'server2']),
2206+            2 : set(['server2', 'server3']),
2207+        }
2208+        self.failUnlessEqual(3, servers_of_happiness(test))
2209+        # Zooko's second puzzle:       
2210+        # (from http://allmydata.org/trac/tahoe-lafs/ticket/778#comment:158)
2211+        #
2212+        # server 1: shares 0, 1
2213+        # server 2: share 1
2214+        #
2215+        # This should yield happiness of 2.
2216+        test = {
2217+            0 : set(['server1']),
2218+            1 : set(['server1', 'server2']),
2219+        }
2220+        self.failUnlessEqual(2, servers_of_happiness(test))
2221 
2222 
2223     def test_shares_by_server(self):
2224hunk ./src/allmydata/test/test_upload.py 1381
2225-        test = dict([(i, "server%d" % i) for i in xrange(1, 5)])
2226-        shares_by_server = upload.shares_by_server(test)
2227-        self.failUnlessEqual(set([1]), shares_by_server["server1"])
2228-        self.failUnlessEqual(set([2]), shares_by_server["server2"])
2229-        self.failUnlessEqual(set([3]), shares_by_server["server3"])
2230-        self.failUnlessEqual(set([4]), shares_by_server["server4"])
2231+        test = dict([(i, set(["server%d" % i])) for i in xrange(1, 5)])
2232+        sbs = shares_by_server(test)
2233+        self.failUnlessEqual(set([1]), sbs["server1"])
2234+        self.failUnlessEqual(set([2]), sbs["server2"])
2235+        self.failUnlessEqual(set([3]), sbs["server3"])
2236+        self.failUnlessEqual(set([4]), sbs["server4"])
2237         test1 = {
2238hunk ./src/allmydata/test/test_upload.py 1388
2239-                    1 : "server1",
2240-                    2 : "server1",
2241-                    3 : "server1",
2242-                    4 : "server2",
2243-                    5 : "server2"
2244+                    1 : set(["server1"]),
2245+                    2 : set(["server1"]),
2246+                    3 : set(["server1"]),
2247+                    4 : set(["server2"]),
2248+                    5 : set(["server2"])
2249                 }
2250hunk ./src/allmydata/test/test_upload.py 1394
2251-        shares_by_server = upload.shares_by_server(test1)
2252-        self.failUnlessEqual(set([1, 2, 3]), shares_by_server["server1"])
2253-        self.failUnlessEqual(set([4, 5]), shares_by_server["server2"])
2254+        sbs = shares_by_server(test1)
2255+        self.failUnlessEqual(set([1, 2, 3]), sbs["server1"])
2256+        self.failUnlessEqual(set([4, 5]), sbs["server2"])
2257+        # This should fail unless the peerid part of the mapping is a set
2258+        test2 = {1: "server1"}
2259+        self.shouldFail(AssertionError,
2260+                       "test_shares_by_server",
2261+                       "",
2262+                       shares_by_server, test2)
2263 
2264 
2265     def test_existing_share_detection(self):
2266hunk ./src/allmydata/test/test_upload.py 1409
2267         self.basedir = self.mktemp()
2268         d = self._setup_and_upload()
2269         # Our final setup should look like this:
2270-        # server 1: shares 1 - 10, read-only
2271+        # server 1: shares 0 - 9, read-only
2272         # server 2: empty
2273         # server 3: empty
2274         # server 4: empty
2275hunk ./src/allmydata/test/test_upload.py 1441
2276         return d
2277 
2278 
2279-    def test_should_add_server(self):
2280-        shares = dict([(i, "server%d" % i) for i in xrange(10)])
2281-        self.failIf(upload.should_add_server(shares, "server1", 4))
2282-        shares[4] = "server1"
2283-        self.failUnless(upload.should_add_server(shares, "server4", 4))
2284-        shares = {}
2285-        self.failUnless(upload.should_add_server(shares, "server1", 1))
2286-
2287-
2288     def test_exception_messages_during_peer_selection(self):
2289hunk ./src/allmydata/test/test_upload.py 1442
2290-        # server 1: readonly, no shares
2291-        # server 2: readonly, no shares
2292-        # server 3: readonly, no shares
2293-        # server 4: readonly, no shares
2294-        # server 5: readonly, no shares
2295+        # server 1: read-only, no shares
2296+        # server 2: read-only, no shares
2297+        # server 3: read-only, no shares
2298+        # server 4: read-only, no shares
2299+        # server 5: read-only, no shares
2300         # This will fail, but we want to make sure that the log messages
2301         # are informative about why it has failed.
2302         self.basedir = self.mktemp()
2303hunk ./src/allmydata/test/test_upload.py 1472
2304             self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
2305                             "peer selection failed for <Tahoe2PeerSelector "
2306                             "for upload dglev>: placed 0 shares out of 10 "
2307-                            "total (10 homeless), want to place on 4 servers,"
2308-                            " sent 5 queries to 5 peers, 0 queries placed "
2309+                            "total (10 homeless), want to place shares on at "
2310+                            "least 4 servers such that any 3 of them have "
2311+                            "enough shares to recover the file, "
2312+                            "sent 5 queries to 5 peers, 0 queries placed "
2313                             "some shares, 5 placed none "
2314                             "(of which 5 placed none due to the server being "
2315                             "full and 0 placed none due to an error)",
2316hunk ./src/allmydata/test/test_upload.py 1483
2317                             upload.Data("data" * 10000, convergence="")))
2318 
2319 
2320-        # server 1: readonly, no shares
2321+        # server 1: read-only, no shares
2322         # server 2: broken, no shares
2323hunk ./src/allmydata/test/test_upload.py 1485
2324-        # server 3: readonly, no shares
2325-        # server 4: readonly, no shares
2326-        # server 5: readonly, no shares
2327+        # server 3: read-only, no shares
2328+        # server 4: read-only, no shares
2329+        # server 5: read-only, no shares
2330         def _reset(ign):
2331             self.basedir = self.mktemp()
2332         d.addCallback(_reset)
2333hunk ./src/allmydata/test/test_upload.py 1500
2334         def _break_server_2(ign):
2335             server = self.g.servers_by_number[2].my_nodeid
2336             # We have to break the server in servers_by_id,
2337-            # because the ones in servers_by_number isn't wrapped,
2338-            # and doesn't look at its broken attribute
2339+            # because the one in servers_by_number isn't wrapped,
2340+            # and doesn't look at its broken attribute when answering
2341+            # queries.
2342             self.g.servers_by_id[server].broken = True
2343         d.addCallback(_break_server_2)
2344         d.addCallback(lambda ign:
2345hunk ./src/allmydata/test/test_upload.py 1513
2346             self._add_server_with_share(server_number=5, readonly=True))
2347         d.addCallback(lambda ign:
2348             self.g.remove_server(self.g.servers_by_number[0].my_nodeid))
2349-        def _reset_encoding_parameters(ign):
2350+        def _reset_encoding_parameters(ign, happy=4):
2351             client = self.g.clients[0]
2352hunk ./src/allmydata/test/test_upload.py 1515
2353-            client.DEFAULT_ENCODING_PARAMETERS['happy'] = 4
2354+            client.DEFAULT_ENCODING_PARAMETERS['happy'] = happy
2355             return client
2356         d.addCallback(_reset_encoding_parameters)
2357         d.addCallback(lambda client:
2358hunk ./src/allmydata/test/test_upload.py 1522
2359             self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
2360                             "peer selection failed for <Tahoe2PeerSelector "
2361                             "for upload dglev>: placed 0 shares out of 10 "
2362-                            "total (10 homeless), want to place on 4 servers,"
2363-                            " sent 5 queries to 5 peers, 0 queries placed "
2364+                            "total (10 homeless), want to place shares on at "
2365+                            "least 4 servers such that any 3 of them have "
2366+                            "enough shares to recover the file, "
2367+                            "sent 5 queries to 5 peers, 0 queries placed "
2368                             "some shares, 5 placed none "
2369                             "(of which 4 placed none due to the server being "
2370                             "full and 1 placed none due to an error)",
2371hunk ./src/allmydata/test/test_upload.py 1531
2372                             client.upload,
2373                             upload.Data("data" * 10000, convergence="")))
2374+        # server 0, server 1 = empty, accepting shares
2375+        # This should place all of the shares, but still fail with happy=4.
2376+        # We want to make sure that the exception message is worded correctly.
2377+        d.addCallback(_reset)
2378+        d.addCallback(lambda ign:
2379+            self._setup_grid())
2380+        d.addCallback(lambda ign:
2381+            self._add_server_with_share(server_number=1))
2382+        d.addCallback(_reset_encoding_parameters)
2383+        d.addCallback(lambda client:
2384+            self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
2385+                            "shares could only be placed or found on 2 "
2386+                            "server(s). We were asked to place shares on at "
2387+                            "least 4 server(s) such that any 3 of them have "
2388+                            "enough shares to recover the file.",
2389+                            client.upload, upload.Data("data" * 10000,
2390+                                                       convergence="")))
2391+        # servers 0 - 4 = empty, accepting shares
2392+        # This too should place all the shares, and this too should fail,
2393+        # but since the effective happiness is more than the k encoding
2394+        # parameter, it should trigger a different error message than the one
2395+        # above.
2396+        d.addCallback(_reset)
2397+        d.addCallback(lambda ign:
2398+            self._setup_grid())
2399+        d.addCallback(lambda ign:
2400+            self._add_server_with_share(server_number=1))
2401+        d.addCallback(lambda ign:
2402+            self._add_server_with_share(server_number=2))
2403+        d.addCallback(lambda ign:
2404+            self._add_server_with_share(server_number=3))
2405+        d.addCallback(lambda ign:
2406+            self._add_server_with_share(server_number=4))
2407+        d.addCallback(_reset_encoding_parameters, happy=7)
2408+        d.addCallback(lambda client:
2409+            self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
2410+                            "shares could only be placed on 5 server(s) such "
2411+                            "that any 3 of them have enough shares to recover "
2412+                            "the file, but we were asked to place shares on "
2413+                            "at least 7 such servers.",
2414+                            client.upload, upload.Data("data" * 10000,
2415+                                                       convergence="")))
2416+        # server 0: shares 0 - 9
2417+        # server 1: share 0, read-only
2418+        # server 2: share 0, read-only
2419+        # server 3: share 0, read-only
2420+        # This should place all of the shares, but fail with happy=4.
2421+        # Since the number of servers with shares is more than the number
2422+        # necessary to reconstitute the file, this will trigger a different
2423+        # error message than either of those above.
2424+        d.addCallback(_reset)
2425+        d.addCallback(lambda ign:
2426+            self._setup_and_upload())
2427+        d.addCallback(lambda ign:
2428+            self._add_server_with_share(server_number=1, share_number=0,
2429+                                        readonly=True))
2430+        d.addCallback(lambda ign:
2431+            self._add_server_with_share(server_number=2, share_number=0,
2432+                                        readonly=True))
2433+        d.addCallback(lambda ign:
2434+            self._add_server_with_share(server_number=3, share_number=0,
2435+                                        readonly=True))
2436+        d.addCallback(_reset_encoding_parameters, happy=7)
2437+        d.addCallback(lambda client:
2438+            self.shouldFail(UploadUnhappinessError, "test_selection_exceptions",
2439+                            "shares could be placed or found on 4 server(s), "
2440+                            "but they are not spread out evenly enough to "
2441+                            "ensure that any 3 of these servers would have "
2442+                            "enough shares to recover the file. We were asked "
2443+                            "to place shares on at least 7 servers such that "
2444+                            "any 3 of them have enough shares to recover the "
2445+                            "file",
2446+                            client.upload, upload.Data("data" * 10000,
2447+                                                       convergence="")))
2448         return d
2449 
2450 
2451}
2452
2453Context:
2454
2455[Dependency on Windmill test framework is not needed yet.
2456david-sarah@jacaranda.org**20100504161043
2457 Ignore-this: be088712bec650d4ef24766c0026ebc8
2458]
2459[tests: pass z to tar so that BSD tar will know to ungzip
2460zooko@zooko.com**20100504090628
2461 Ignore-this: 1339e493f255e8fc0b01b70478f23a09
2462]
2463[setup: update comments and URLs in setup.cfg
2464zooko@zooko.com**20100504061653
2465 Ignore-this: f97692807c74bcab56d33100c899f829
2466]
2467[setup: reorder and extend the show-tool-versions script, the better to glean information about our new buildslaves
2468zooko@zooko.com**20100504045643
2469 Ignore-this: 836084b56b8d4ee8f1de1f4efb706d36
2470]
2471[CLI: Support for https url in option --node-url
2472Francois Deppierraz <francois@ctrlaltdel.ch>**20100430185609
2473 Ignore-this: 1717176b4d27c877e6bc67a944d9bf34
2474 
2475 This patch modifies the regular expression used for verifying of '--node-url'
2476 parameter.  Support for accessing a Tahoe gateway over HTTPS was already
2477 present, thanks to Python's urllib.
2478 
2479]
2480[backupdb.did_create_directory: use REPLACE INTO, not INSERT INTO + ignore error
2481Brian Warner <warner@lothar.com>**20100428050803
2482 Ignore-this: 1fca7b8f364a21ae413be8767161e32f
2483 
2484 This handles the case where we upload a new tahoe directory for a
2485 previously-processed local directory, possibly creating a new dircap (if the
2486 metadata had changed). Now we replace the old dirhash->dircap record. The
2487 previous behavior left the old record in place (with the old dircap and
2488 timestamps), so we'd never stop creating new directories and never converge
2489 on a null backup.
2490]
2491["tahoe webopen": add --info flag, to get ?t=info
2492Brian Warner <warner@lothar.com>**20100424233003
2493 Ignore-this: 126b0bb6db340fabacb623d295eb45fa
2494 
2495 Also fix some trailing whitespace.
2496]
2497[docs: install.html http-equiv refresh to quickstart.html
2498zooko@zooko.com**20100421165708
2499 Ignore-this: 52b4b619f9dde5886ae2cd7f1f3b734b
2500]
2501[docs: install.html -> quickstart.html
2502zooko@zooko.com**20100421155757
2503 Ignore-this: 6084e203909306bed93efb09d0e6181d
2504 It is not called "installing" because that implies that it is going to change the configuration of your operating system. It is not called "building" because that implies that you need developer tools like a compiler. Also I added a stern warning against looking at the "InstallDetails" wiki page, which I have renamed to "AdvancedInstall".
2505]
2506[Fix another typo in tahoe_storagespace munin plugin
2507david-sarah@jacaranda.org**20100416220935
2508 Ignore-this: ad1f7aa66b554174f91dfb2b7a3ea5f3
2509]
2510[Add dependency on windmill >= 1.3
2511david-sarah@jacaranda.org**20100416190404
2512 Ignore-this: 4437a7a464e92d6c9012926b18676211
2513]
2514[licensing: phrase the OpenSSL-exemption in the vocabulary of copyright instead of computer technology, and replicate the exemption from the GPL to the TGPPL
2515zooko@zooko.com**20100414232521
2516 Ignore-this: a5494b2f582a295544c6cad3f245e91
2517]
2518[munin-tahoe_storagespace
2519freestorm77@gmail.com**20100221203626
2520 Ignore-this: 14d6d6a587afe1f8883152bf2e46b4aa
2521 
2522 Plugin configuration rename
2523 
2524]
2525[setup: add licensing declaration for setuptools (noticed by the FSF compliance folks)
2526zooko@zooko.com**20100309184415
2527 Ignore-this: 2dfa7d812d65fec7c72ddbf0de609ccb
2528]
2529[setup: fix error in licensing declaration from Shawn Willden, as noted by the FSF compliance division
2530zooko@zooko.com**20100309163736
2531 Ignore-this: c0623d27e469799d86cabf67921a13f8
2532]
2533[CREDITS to Jacob Appelbaum
2534zooko@zooko.com**20100304015616
2535 Ignore-this: 70db493abbc23968fcc8db93f386ea54
2536]
2537[desert-island-build-with-proper-versions
2538jacob@appelbaum.net**20100304013858]
2539[docs: a few small edits to try to guide newcomers through the docs
2540zooko@zooko.com**20100303231902
2541 Ignore-this: a6aab44f5bf5ad97ea73e6976bc4042d
2542 These edits were suggested by my watching over Jake Appelbaum's shoulder as he completely ignored/skipped/missed install.html and also as he decided that debian.txt wouldn't help him with basic installation. Then I threw in a few docs edits that have been sitting around in my sandbox asking to be committed for months.
2543]
2544[TAG allmydata-tahoe-1.6.1
2545david-sarah@jacaranda.org**20100228062314
2546 Ignore-this: eb5f03ada8ea953ee7780e7fe068539
2547]
2548Patch bundle hash:
2549a703df8b85684b2a84e19d5f1e7d055c7ee1d317