Rewrite immutable downloader (#798). This patch adds and updates unit tests.

This commit is contained in:
Brian Warner 2010-08-04 00:27:10 -07:00
parent 7b7b0c9709
commit 63b61ce7bd
13 changed files with 1043 additions and 639 deletions

View File

@ -223,6 +223,7 @@ class NoNetworkGrid(service.MultiService):
fileutil.make_dirs(serverdir)
ss = StorageServer(serverdir, serverid, stats_provider=SimpleStats(),
readonly_storage=readonly)
ss._no_network_server_number = i
return ss
def add_server(self, i, ss):
@ -319,6 +320,16 @@ class GridTestMixin:
pass
return sorted(shares)
def copy_shares(self, uri):
shares = {}
for (shnum, serverid, sharefile) in self.find_uri_shares(uri):
shares[sharefile] = open(sharefile, "rb").read()
return shares
def restore_all_shares(self, shares):
for sharefile, data in shares.items():
open(sharefile, "wb").write(data)
def delete_share(self, (shnum, serverid, sharefile)):
os.unlink(sharefile)
@ -339,6 +350,12 @@ class GridTestMixin:
corruptdata = corruptor(sharedata, debug=debug)
open(i_sharefile, "wb").write(corruptdata)
def corrupt_all_shares(self, uri, corruptor, debug=False):
for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri):
sharedata = open(i_sharefile, "rb").read()
corruptdata = corruptor(sharedata, debug=debug)
open(i_sharefile, "wb").write(corruptdata)
def GET(self, urlpath, followRedirect=False, return_response=False,
method="GET", clientnum=0, **kwargs):
# if return_response=True, this fires with (data, statuscode,

View File

@ -2300,12 +2300,19 @@ class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
self.delete_shares_numbered(ur.uri, range(1,10))
d.addCallback(_stash_bad)
# the download is abandoned as soon as it's clear that we won't get
# enough shares. The one remaining share might be in either the
# COMPLETE or the PENDING state.
in_complete_msg = "ran out of shares: 1 complete, 0 pending, 0 overdue, 0 unused, need 3"
in_pending_msg = "ran out of shares: 0 complete, 1 pending, 0 overdue, 0 unused, need 3"
d.addCallback(lambda ign: self.do_cli("get", self.uri_1share))
def _check1((rc, out, err)):
self.failIfEqual(rc, 0)
self.failUnless("410 Gone" in err, err)
self.failUnlessIn("NotEnoughSharesError: ", err)
self.failUnlessIn("Failed to get enough shareholders: have 1, need 3", err)
self.failUnless(in_complete_msg in err or in_pending_msg in err,
err)
d.addCallback(_check1)
targetf = os.path.join(self.basedir, "output")
@ -2314,7 +2321,8 @@ class Errors(GridTestMixin, CLITestMixin, unittest.TestCase):
self.failIfEqual(rc, 0)
self.failUnless("410 Gone" in err, err)
self.failUnlessIn("NotEnoughSharesError: ", err)
self.failUnlessIn("Failed to get enough shareholders: have 1, need 3", err)
self.failUnless(in_complete_msg in err or in_pending_msg in err,
err)
self.failIf(os.path.exists(targetf))
d.addCallback(_check2)

View File

@ -1202,7 +1202,7 @@ class Packing(testutil.ReallyEqualMixin, unittest.TestCase):
def test_unpack_and_pack_behavior(self):
known_tree = b32decode(self.known_tree)
nodemaker = NodeMaker(None, None, None,
None, None, None,
None, None,
{"k": 3, "n": 10}, None)
write_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
filenode = nodemaker.create_from_cap(write_uri)
@ -1264,8 +1264,7 @@ class Packing(testutil.ReallyEqualMixin, unittest.TestCase):
return kids
def test_deep_immutable(self):
nm = NodeMaker(None, None, None, None, None, None, {"k": 3, "n": 10},
None)
nm = NodeMaker(None, None, None, None, None, {"k": 3, "n": 10}, None)
fn = MinimalFakeMutableFile()
kids = self._make_kids(nm, ["imm", "lit", "write", "read",
@ -1359,7 +1358,7 @@ class FakeNodeMaker(NodeMaker):
class FakeClient2(Client):
def __init__(self):
self.nodemaker = FakeNodeMaker(None, None, None,
None, None, None,
None, None,
{"k":3,"n":10}, None)
def create_node_from_uri(self, rwcap, rocap):
return self.nodemaker.create_from_cap(rwcap, rocap)
@ -1643,8 +1642,7 @@ class Deleter(GridTestMixin, testutil.ReallyEqualMixin, unittest.TestCase):
def _do_delete(ignored):
nm = UCWEingNodeMaker(c0.storage_broker, c0._secret_holder,
c0.get_history(), c0.getServiceNamed("uploader"),
c0.downloader,
c0.download_cache_dirman,
c0.terminator,
c0.get_encoding_parameters(),
c0._key_generator)
n = nm.create_from_cap(self.root_uri)

View File

@ -5,12 +5,19 @@
import os
from twisted.trial import unittest
from twisted.internet import defer, reactor
from allmydata import uri
from allmydata.storage.server import storage_index_to_dir
from allmydata.util import base32, fileutil
from allmydata.util.consumer import download_to_data
from allmydata.immutable import upload
from allmydata.util import base32, fileutil, spans, log
from allmydata.util.consumer import download_to_data, MemoryConsumer
from allmydata.immutable import upload, layout
from allmydata.test.no_network import GridTestMixin
from allmydata.test.common import ShouldFailMixin
from allmydata.interfaces import NotEnoughSharesError, NoSharesError
from allmydata.immutable.downloader.common import BadSegmentNumberError, \
BadCiphertextHashError, DownloadStopped
from allmydata.codec import CRSDecoder
from foolscap.eventual import fireEventually, flushEventualQueue
plaintext = "This is a moderate-sized file.\n" * 10
mutable_plaintext = "This is a moderate-sized mutable file.\n" * 10
@ -68,20 +75,7 @@ mutable_shares = {
}
#--------- END stored_shares.py ----------------
class DownloadTest(GridTestMixin, unittest.TestCase):
timeout = 2400 # It takes longer than 240 seconds on Zandr's ARM box.
def test_download(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# do this to create the shares
#return self.create_shares()
self.load_shares()
d = self.download_immutable()
d.addCallback(self.download_mutable)
return d
class _Base(GridTestMixin, ShouldFailMixin):
def create_shares(self, ignored=None):
u = upload.Data(plaintext, None)
@ -178,6 +172,9 @@ class DownloadTest(GridTestMixin, unittest.TestCase):
def _got_data(data):
self.failUnlessEqual(data, plaintext)
d.addCallback(_got_data)
# make sure we can use the same node twice
d.addCallback(lambda ign: download_to_data(n))
d.addCallback(_got_data)
return d
def download_mutable(self, ignored=None):
@ -188,3 +185,867 @@ class DownloadTest(GridTestMixin, unittest.TestCase):
d.addCallback(_got_data)
return d
class DownloadTest(_Base, unittest.TestCase):
timeout = 2400 # It takes longer than 240 seconds on Zandr's ARM box.
def test_download(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# do this to create the shares
#return self.create_shares()
self.load_shares()
d = self.download_immutable()
d.addCallback(self.download_mutable)
return d
def test_download_failover(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
si = uri.from_string(immutable_uri).get_storage_index()
si_dir = storage_index_to_dir(si)
n = self.c0.create_node_from_uri(immutable_uri)
d = download_to_data(n)
def _got_data(data):
self.failUnlessEqual(data, plaintext)
d.addCallback(_got_data)
def _clobber_some_shares(ign):
# find the three shares that were used, and delete them. Then
# download again, forcing the downloader to fail over to other
# shares
for s in n._cnode._node._shares:
for clientnum in immutable_shares:
for shnum in immutable_shares[clientnum]:
if s._shnum == shnum:
fn = os.path.join(self.get_serverdir(clientnum),
"shares", si_dir, str(shnum))
os.unlink(fn)
d.addCallback(_clobber_some_shares)
d.addCallback(lambda ign: download_to_data(n))
d.addCallback(_got_data)
def _clobber_most_shares(ign):
# delete all but one of the shares that are still alive
live_shares = [s for s in n._cnode._node._shares if s.is_alive()]
save_me = live_shares[0]._shnum
for clientnum in immutable_shares:
for shnum in immutable_shares[clientnum]:
if shnum == save_me:
continue
fn = os.path.join(self.get_serverdir(clientnum),
"shares", si_dir, str(shnum))
if os.path.exists(fn):
os.unlink(fn)
# now the download should fail with NotEnoughSharesError
return self.shouldFail(NotEnoughSharesError, "1shares", None,
download_to_data, n)
d.addCallback(_clobber_most_shares)
def _clobber_all_shares(ign):
# delete the last remaining share
for clientnum in immutable_shares:
for shnum in immutable_shares[clientnum]:
fn = os.path.join(self.get_serverdir(clientnum),
"shares", si_dir, str(shnum))
if os.path.exists(fn):
os.unlink(fn)
# now a new download should fail with NoSharesError. We want a
# new ImmutableFileNode so it will forget about the old shares.
# If we merely called create_node_from_uri() without first
# dereferencing the original node, the NodeMaker's _node_cache
# would give us back the old one.
n = None
n = self.c0.create_node_from_uri(immutable_uri)
return self.shouldFail(NoSharesError, "0shares", None,
download_to_data, n)
d.addCallback(_clobber_all_shares)
return d
def test_lost_servers(self):
# while downloading a file (after seg[0], before seg[1]), lose the
# three servers that we were using. The download should switch over
# to other servers.
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# upload a file with multiple segments, so we can catch the download
# in the middle.
u = upload.Data(plaintext, None)
u.max_segment_size = 70 # 5 segs
d = self.c0.upload(u)
def _uploaded(ur):
self.uri = ur.uri
self.n = self.c0.create_node_from_uri(self.uri)
return download_to_data(self.n)
d.addCallback(_uploaded)
def _got_data(data):
self.failUnlessEqual(data, plaintext)
d.addCallback(_got_data)
def _kill_some_servers():
# find the three shares that were used, and delete them. Then
# download again, forcing the downloader to fail over to other
# shares
servers = []
shares = sorted([s._shnum for s in self.n._cnode._node._shares])
self.failUnlessEqual(shares, [0,1,2])
# break the RIBucketReader references
for s in self.n._cnode._node._shares:
s._rref.broken = True
for servernum in immutable_shares:
for shnum in immutable_shares[servernum]:
if s._shnum == shnum:
ss = self.g.servers_by_number[servernum]
servers.append(ss)
# and, for good measure, break the RIStorageServer references
# too, just in case the downloader gets more aggressive in the
# future and tries to re-fetch the same share.
for ss in servers:
wrapper = self.g.servers_by_id[ss.my_nodeid]
wrapper.broken = True
def _download_again(ign):
c = StallingConsumer(_kill_some_servers)
return self.n.read(c)
d.addCallback(_download_again)
def _check_failover(c):
self.failUnlessEqual("".join(c.chunks), plaintext)
shares = sorted([s._shnum for s in self.n._cnode._node._shares])
# we should now be using more shares than we were before
self.failIfEqual(shares, [0,1,2])
d.addCallback(_check_failover)
return d
def test_badguess(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
# Cause the downloader to guess a segsize that's too low, so it will
# ask for a segment number that's too high (beyond the end of the
# real list, causing BadSegmentNumberError), to exercise
# Segmentation._retry_bad_segment
con1 = MemoryConsumer()
n._cnode._node._build_guessed_tables(90)
# plaintext size of 310 bytes, wrong-segsize of 90 bytes, will make
# us think that file[180:200] is in the third segment (segnum=2), but
# really there's only one segment
d = n.read(con1, 180, 20)
def _done(res):
self.failUnlessEqual("".join(con1.chunks), plaintext[180:200])
d.addCallback(_done)
return d
def test_simultaneous_badguess(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# upload a file with multiple segments, and a non-default segsize, to
# exercise the offset-guessing code. Because we don't tell the
# downloader about the unusual segsize, it will guess wrong, and have
# to do extra roundtrips to get the correct data.
u = upload.Data(plaintext, None)
u.max_segment_size = 70 # 5 segs, 8-wide hashtree
con1 = MemoryConsumer()
con2 = MemoryConsumer()
d = self.c0.upload(u)
def _uploaded(ur):
n = self.c0.create_node_from_uri(ur.uri)
d1 = n.read(con1, 70, 20)
d2 = n.read(con2, 140, 20)
return defer.gatherResults([d1,d2])
d.addCallback(_uploaded)
def _done(res):
self.failUnlessEqual("".join(con1.chunks), plaintext[70:90])
self.failUnlessEqual("".join(con2.chunks), plaintext[140:160])
d.addCallback(_done)
return d
def test_simultaneous_goodguess(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# upload a file with multiple segments, and a non-default segsize, to
# exercise the offset-guessing code. This time we *do* tell the
# downloader about the unusual segsize, so it can guess right.
u = upload.Data(plaintext, None)
u.max_segment_size = 70 # 5 segs, 8-wide hashtree
con1 = MemoryConsumer()
con2 = MemoryConsumer()
d = self.c0.upload(u)
def _uploaded(ur):
n = self.c0.create_node_from_uri(ur.uri)
n._cnode._node._build_guessed_tables(u.max_segment_size)
d1 = n.read(con1, 70, 20)
#d2 = n.read(con2, 140, 20) # XXX
d2 = defer.succeed(None)
return defer.gatherResults([d1,d2])
d.addCallback(_uploaded)
def _done(res):
self.failUnlessEqual("".join(con1.chunks), plaintext[70:90])
self.failUnlessEqual("".join(con2.chunks), plaintext[140:160])
#d.addCallback(_done)
return d
def test_sequential_goodguess(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
data = (plaintext*100)[:30000] # multiple of k
# upload a file with multiple segments, and a non-default segsize, to
# exercise the offset-guessing code. This time we *do* tell the
# downloader about the unusual segsize, so it can guess right.
u = upload.Data(data, None)
u.max_segment_size = 6000 # 5 segs, 8-wide hashtree
con1 = MemoryConsumer()
con2 = MemoryConsumer()
d = self.c0.upload(u)
def _uploaded(ur):
n = self.c0.create_node_from_uri(ur.uri)
n._cnode._node._build_guessed_tables(u.max_segment_size)
d = n.read(con1, 12000, 20)
def _read1(ign):
self.failUnlessEqual("".join(con1.chunks), data[12000:12020])
return n.read(con2, 24000, 20)
d.addCallback(_read1)
def _read2(ign):
self.failUnlessEqual("".join(con2.chunks), data[24000:24020])
d.addCallback(_read2)
return d
d.addCallback(_uploaded)
return d
def test_simultaneous_get_blocks(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
stay_empty = []
n = self.c0.create_node_from_uri(immutable_uri)
d = download_to_data(n)
def _use_shares(ign):
shares = list(n._cnode._node._shares)
s0 = shares[0]
# make sure .cancel works too
o0 = s0.get_block(0)
o0.subscribe(lambda **kwargs: stay_empty.append(kwargs))
o1 = s0.get_block(0)
o2 = s0.get_block(0)
o0.cancel()
o3 = s0.get_block(1) # state=BADSEGNUM
d1 = defer.Deferred()
d2 = defer.Deferred()
d3 = defer.Deferred()
o1.subscribe(lambda **kwargs: d1.callback(kwargs))
o2.subscribe(lambda **kwargs: d2.callback(kwargs))
o3.subscribe(lambda **kwargs: d3.callback(kwargs))
return defer.gatherResults([d1,d2,d3])
d.addCallback(_use_shares)
def _done(res):
r1,r2,r3 = res
self.failUnlessEqual(r1["state"], "COMPLETE")
self.failUnlessEqual(r2["state"], "COMPLETE")
self.failUnlessEqual(r3["state"], "BADSEGNUM")
self.failUnless("block" in r1)
self.failUnless("block" in r2)
self.failIf(stay_empty)
d.addCallback(_done)
return d
def test_download_no_overrun(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
# tweak the client's copies of server-version data, so it believes
# that they're old and can't handle reads that overrun the length of
# the share. This exercises a different code path.
for (peerid, rref) in self.c0.storage_broker.get_all_servers():
v1 = rref.version["http://allmydata.org/tahoe/protocols/storage/v1"]
v1["tolerates-immutable-read-overrun"] = False
n = self.c0.create_node_from_uri(immutable_uri)
d = download_to_data(n)
def _got_data(data):
self.failUnlessEqual(data, plaintext)
d.addCallback(_got_data)
return d
def test_download_segment(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
cn = n._cnode
(d,c) = cn.get_segment(0)
def _got_segment((offset,data,decodetime)):
self.failUnlessEqual(offset, 0)
self.failUnlessEqual(len(data), len(plaintext))
d.addCallback(_got_segment)
return d
def test_download_segment_cancel(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
cn = n._cnode
(d,c) = cn.get_segment(0)
fired = []
d.addCallback(fired.append)
c.cancel()
d = fireEventually()
d.addCallback(flushEventualQueue)
def _check(ign):
self.failUnlessEqual(fired, [])
d.addCallback(_check)
return d
def test_download_bad_segment(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
cn = n._cnode
def _try_download():
(d,c) = cn.get_segment(1)
return d
d = self.shouldFail(BadSegmentNumberError, "badseg",
"segnum=1, numsegs=1",
_try_download)
return d
def test_download_segment_terminate(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
cn = n._cnode
(d,c) = cn.get_segment(0)
fired = []
d.addCallback(fired.append)
self.c0.terminator.disownServiceParent()
d = fireEventually()
d.addCallback(flushEventualQueue)
def _check(ign):
self.failUnlessEqual(fired, [])
d.addCallback(_check)
return d
def test_pause(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
c = PausingConsumer()
d = n.read(c)
def _downloaded(mc):
newdata = "".join(mc.chunks)
self.failUnlessEqual(newdata, plaintext)
d.addCallback(_downloaded)
return d
def test_pause_then_stop(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
c = PausingAndStoppingConsumer()
d = self.shouldFail(DownloadStopped, "test_pause_then_stop",
"our Consumer called stopProducing()",
n.read, c)
return d
def test_stop(self):
# use a download targetthat does an immediate stop (ticket #473)
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
c = StoppingConsumer()
d = self.shouldFail(DownloadStopped, "test_stop",
"our Consumer called stopProducing()",
n.read, c)
return d
def test_download_segment_bad_ciphertext_hash(self):
# The crypttext_hash_tree asserts the integrity of the decoded
# ciphertext, and exists to detect two sorts of problems. The first
# is a bug in zfec decode. The second is the "two-sided t-shirt"
# attack (found by Christian Grothoff), in which a malicious uploader
# creates two sets of shares (one for file A, second for file B),
# uploads a combination of them (shares 0-4 of A, 5-9 of B), and then
# builds an otherwise normal UEB around those shares: their goal is
# to give their victim a filecap which sometimes downloads the good A
# contents, and sometimes the bad B contents, depending upon which
# servers/shares they can get to. Having a hash of the ciphertext
# forces them to commit to exactly one version. (Christian's prize
# for finding this problem was a t-shirt with two sides: the shares
# of file A on the front, B on the back).
# creating a set of shares with this property is too hard, although
# it'd be nice to do so and confirm our fix. (it requires a lot of
# tampering with the uploader). So instead, we just damage the
# decoder. The tail decoder is rebuilt each time, so we need to use a
# file with multiple segments.
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
u = upload.Data(plaintext, None)
u.max_segment_size = 60 # 6 segs
d = self.c0.upload(u)
def _uploaded(ur):
n = self.c0.create_node_from_uri(ur.uri)
n._cnode._node._build_guessed_tables(u.max_segment_size)
d = download_to_data(n)
def _break_codec(data):
# the codec isn't created until the UEB is retrieved
node = n._cnode._node
vcap = node._verifycap
k, N = vcap.needed_shares, vcap.total_shares
bad_codec = BrokenDecoder()
bad_codec.set_params(node.segment_size, k, N)
node._codec = bad_codec
d.addCallback(_break_codec)
# now try to download it again. The broken codec will provide
# ciphertext that fails the hash test.
d.addCallback(lambda ign:
self.shouldFail(BadCiphertextHashError, "badhash",
"hash failure in "
"ciphertext_hash_tree: segnum=0",
download_to_data, n))
return d
d.addCallback(_uploaded)
return d
def OFFtest_download_segment_XXX(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# upload a file with multiple segments, and a non-default segsize, to
# exercise the offset-guessing code. This time we *do* tell the
# downloader about the unusual segsize, so it can guess right.
u = upload.Data(plaintext, None)
u.max_segment_size = 70 # 5 segs, 8-wide hashtree
con1 = MemoryConsumer()
con2 = MemoryConsumer()
d = self.c0.upload(u)
def _uploaded(ur):
n = self.c0.create_node_from_uri(ur.uri)
n._cnode._node._build_guessed_tables(u.max_segment_size)
d1 = n.read(con1, 70, 20)
#d2 = n.read(con2, 140, 20)
d2 = defer.succeed(None)
return defer.gatherResults([d1,d2])
d.addCallback(_uploaded)
def _done(res):
self.failUnlessEqual("".join(con1.chunks), plaintext[70:90])
self.failUnlessEqual("".join(con2.chunks), plaintext[140:160])
#d.addCallback(_done)
return d
def test_duplicate_shares(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
# make sure everybody has a copy of sh0. The second server contacted
# will report two shares, and the ShareFinder will handle the
# duplicate by attaching both to the same CommonShare instance.
si = uri.from_string(immutable_uri).get_storage_index()
si_dir = storage_index_to_dir(si)
sh0_file = [sharefile
for (shnum, serverid, sharefile)
in self.find_uri_shares(immutable_uri)
if shnum == 0][0]
sh0_data = open(sh0_file, "rb").read()
for clientnum in immutable_shares:
if 0 in immutable_shares[clientnum]:
continue
cdir = self.get_serverdir(clientnum)
target = os.path.join(cdir, "shares", si_dir, "0")
outf = open(target, "wb")
outf.write(sh0_data)
outf.close()
d = self.download_immutable()
return d
def test_verifycap(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
self.load_shares()
n = self.c0.create_node_from_uri(immutable_uri)
vcap = n.get_verify_cap().to_string()
vn = self.c0.create_node_from_uri(vcap)
d = download_to_data(vn)
def _got_ciphertext(ciphertext):
self.failUnlessEqual(len(ciphertext), len(plaintext))
self.failIfEqual(ciphertext, plaintext)
d.addCallback(_got_ciphertext)
return d
class BrokenDecoder(CRSDecoder):
def decode(self, shares, shareids):
d = CRSDecoder.decode(self, shares, shareids)
def _decoded(buffers):
def _corruptor(s, which):
return s[:which] + chr(ord(s[which])^0x01) + s[which+1:]
buffers[0] = _corruptor(buffers[0], 0) # flip lsb of first byte
return buffers
d.addCallback(_decoded)
return d
class PausingConsumer(MemoryConsumer):
def __init__(self):
MemoryConsumer.__init__(self)
self.size = 0
self.writes = 0
def write(self, data):
self.size += len(data)
self.writes += 1
if self.writes <= 2:
# we happen to use 4 segments, and want to avoid pausing on the
# last one (since then the _unpause timer will still be running)
self.producer.pauseProducing()
reactor.callLater(0.1, self._unpause)
return MemoryConsumer.write(self, data)
def _unpause(self):
self.producer.resumeProducing()
class PausingAndStoppingConsumer(PausingConsumer):
def write(self, data):
self.producer.pauseProducing()
reactor.callLater(0.5, self._stop)
def _stop(self):
self.producer.stopProducing()
class StoppingConsumer(PausingConsumer):
def write(self, data):
self.producer.stopProducing()
class StallingConsumer(MemoryConsumer):
def __init__(self, halfway_cb):
MemoryConsumer.__init__(self)
self.halfway_cb = halfway_cb
self.writes = 0
def write(self, data):
self.writes += 1
if self.writes == 1:
self.halfway_cb()
return MemoryConsumer.write(self, data)
class Corruption(_Base, unittest.TestCase):
def _corrupt_flip(self, ign, imm_uri, which):
log.msg("corrupt %d" % which)
def _corruptor(s, debug=False):
return s[:which] + chr(ord(s[which])^0x01) + s[which+1:]
self.corrupt_shares_numbered(imm_uri, [0], _corruptor)
def _corrupt_set(self, ign, imm_uri, which, newvalue):
log.msg("corrupt %d" % which)
def _corruptor(s, debug=False):
return s[:which] + chr(newvalue) + s[which+1:]
self.corrupt_shares_numbered(imm_uri, [0], _corruptor)
def test_each_byte(self):
# Setting catalog_detection=True performs an exhaustive test of the
# Downloader's response to corruption in the lsb of each byte of the
# 2070-byte share, with two goals: make sure we tolerate all forms of
# corruption (i.e. don't hang or return bad data), and make a list of
# which bytes can be corrupted without influencing the download
# (since we don't need every byte of the share). That takes 50s to
# run on my laptop and doesn't have any actual asserts, so we don't
# normally do that.
self.catalog_detection = False
self.basedir = "download/Corruption/each_byte"
self.set_up_grid()
self.c0 = self.g.clients[0]
# to exercise the block-hash-tree code properly, we need to have
# multiple segments. We don't tell the downloader about the different
# segsize, so it guesses wrong and must do extra roundtrips.
u = upload.Data(plaintext, None)
u.max_segment_size = 120 # 3 segs, 4-wide hashtree
if self.catalog_detection:
undetected = spans.Spans()
def _download(ign, imm_uri, which, expected):
n = self.c0.create_node_from_uri(imm_uri)
# for this test to work, we need to have a new Node each time.
# Make sure the NodeMaker's weakcache hasn't interfered.
assert not n._cnode._node._shares
d = download_to_data(n)
def _got_data(data):
self.failUnlessEqual(data, plaintext)
shnums = sorted([s._shnum for s in n._cnode._node._shares])
no_sh0 = bool(0 not in shnums)
sh0 = [s for s in n._cnode._node._shares if s._shnum == 0]
sh0_had_corruption = False
if sh0 and sh0[0].had_corruption:
sh0_had_corruption = True
num_needed = len(n._cnode._node._shares)
if self.catalog_detection:
detected = no_sh0 or sh0_had_corruption or (num_needed!=3)
if not detected:
undetected.add(which, 1)
if expected == "no-sh0":
self.failIfIn(0, shnums)
elif expected == "0bad-need-3":
self.failIf(no_sh0)
self.failUnless(sh0[0].had_corruption)
self.failUnlessEqual(num_needed, 3)
elif expected == "need-4th":
self.failIf(no_sh0)
self.failUnless(sh0[0].had_corruption)
self.failIfEqual(num_needed, 3)
d.addCallback(_got_data)
return d
d = self.c0.upload(u)
def _uploaded(ur):
imm_uri = ur.uri
self.shares = self.copy_shares(imm_uri)
d = defer.succeed(None)
# 'victims' is a list of corruption tests to run. Each one flips
# the low-order bit of the specified offset in the share file (so
# offset=0 is the MSB of the container version, offset=15 is the
# LSB of the share version, offset=24 is the MSB of the
# data-block-offset, and offset=48 is the first byte of the first
# data-block). Each one also specifies what sort of corruption
# we're expecting to see.
no_sh0_victims = [0,1,2,3] # container version
need3_victims = [ ] # none currently in this category
# when the offsets are corrupted, the Share will be unable to
# retrieve the data it wants (because it thinks that data lives
# off in the weeds somewhere), and Share treats DataUnavailable
# as abandon-this-share, so in general we'll be forced to look
# for a 4th share.
need_4th_victims = [12,13,14,15, # share version
24,25,26,27, # offset[data]
32,33,34,35, # offset[crypttext_hash_tree]
36,37,38,39, # offset[block_hashes]
44,45,46,47, # offset[UEB]
]
need_4th_victims.append(48) # block data
# when corrupting hash trees, we must corrupt a value that isn't
# directly set from somewhere else. Since we download data from
# seg0, corrupt something on its hash chain, like [2] (the
# right-hand child of the root)
need_4th_victims.append(600+2*32) # block_hashes[2]
# Share.loop is pretty conservative: it abandons the share at the
# first sign of corruption. It doesn't strictly need to be this
# way: if the UEB were corrupt, we could still get good block
# data from that share, as long as there was a good copy of the
# UEB elsewhere. If this behavior is relaxed, then corruption in
# the following fields (which are present in multiple shares)
# should fall into the "need3_victims" case instead of the
# "need_4th_victims" case.
need_4th_victims.append(376+2*32) # crypttext_hash_tree[2]
need_4th_victims.append(824) # share_hashes
need_4th_victims.append(994) # UEB length
need_4th_victims.append(998) # UEB
corrupt_me = ([(i,"no-sh0") for i in no_sh0_victims] +
[(i, "0bad-need-3") for i in need3_victims] +
[(i, "need-4th") for i in need_4th_victims])
if self.catalog_detection:
corrupt_me = [(i, "") for i in range(len(self.sh0_orig))]
for i,expected in corrupt_me:
# All these tests result in a successful download. What we're
# measuring is how many shares the downloader had to use.
d.addCallback(self._corrupt_flip, imm_uri, i)
d.addCallback(_download, imm_uri, i, expected)
d.addCallback(lambda ign: self.restore_all_shares(self.shares))
d.addCallback(fireEventually)
corrupt_values = [(3, 2, "no-sh0"),
(15, 2, "need-4th"), # share looks v2
]
for i,newvalue,expected in corrupt_values:
d.addCallback(self._corrupt_set, imm_uri, i, newvalue)
d.addCallback(_download, imm_uri, i, expected)
d.addCallback(lambda ign: self.restore_all_shares(self.shares))
d.addCallback(fireEventually)
return d
d.addCallback(_uploaded)
def _show_results(ign):
print
print ("of [0:%d], corruption ignored in %s" %
(len(self.sh0_orig), undetected.dump()))
if self.catalog_detection:
d.addCallback(_show_results)
# of [0:2070], corruption ignored in len=1133:
# [4-11],[16-23],[28-31],[152-439],[600-663],[1309-2069]
# [4-11]: container sizes
# [16-23]: share block/data sizes
# [152-375]: plaintext hash tree
# [376-408]: crypttext_hash_tree[0] (root)
# [408-439]: crypttext_hash_tree[1] (computed)
# [600-631]: block hash tree[0] (root)
# [632-663]: block hash tree[1] (computed)
# [1309-]: reserved+unused UEB space
return d
def test_failure(self):
# this test corrupts all shares in the same way, and asserts that the
# download fails.
self.basedir = "download/Corruption/failure"
self.set_up_grid()
self.c0 = self.g.clients[0]
# to exercise the block-hash-tree code properly, we need to have
# multiple segments. We don't tell the downloader about the different
# segsize, so it guesses wrong and must do extra roundtrips.
u = upload.Data(plaintext, None)
u.max_segment_size = 120 # 3 segs, 4-wide hashtree
d = self.c0.upload(u)
def _uploaded(ur):
imm_uri = ur.uri
self.shares = self.copy_shares(imm_uri)
corrupt_me = [(48, "block data", "Last failure: None"),
(600+2*32, "block_hashes[2]", "BadHashError"),
(376+2*32, "crypttext_hash_tree[2]", "BadHashError"),
(824, "share_hashes", "BadHashError"),
]
def _download(imm_uri):
n = self.c0.create_node_from_uri(imm_uri)
# for this test to work, we need to have a new Node each time.
# Make sure the NodeMaker's weakcache hasn't interfered.
assert not n._cnode._node._shares
return download_to_data(n)
d = defer.succeed(None)
for i,which,substring in corrupt_me:
# All these tests result in a failed download.
d.addCallback(self._corrupt_flip_all, imm_uri, i)
d.addCallback(lambda ign:
self.shouldFail(NotEnoughSharesError, which,
substring,
_download, imm_uri))
d.addCallback(lambda ign: self.restore_all_shares(self.shares))
d.addCallback(fireEventually)
return d
d.addCallback(_uploaded)
return d
def _corrupt_flip_all(self, ign, imm_uri, which):
def _corruptor(s, debug=False):
return s[:which] + chr(ord(s[which])^0x01) + s[which+1:]
self.corrupt_all_shares(imm_uri, _corruptor)
class DownloadV2(_Base, unittest.TestCase):
# tests which exercise v2-share code. They first upload a file with
# FORCE_V2 set.
def setUp(self):
d = defer.maybeDeferred(_Base.setUp, self)
def _set_force_v2(ign):
self.old_force_v2 = layout.FORCE_V2
layout.FORCE_V2 = True
d.addCallback(_set_force_v2)
return d
def tearDown(self):
layout.FORCE_V2 = self.old_force_v2
return _Base.tearDown(self)
def test_download(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# upload a file
u = upload.Data(plaintext, None)
d = self.c0.upload(u)
def _uploaded(ur):
imm_uri = ur.uri
n = self.c0.create_node_from_uri(imm_uri)
return download_to_data(n)
d.addCallback(_uploaded)
return d
def test_download_no_overrun(self):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
# tweak the client's copies of server-version data, so it believes
# that they're old and can't handle reads that overrun the length of
# the share. This exercises a different code path.
for (peerid, rref) in self.c0.storage_broker.get_all_servers():
v1 = rref.version["http://allmydata.org/tahoe/protocols/storage/v1"]
v1["tolerates-immutable-read-overrun"] = False
# upload a file
u = upload.Data(plaintext, None)
d = self.c0.upload(u)
def _uploaded(ur):
imm_uri = ur.uri
n = self.c0.create_node_from_uri(imm_uri)
return download_to_data(n)
d.addCallback(_uploaded)
return d
def OFF_test_no_overrun_corrupt_shver(self): # unnecessary
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
for (peerid, rref) in self.c0.storage_broker.get_all_servers():
v1 = rref.version["http://allmydata.org/tahoe/protocols/storage/v1"]
v1["tolerates-immutable-read-overrun"] = False
# upload a file
u = upload.Data(plaintext, None)
d = self.c0.upload(u)
def _uploaded(ur):
imm_uri = ur.uri
def _do_corrupt(which, newvalue):
def _corruptor(s, debug=False):
return s[:which] + chr(newvalue) + s[which+1:]
self.corrupt_shares_numbered(imm_uri, [0], _corruptor)
_do_corrupt(12+3, 0x00)
n = self.c0.create_node_from_uri(imm_uri)
d = download_to_data(n)
def _got_data(data):
self.failUnlessEqual(data, plaintext)
d.addCallback(_got_data)
return d
d.addCallback(_uploaded)
return d

View File

@ -1,17 +1,15 @@
from zope.interface import implements
from twisted.trial import unittest
from twisted.internet import defer, reactor
from twisted.internet import defer
from twisted.python.failure import Failure
from foolscap.api import fireEventually
from allmydata import hashtree, uri
from allmydata.immutable import encode, upload, download
from allmydata import uri
from allmydata.immutable import encode, upload, checker
from allmydata.util import hashutil
from allmydata.util.assertutil import _assert
from allmydata.util.consumer import MemoryConsumer
from allmydata.interfaces import IStorageBucketWriter, IStorageBucketReader, \
NotEnoughSharesError, IStorageBroker, UploadUnhappinessError
from allmydata.monitor import Monitor
import allmydata.test.common_util as testutil
from allmydata.util.consumer import download_to_data
from allmydata.interfaces import IStorageBucketWriter, IStorageBucketReader
from allmydata.test.no_network import GridTestMixin
class LostPeerError(Exception):
pass
@ -19,9 +17,6 @@ class LostPeerError(Exception):
def flip_bit(good): # flips the last bit
return good[:-1] + chr(ord(good[-1]) ^ 0x01)
class FakeStorageBroker:
implements(IStorageBroker)
class FakeBucketReaderWriterProxy:
implements(IStorageBucketWriter, IStorageBucketReader)
# these are used for both reading and writing
@ -59,13 +54,6 @@ class FakeBucketReaderWriterProxy:
self.blocks[segmentnum] = data
return defer.maybeDeferred(_try)
def put_plaintext_hashes(self, hashes):
def _try():
assert not self.closed
assert not self.plaintext_hashes
self.plaintext_hashes = hashes
return defer.maybeDeferred(_try)
def put_crypttext_hashes(self, hashes):
def _try():
assert not self.closed
@ -223,7 +211,7 @@ class ValidatedExtendedURIProxy(unittest.TestCase):
fb = FakeBucketReaderWriterProxy()
fb.put_uri_extension(uebstring)
verifycap = uri.CHKFileVerifierURI(storage_index='x'*16, uri_extension_hash=uebhash, needed_shares=self.K, total_shares=self.M, size=self.SIZE)
vup = download.ValidatedExtendedURIProxy(fb, verifycap)
vup = checker.ValidatedExtendedURIProxy(fb, verifycap)
return vup.start()
def _test_accept(self, uebdict):
@ -237,7 +225,7 @@ class ValidatedExtendedURIProxy(unittest.TestCase):
def _test_reject(self, uebdict):
d = self._test(uebdict)
d.addBoth(self._should_fail, (KeyError, download.BadURIExtension))
d.addBoth(self._should_fail, (KeyError, checker.BadURIExtension))
return d
def test_accept_minimal(self):
@ -333,30 +321,6 @@ class Encode(unittest.TestCase):
return d
# a series of 3*3 tests to check out edge conditions. One axis is how the
# plaintext is divided into segments: kn+(-1,0,1). Another way to express
# that is that n%k == -1 or 0 or 1. For example, for 25-byte segments, we
# might test 74 bytes, 75 bytes, and 76 bytes.
# on the other axis is how many leaves in the block hash tree we wind up
# with, relative to a power of 2, so 2^a+(-1,0,1). Each segment turns
# into a single leaf. So we'd like to check out, e.g., 3 segments, 4
# segments, and 5 segments.
# that results in the following series of data lengths:
# 3 segs: 74, 75, 51
# 4 segs: 99, 100, 76
# 5 segs: 124, 125, 101
# all tests encode to 100 shares, which means the share hash tree will
# have 128 leaves, which means that buckets will be given an 8-long share
# hash chain
# all 3-segment files will have a 4-leaf blockhashtree, and thus expect
# to get 7 blockhashes. 4-segment files will also get 4-leaf block hash
# trees and 7 blockhashes. 5-segment files will get 8-leaf block hash
# trees, which get 15 blockhashes.
def test_send_74(self):
# 3 segments (25, 25, 24)
return self.do_encode(25, 74, 100, 3, 7, 8)
@ -387,422 +351,62 @@ class Encode(unittest.TestCase):
# 5 segments: 25, 25, 25, 25, 1
return self.do_encode(25, 101, 100, 5, 15, 8)
class PausingConsumer(MemoryConsumer):
def __init__(self):
MemoryConsumer.__init__(self)
self.size = 0
self.writes = 0
def write(self, data):
self.size += len(data)
self.writes += 1
if self.writes <= 2:
# we happen to use 4 segments, and want to avoid pausing on the
# last one (since then the _unpause timer will still be running)
self.producer.pauseProducing()
reactor.callLater(0.1, self._unpause)
return MemoryConsumer.write(self, data)
def _unpause(self):
self.producer.resumeProducing()
class PausingAndStoppingConsumer(PausingConsumer):
def write(self, data):
self.producer.pauseProducing()
reactor.callLater(0.5, self._stop)
def _stop(self):
self.producer.stopProducing()
class Roundtrip(GridTestMixin, unittest.TestCase):
class StoppingConsumer(PausingConsumer):
def write(self, data):
self.producer.stopProducing()
# a series of 3*3 tests to check out edge conditions. One axis is how the
# plaintext is divided into segments: kn+(-1,0,1). Another way to express
# this is n%k == -1 or 0 or 1. For example, for 25-byte segments, we
# might test 74 bytes, 75 bytes, and 76 bytes.
class Roundtrip(unittest.TestCase, testutil.ShouldFailMixin):
timeout = 2400 # It takes longer than 240 seconds on Zandr's ARM box.
def send_and_recover(self, k_and_happy_and_n=(25,75,100),
AVAILABLE_SHARES=None,
datalen=76,
max_segment_size=25,
bucket_modes={},
recover_mode="recover",
consumer=None,
):
if AVAILABLE_SHARES is None:
AVAILABLE_SHARES = k_and_happy_and_n[2]
data = make_data(datalen)
d = self.send(k_and_happy_and_n, AVAILABLE_SHARES,
max_segment_size, bucket_modes, data)
# that fires with (uri_extension_hash, e, shareholders)
d.addCallback(self.recover, AVAILABLE_SHARES, recover_mode,
consumer=consumer)
# that fires with newdata
def _downloaded((newdata, fd)):
self.failUnless(newdata == data, str((len(newdata), len(data))))
return fd
# on the other axis is how many leaves in the block hash tree we wind up
# with, relative to a power of 2, so 2^a+(-1,0,1). Each segment turns
# into a single leaf. So we'd like to check out, e.g., 3 segments, 4
# segments, and 5 segments.
# that results in the following series of data lengths:
# 3 segs: 74, 75, 51
# 4 segs: 99, 100, 76
# 5 segs: 124, 125, 101
# all tests encode to 100 shares, which means the share hash tree will
# have 128 leaves, which means that buckets will be given an 8-long share
# hash chain
# all 3-segment files will have a 4-leaf blockhashtree, and thus expect
# to get 7 blockhashes. 4-segment files will also get 4-leaf block hash
# trees and 7 blockhashes. 5-segment files will get 8-leaf block hash
# trees, which gets 15 blockhashes.
def test_74(self): return self.do_test_size(74)
def test_75(self): return self.do_test_size(75)
def test_51(self): return self.do_test_size(51)
def test_99(self): return self.do_test_size(99)
def test_100(self): return self.do_test_size(100)
def test_76(self): return self.do_test_size(76)
def test_124(self): return self.do_test_size(124)
def test_125(self): return self.do_test_size(125)
def test_101(self): return self.do_test_size(101)
def upload(self, data):
u = upload.Data(data, None)
u.max_segment_size = 25
u.encoding_param_k = 25
u.encoding_param_happy = 1
u.encoding_param_n = 100
d = self.c0.upload(u)
d.addCallback(lambda ur: self.c0.create_node_from_uri(ur.uri))
# returns a FileNode
return d
def do_test_size(self, size):
self.basedir = self.mktemp()
self.set_up_grid()
self.c0 = self.g.clients[0]
DATA = "p"*size
d = self.upload(DATA)
d.addCallback(lambda n: download_to_data(n))
def _downloaded(newdata):
self.failUnlessEqual(newdata, DATA)
d.addCallback(_downloaded)
return d
def send(self, k_and_happy_and_n, AVAILABLE_SHARES, max_segment_size,
bucket_modes, data):
k, happy, n = k_and_happy_and_n
NUM_SHARES = k_and_happy_and_n[2]
if AVAILABLE_SHARES is None:
AVAILABLE_SHARES = NUM_SHARES
e = encode.Encoder()
u = upload.Data(data, convergence="some convergence string")
# force use of multiple segments by using a low max_segment_size
u.max_segment_size = max_segment_size
u.encoding_param_k = k
u.encoding_param_happy = happy
u.encoding_param_n = n
eu = upload.EncryptAnUploadable(u)
d = e.set_encrypted_uploadable(eu)
shareholders = {}
def _ready(res):
k,happy,n = e.get_param("share_counts")
assert n == NUM_SHARES # else we'll be completely confused
servermap = {}
for shnum in range(NUM_SHARES):
mode = bucket_modes.get(shnum, "good")
peer = FakeBucketReaderWriterProxy(mode, "peer%d" % shnum)
shareholders[shnum] = peer
servermap.setdefault(shnum, set()).add(peer.get_peerid())
e.set_shareholders(shareholders, servermap)
return e.start()
d.addCallback(_ready)
def _sent(res):
d1 = u.get_encryption_key()
d1.addCallback(lambda key: (res, key, shareholders))
return d1
d.addCallback(_sent)
return d
def recover(self, (res, key, shareholders), AVAILABLE_SHARES,
recover_mode, consumer=None):
verifycap = res
if "corrupt_key" in recover_mode:
# we corrupt the key, so that the decrypted data is corrupted and
# will fail the plaintext hash check. Since we're manually
# attaching shareholders, the fact that the storage index is also
# corrupted doesn't matter.
key = flip_bit(key)
u = uri.CHKFileURI(key=key,
uri_extension_hash=verifycap.uri_extension_hash,
needed_shares=verifycap.needed_shares,
total_shares=verifycap.total_shares,
size=verifycap.size)
sb = FakeStorageBroker()
if not consumer:
consumer = MemoryConsumer()
innertarget = download.ConsumerAdapter(consumer)
target = download.DecryptingTarget(innertarget, u.key)
fd = download.CiphertextDownloader(sb, u.get_verify_cap(), target, monitor=Monitor())
# we manually cycle the CiphertextDownloader through a number of steps that
# would normally be sequenced by a Deferred chain in
# CiphertextDownloader.start(), to give us more control over the process.
# In particular, by bypassing _get_all_shareholders, we skip
# permuted-peerlist selection.
for shnum, bucket in shareholders.items():
if shnum < AVAILABLE_SHARES and bucket.closed:
fd.add_share_bucket(shnum, bucket)
fd._got_all_shareholders(None)
# Make it possible to obtain uri_extension from the shareholders.
# Arrange for shareholders[0] to be the first, so we can selectively
# corrupt the data it returns.
uri_extension_sources = shareholders.values()
uri_extension_sources.remove(shareholders[0])
uri_extension_sources.insert(0, shareholders[0])
d = defer.succeed(None)
# have the CiphertextDownloader retrieve a copy of uri_extension itself
d.addCallback(fd._obtain_uri_extension)
if "corrupt_crypttext_hashes" in recover_mode:
# replace everybody's crypttext hash trees with a different one
# (computed over a different file), then modify our uri_extension
# to reflect the new crypttext hash tree root
def _corrupt_crypttext_hashes(unused):
assert isinstance(fd._vup, download.ValidatedExtendedURIProxy), fd._vup
assert fd._vup.crypttext_root_hash, fd._vup
badhash = hashutil.tagged_hash("bogus", "data")
bad_crypttext_hashes = [badhash] * fd._vup.num_segments
badtree = hashtree.HashTree(bad_crypttext_hashes)
for bucket in shareholders.values():
bucket.crypttext_hashes = list(badtree)
fd._crypttext_hash_tree = hashtree.IncompleteHashTree(fd._vup.num_segments)
fd._crypttext_hash_tree.set_hashes({0: badtree[0]})
return fd._vup
d.addCallback(_corrupt_crypttext_hashes)
# also have the CiphertextDownloader ask for hash trees
d.addCallback(fd._get_crypttext_hash_tree)
d.addCallback(fd._download_all_segments)
d.addCallback(fd._done)
def _done(t):
newdata = "".join(consumer.chunks)
return (newdata, fd)
d.addCallback(_done)
return d
def test_not_enough_shares(self):
d = self.send_and_recover((4,8,10), AVAILABLE_SHARES=2)
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(NotEnoughSharesError))
d.addBoth(_done)
return d
def test_one_share_per_peer(self):
return self.send_and_recover()
def test_74(self):
return self.send_and_recover(datalen=74)
def test_75(self):
return self.send_and_recover(datalen=75)
def test_51(self):
return self.send_and_recover(datalen=51)
def test_99(self):
return self.send_and_recover(datalen=99)
def test_100(self):
return self.send_and_recover(datalen=100)
def test_76(self):
return self.send_and_recover(datalen=76)
def test_124(self):
return self.send_and_recover(datalen=124)
def test_125(self):
return self.send_and_recover(datalen=125)
def test_101(self):
return self.send_and_recover(datalen=101)
def test_pause(self):
# use a download target that does pauseProducing/resumeProducing a
# few times, then finishes
c = PausingConsumer()
d = self.send_and_recover(consumer=c)
return d
def test_pause_then_stop(self):
# use a download target that pauses, then stops.
c = PausingAndStoppingConsumer()
d = self.shouldFail(download.DownloadStopped, "test_pause_then_stop",
"our Consumer called stopProducing()",
self.send_and_recover, consumer=c)
return d
def test_stop(self):
# use a download targetthat does an immediate stop (ticket #473)
c = StoppingConsumer()
d = self.shouldFail(download.DownloadStopped, "test_stop",
"our Consumer called stopProducing()",
self.send_and_recover, consumer=c)
return d
# the following tests all use 4-out-of-10 encoding
def test_bad_blocks(self):
# the first 6 servers have bad blocks, which will be caught by the
# blockhashes
modemap = dict([(i, "bad block")
for i in range(6)]
+ [(i, "good")
for i in range(6, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def test_bad_blocks_failure(self):
# the first 7 servers have bad blocks, which will be caught by the
# blockhashes, and the download will fail
modemap = dict([(i, "bad block")
for i in range(7)]
+ [(i, "good")
for i in range(7, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure), res)
self.failUnless(res.check(NotEnoughSharesError), res)
d.addBoth(_done)
return d
def test_bad_blockhashes(self):
# the first 6 servers have bad block hashes, so the blockhash tree
# will not validate
modemap = dict([(i, "bad blockhash")
for i in range(6)]
+ [(i, "good")
for i in range(6, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def test_bad_blockhashes_failure(self):
# the first 7 servers have bad block hashes, so the blockhash tree
# will not validate, and the download will fail
modemap = dict([(i, "bad blockhash")
for i in range(7)]
+ [(i, "good")
for i in range(7, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(NotEnoughSharesError), res)
d.addBoth(_done)
return d
def test_bad_sharehashes(self):
# the first 6 servers have bad block hashes, so the sharehash tree
# will not validate
modemap = dict([(i, "bad sharehash")
for i in range(6)]
+ [(i, "good")
for i in range(6, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def assertFetchFailureIn(self, fd, where):
expected = {"uri_extension": 0,
"crypttext_hash_tree": 0,
}
if where is not None:
expected[where] += 1
self.failUnlessEqual(fd._fetch_failures, expected)
def test_good(self):
# just to make sure the test harness works when we aren't
# intentionally causing failures
modemap = dict([(i, "good") for i in range(0, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
d.addCallback(self.assertFetchFailureIn, None)
return d
def test_bad_uri_extension(self):
# the first server has a bad uri_extension block, so we will fail
# over to a different server.
modemap = dict([(i, "bad uri_extension") for i in range(1)] +
[(i, "good") for i in range(1, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
d.addCallback(self.assertFetchFailureIn, "uri_extension")
return d
def test_bad_crypttext_hashroot(self):
# the first server has a bad crypttext hashroot, so we will fail
# over to a different server.
modemap = dict([(i, "bad crypttext hashroot") for i in range(1)] +
[(i, "good") for i in range(1, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
d.addCallback(self.assertFetchFailureIn, "crypttext_hash_tree")
return d
def test_bad_crypttext_hashes(self):
# the first server has a bad crypttext hash block, so we will fail
# over to a different server.
modemap = dict([(i, "bad crypttext hash") for i in range(1)] +
[(i, "good") for i in range(1, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
d.addCallback(self.assertFetchFailureIn, "crypttext_hash_tree")
return d
def test_bad_crypttext_hashes_failure(self):
# to test that the crypttext merkle tree is really being applied, we
# sneak into the download process and corrupt two things: we replace
# everybody's crypttext hashtree with a bad version (computed over
# bogus data), and we modify the supposedly-validated uri_extension
# block to match the new crypttext hashtree root. The download
# process should notice that the crypttext coming out of FEC doesn't
# match the tree, and fail.
modemap = dict([(i, "good") for i in range(0, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap,
recover_mode=("corrupt_crypttext_hashes"))
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(hashtree.BadHashError), res)
d.addBoth(_done)
return d
def OFF_test_bad_plaintext(self):
# faking a decryption failure is easier: just corrupt the key
modemap = dict([(i, "good") for i in range(0, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap,
recover_mode=("corrupt_key"))
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(hashtree.BadHashError), res)
d.addBoth(_done)
return d
def test_bad_sharehashes_failure(self):
# all ten servers have bad share hashes, so the sharehash tree
# will not validate, and the download will fail
modemap = dict([(i, "bad sharehash")
for i in range(10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(NotEnoughSharesError))
d.addBoth(_done)
return d
def test_missing_sharehashes(self):
# the first 6 servers are missing their sharehashes, so the
# sharehash tree will not validate
modemap = dict([(i, "missing sharehash")
for i in range(6)]
+ [(i, "good")
for i in range(6, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def test_missing_sharehashes_failure(self):
# all servers are missing their sharehashes, so the sharehash tree will not validate,
# and the download will fail
modemap = dict([(i, "missing sharehash")
for i in range(10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure), res)
self.failUnless(res.check(NotEnoughSharesError), res)
d.addBoth(_done)
return d
def test_lost_one_shareholder(self):
# we have enough shareholders when we start, but one segment in we
# lose one of them. The upload should still succeed, as long as we
# still have 'servers_of_happiness' peers left.
modemap = dict([(i, "good") for i in range(9)] +
[(i, "lost") for i in range(9, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def test_lost_one_shareholder_early(self):
# we have enough shareholders when we choose peers, but just before
# we send the 'start' message, we lose one of them. The upload should
# still succeed, as long as we still have 'servers_of_happiness' peers
# left.
modemap = dict([(i, "good") for i in range(9)] +
[(i, "lost-early") for i in range(9, 10)])
return self.send_and_recover((4,8,10), bucket_modes=modemap)
def test_lost_many_shareholders(self):
# we have enough shareholders when we start, but one segment in we
# lose all but one of them. The upload should fail.
modemap = dict([(i, "good") for i in range(1)] +
[(i, "lost") for i in range(1, 10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(UploadUnhappinessError), res)
d.addBoth(_done)
return d
def test_lost_all_shareholders(self):
# we have enough shareholders when we start, but one segment in we
# lose all of them. The upload should fail.
modemap = dict([(i, "lost") for i in range(10)])
d = self.send_and_recover((4,8,10), bucket_modes=modemap)
def _done(res):
self.failUnless(isinstance(res, Failure))
self.failUnless(res.check(UploadUnhappinessError))
d.addBoth(_done)
return d

View File

@ -2,9 +2,10 @@
from twisted.trial import unittest
from allmydata import uri, client
from allmydata.monitor import Monitor
from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
from allmydata.immutable.literal import LiteralFileNode
from allmydata.immutable.filenode import ImmutableFileNode
from allmydata.mutable.filenode import MutableFileNode
from allmydata.util import hashutil, cachedir
from allmydata.util import hashutil
from allmydata.util.consumer import download_to_data
class NotANode:
@ -30,9 +31,8 @@ class Node(unittest.TestCase):
needed_shares=3,
total_shares=10,
size=1000)
cf = cachedir.CacheFile("none")
fn1 = ImmutableFileNode(u, None, None, None, None, cf)
fn2 = ImmutableFileNode(u, None, None, None, None, cf)
fn1 = ImmutableFileNode(u, None, None, None, None)
fn2 = ImmutableFileNode(u, None, None, None, None)
self.failUnlessEqual(fn1, fn2)
self.failIfEqual(fn1, "I am not a filenode")
self.failIfEqual(fn1, NotANode())

View File

@ -23,6 +23,7 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, unittest.TestCase):
# MM's buildslave varies a lot in how long it takes to run tests.
timeout = 240
skip="not ready"
def _break(self, servers):
for (id, ss) in servers:
@ -113,7 +114,8 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, unittest.TestCase):
stage_4_d = None # currently we aren't doing any tests which require this for mutable files
else:
d = download_to_data(n)
stage_4_d = n._downloader._all_downloads.keys()[0]._stage_4_d # too ugly! FIXME
#stage_4_d = n._downloader._all_downloads.keys()[0]._stage_4_d # too ugly! FIXME
stage_4_d = None
return (d, stage_4_d,)
def _wait_for_data(self, n):
@ -141,7 +143,7 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, unittest.TestCase):
self._download_and_check)
else:
return self.shouldFail(NotEnoughSharesError, self.basedir,
"Failed to get enough shareholders",
"ran out of shares",
self._download_and_check)
@ -234,6 +236,7 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, unittest.TestCase):
return d
def test_failover_during_stage_4(self):
raise unittest.SkipTest("needs rewrite")
# See #287
d = defer.succeed(None)
for mutable in [False]:

View File

@ -5,7 +5,7 @@ from twisted.internet import defer
from twisted.trial import unittest
import random
class Test(common.ShareManglingMixin, unittest.TestCase):
class Test(common.ShareManglingMixin, common.ShouldFailMixin, unittest.TestCase):
def test_test_code(self):
# The following process of stashing the shares, running
# replace_shares, and asserting that the new set of shares equals the
@ -18,8 +18,9 @@ class Test(common.ShareManglingMixin, unittest.TestCase):
return res
d.addCallback(_stash_it)
# The following process of deleting 8 of the shares and asserting that you can't
# download it is more to test this test code than to test the Tahoe code...
# The following process of deleting 8 of the shares and asserting
# that you can't download it is more to test this test code than to
# test the Tahoe code...
def _then_delete_8(unused=None):
self.replace_shares(stash[0], storage_index=self.uri.get_storage_index())
for i in range(8):
@ -42,21 +43,24 @@ class Test(common.ShareManglingMixin, unittest.TestCase):
return d
def test_download(self):
""" Basic download. (This functionality is more or less already tested by test code in
other modules, but this module is also going to test some more specific things about
immutable download.)
""" Basic download. (This functionality is more or less already
tested by test code in other modules, but this module is also going
to test some more specific things about immutable download.)
"""
d = defer.succeed(None)
before_download_reads = self._count_reads()
def _after_download(unused=None):
after_download_reads = self._count_reads()
self.failIf(after_download_reads-before_download_reads > 27, (after_download_reads, before_download_reads))
#print before_download_reads, after_download_reads
self.failIf(after_download_reads-before_download_reads > 27,
(after_download_reads, before_download_reads))
d.addCallback(self._download_and_check_plaintext)
d.addCallback(_after_download)
return d
def test_download_from_only_3_remaining_shares(self):
""" Test download after 7 random shares (of the 10) have been removed. """
""" Test download after 7 random shares (of the 10) have been
removed."""
d = defer.succeed(None)
def _then_delete_7(unused=None):
for i in range(7):
@ -65,13 +69,15 @@ class Test(common.ShareManglingMixin, unittest.TestCase):
d.addCallback(_then_delete_7)
def _after_download(unused=None):
after_download_reads = self._count_reads()
#print before_download_reads, after_download_reads
self.failIf(after_download_reads-before_download_reads > 27, (after_download_reads, before_download_reads))
d.addCallback(self._download_and_check_plaintext)
d.addCallback(_after_download)
return d
def test_download_from_only_3_shares_with_good_crypttext_hash(self):
""" Test download after 7 random shares (of the 10) have had their crypttext hash tree corrupted. """
""" Test download after 7 random shares (of the 10) have had their
crypttext hash tree corrupted."""
d = defer.succeed(None)
def _then_corrupt_7(unused=None):
shnums = range(10)
@ -84,39 +90,21 @@ class Test(common.ShareManglingMixin, unittest.TestCase):
return d
def test_download_abort_if_too_many_missing_shares(self):
""" Test that download gives up quickly when it realizes there aren't enough shares out
there."""
d = defer.succeed(None)
def _then_delete_8(unused=None):
for i in range(8):
self._delete_a_share()
d.addCallback(_then_delete_8)
before_download_reads = self._count_reads()
def _attempt_to_download(unused=None):
d2 = download_to_data(self.n)
def _callb(res):
self.fail("Should have gotten an error from attempt to download, not %r" % (res,))
def _errb(f):
self.failUnless(f.check(NotEnoughSharesError))
d2.addCallbacks(_callb, _errb)
return d2
d.addCallback(_attempt_to_download)
def _after_attempt(unused=None):
after_download_reads = self._count_reads()
# To pass this test, you are required to give up before actually trying to read any
# share data.
self.failIf(after_download_reads-before_download_reads > 0, (after_download_reads, before_download_reads))
d.addCallback(_after_attempt)
""" Test that download gives up quickly when it realizes there aren't
enough shares out there."""
for i in range(8):
self._delete_a_share()
d = self.shouldFail(NotEnoughSharesError, "delete 8", None,
download_to_data, self.n)
# the new downloader pipelines a bunch of read requests in parallel,
# so don't bother asserting anything about the number of reads
return d
def test_download_abort_if_too_many_corrupted_shares(self):
""" Test that download gives up quickly when it realizes there aren't enough uncorrupted
shares out there. It should be able to tell because the corruption occurs in the
sharedata version number, which it checks first."""
"""Test that download gives up quickly when it realizes there aren't
enough uncorrupted shares out there. It should be able to tell
because the corruption occurs in the sharedata version number, which
it checks first."""
d = defer.succeed(None)
def _then_corrupt_8(unused=None):
shnums = range(10)
@ -140,17 +128,22 @@ class Test(common.ShareManglingMixin, unittest.TestCase):
def _after_attempt(unused=None):
after_download_reads = self._count_reads()
# To pass this test, you are required to give up before reading all of the share
# data. Actually, we could give up sooner than 45 reads, but currently our download
# code does 45 reads. This test then serves as a "performance regression detector"
# -- if you change download code so that it takes *more* reads, then this test will
# fail.
self.failIf(after_download_reads-before_download_reads > 45, (after_download_reads, before_download_reads))
#print before_download_reads, after_download_reads
# To pass this test, you are required to give up before reading
# all of the share data. Actually, we could give up sooner than
# 45 reads, but currently our download code does 45 reads. This
# test then serves as a "performance regression detector" -- if
# you change download code so that it takes *more* reads, then
# this test will fail.
self.failIf(after_download_reads-before_download_reads > 45,
(after_download_reads, before_download_reads))
d.addCallback(_after_attempt)
return d
# XXX extend these tests to show bad behavior of various kinds from servers: raising exception from each remove_foo() method, for example
# XXX extend these tests to show bad behavior of various kinds from servers:
# raising exception from each remove_foo() method, for example
# XXX test disconnect DeadReferenceError from get_buckets and get_block_whatsit
# TODO: delete this whole file

View File

@ -197,7 +197,7 @@ def make_nodemaker(s=None, num_peers=10):
keygen = client.KeyGenerator()
keygen.set_default_keysize(522)
nodemaker = NodeMaker(storage_broker, sh, None,
None, None, None,
None, None,
{"k": 3, "n": 10}, keygen)
return nodemaker

View File

@ -3,7 +3,7 @@ from allmydata.test import common
from allmydata.monitor import Monitor
from allmydata import check_results
from allmydata.interfaces import NotEnoughSharesError
from allmydata.immutable import repairer, upload
from allmydata.immutable import upload
from allmydata.util.consumer import download_to_data
from twisted.internet import defer
from twisted.trial import unittest
@ -363,99 +363,6 @@ WRITE_LEEWAY = 35
# Optimally, you could repair one of these (small) files in a single write.
DELTA_WRITES_PER_SHARE = 1 * WRITE_LEEWAY
class DownUpConnector(unittest.TestCase):
def test_deferred_satisfaction(self):
duc = repairer.DownUpConnector()
duc.registerProducer(None, True) # just because you have to call registerProducer first
# case 1: total data in buf is < requested data at time of request
duc.write('\x01')
d = duc.read_encrypted(2, False)
def _then(data):
self.failUnlessEqual(len(data), 2)
self.failUnlessEqual(data[0], '\x01')
self.failUnlessEqual(data[1], '\x02')
d.addCallback(_then)
duc.write('\x02')
return d
def test_extra(self):
duc = repairer.DownUpConnector()
duc.registerProducer(None, True) # just because you have to call registerProducer first
# case 1: total data in buf is < requested data at time of request
duc.write('\x01')
d = duc.read_encrypted(2, False)
def _then(data):
self.failUnlessEqual(len(data), 2)
self.failUnlessEqual(data[0], '\x01')
self.failUnlessEqual(data[1], '\x02')
d.addCallback(_then)
duc.write('\x02\0x03')
return d
def test_short_reads_1(self):
# You don't get fewer bytes than you requested -- instead you get no callback at all.
duc = repairer.DownUpConnector()
duc.registerProducer(None, True) # just because you have to call registerProducer first
d = duc.read_encrypted(2, False)
duc.write('\x04')
def _callb(res):
self.fail("Shouldn't have gotten this callback res: %s" % (res,))
d.addCallback(_callb)
# Also in the other order of read-vs-write:
duc2 = repairer.DownUpConnector()
duc2.registerProducer(None, True) # just because you have to call registerProducer first
duc2.write('\x04')
d = duc2.read_encrypted(2, False)
def _callb2(res):
self.fail("Shouldn't have gotten this callback res: %s" % (res,))
d.addCallback(_callb2)
# But once the DUC is closed then you *do* get short reads.
duc3 = repairer.DownUpConnector()
duc3.registerProducer(None, True) # just because you have to call registerProducer first
d = duc3.read_encrypted(2, False)
duc3.write('\x04')
duc3.close()
def _callb3(res):
self.failUnlessEqual(len(res), 1)
self.failUnlessEqual(res[0], '\x04')
d.addCallback(_callb3)
return d
def test_short_reads_2(self):
# Also in the other order of read-vs-write.
duc = repairer.DownUpConnector()
duc.registerProducer(None, True) # just because you have to call registerProducer first
duc.write('\x04')
d = duc.read_encrypted(2, False)
duc.close()
def _callb(res):
self.failUnlessEqual(len(res), 1)
self.failUnlessEqual(res[0], '\x04')
d.addCallback(_callb)
return d
def test_short_reads_3(self):
# Also if it is closed before the read.
duc = repairer.DownUpConnector()
duc.registerProducer(None, True) # just because you have to call registerProducer first
duc.write('\x04')
duc.close()
d = duc.read_encrypted(2, False)
def _callb(res):
self.failUnlessEqual(len(res), 1)
self.failUnlessEqual(res[0], '\x04')
d.addCallback(_callb)
return d
class Repairer(GridTestMixin, unittest.TestCase, RepairTestMixin,
common.ShouldFailMixin):

View File

@ -9,7 +9,8 @@ from allmydata import uri
from allmydata.storage.mutable import MutableShareFile
from allmydata.storage.server import si_a2b
from allmydata.immutable import offloaded, upload
from allmydata.immutable.filenode import ImmutableFileNode, LiteralFileNode
from allmydata.immutable.literal import LiteralFileNode
from allmydata.immutable.filenode import ImmutableFileNode
from allmydata.util import idlib, mathutil
from allmydata.util import log, base32
from allmydata.util.encodingutil import quote_output, unicode_to_argv, get_filesystem_encoding

View File

@ -2086,3 +2086,11 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin,
# upload with exactly 75 peers (shares_of_happiness)
# have a download fail
# cancel a download (need to implement more cancel stuff)
# from test_encode:
# NoNetworkGrid, upload part of ciphertext, kill server, continue upload
# check with Kevan, they want to live in test_upload, existing tests might cover
# def test_lost_one_shareholder(self): # these are upload-side tests
# def test_lost_one_shareholder_early(self):
# def test_lost_many_shareholders(self):
# def test_lost_all_shareholders(self):

View File

@ -12,7 +12,8 @@ from nevow import rend
from allmydata import interfaces, uri, webish, dirnode
from allmydata.storage.shares import get_share_file
from allmydata.storage_client import StorageFarmBroker
from allmydata.immutable import upload, download
from allmydata.immutable import upload
from allmydata.immutable.downloader.status import DownloadStatus
from allmydata.dirnode import DirectoryNode
from allmydata.nodemaker import NodeMaker
from allmydata.unknown import UnknownNode
@ -75,7 +76,7 @@ class FakeUploader(service.Service):
class FakeHistory:
_all_upload_status = [upload.UploadStatus()]
_all_download_status = [download.DownloadStatus()]
_all_download_status = [DownloadStatus("storage_index", 1234)]
_all_mapupdate_statuses = [servermap.UpdateStatus()]
_all_publish_statuses = [publish.PublishStatus()]
_all_retrieve_statuses = [retrieve.RetrieveStatus()]
@ -111,7 +112,7 @@ class FakeClient(Client):
self.uploader = FakeUploader()
self.uploader.setServiceParent(self)
self.nodemaker = FakeNodeMaker(None, self._secret_holder, None,
self.uploader, None, None,
self.uploader, None,
None, None)
def startService(self):
@ -4187,7 +4188,7 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
"no servers were connected, but it might also indicate "
"severe corruption. You should perform a filecheck on "
"this object to learn more. The full error message is: "
"Failed to get enough shareholders: have 0, need 3")
"no shares (need 3). Last failure: None")
self.failUnlessReallyEqual(exp, body)
d.addCallback(_check_zero_shares)
@ -4199,13 +4200,16 @@ class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMi
def _check_one_share(body):
self.failIf("<html>" in body, body)
body = " ".join(body.strip().split())
exp = ("NotEnoughSharesError: This indicates that some "
msg = ("NotEnoughSharesError: This indicates that some "
"servers were unavailable, or that shares have been "
"lost to server departure, hard drive failure, or disk "
"corruption. You should perform a filecheck on "
"this object to learn more. The full error message is:"
" Failed to get enough shareholders: have 1, need 3")
self.failUnlessReallyEqual(exp, body)
" ran out of shares: %d complete, %d pending, 0 overdue,"
" 0 unused, need 3. Last failure: None")
msg1 = msg % (1, 0)
msg2 = msg % (0, 1)
self.failUnless(body == msg1 or body == msg2, body)
d.addCallback(_check_one_share)
d.addCallback(lambda ignored: