From 73ea91dd169d40e9001099d38fcf0de57ffa1765 Mon Sep 17 00:00:00 2001 From: Daira Hopwood Date: Fri, 10 Jul 2015 04:43:07 +0100 Subject: [PATCH] Fixes to test infrastructure. Signed-off-by: Daira Hopwood --- src/allmydata/test/common.py | 35 ++++- src/allmydata/test/common_web.py | 5 +- src/allmydata/test/no_network.py | 213 ++++++++++++++++++++---------- src/allmydata/test/test_upload.py | 4 +- 4 files changed, 179 insertions(+), 78 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index 0d581755..7d6b794a 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -35,11 +35,32 @@ def flush_but_dont_ignore(res): d.addCallback(_done) return d + class DummyProducer: implements(IPullProducer) def resumeProducing(self): pass + +class Marker: + pass + +class FakeCanary: + def __init__(self, ignore_disconnectors=False): + self.ignore = ignore_disconnectors + self.disconnectors = {} + def notifyOnDisconnect(self, f, *args, **kwargs): + if self.ignore: + return + m = Marker() + self.disconnectors[m] = (f, args, kwargs) + return m + def dontNotifyOnDisconnect(self, marker): + if self.ignore: + return + del self.disconnectors[marker] + + class FakeCHKFileNode: """I provide IImmutableFileNode, but all of my data is stored in a class-level dictionary.""" @@ -485,6 +506,9 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): d.addBoth(flush_but_dont_ignore) return d + def workdir(self, name): + return os.path.join("system", self.__class__.__name__, name) + def getdir(self, subdir): return os.path.join(self.basedir, subdir) @@ -603,11 +627,10 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): else: config += nodeconfig - fileutil.write(os.path.join(basedir, 'tahoe.cfg'), config) + # give subclasses a chance to append lines to the nodes' tahoe.cfg files. + config += self._get_extra_config(i) - # give subclasses a chance to append lines to the node's tahoe.cfg - # files before they are launched. - self._set_up_nodes_extra_config() + fileutil.write(os.path.join(basedir, 'tahoe.cfg'), config) # start clients[0], wait for it's tub to be ready (at which point it # will have registered the helper furl). @@ -645,9 +668,9 @@ class SystemTestMixin(pollmixin.PollMixin, testutil.StallMixin): d.addCallback(_connected) return d - def _set_up_nodes_extra_config(self): + def _get_extra_config(self, i): # for overriding by subclasses - pass + return "" def _grab_stats(self, res): d = self.stats_gatherer.poll() diff --git a/src/allmydata/test/common_web.py b/src/allmydata/test/common_web.py index f6e0ac7a..1b064f15 100644 --- a/src/allmydata/test/common_web.py +++ b/src/allmydata/test/common_web.py @@ -51,8 +51,9 @@ class WebRenderingMixin: ctx = self.make_context(req) return page.renderSynchronously(ctx) - def failUnlessIn(self, substring, s): - self.failUnless(substring in s, s) + def render_json(self, page): + d = self.render1(page, args={"t": ["json"]}) + return d def remove_tags(self, s): s = re.sub(r'<[^>]*>', ' ', s) diff --git a/src/allmydata/test/no_network.py b/src/allmydata/test/no_network.py index fd25d8d6..ba9f9cae 100644 --- a/src/allmydata/test/no_network.py +++ b/src/allmydata/test/no_network.py @@ -13,23 +13,28 @@ # Tubs, so it is not useful for tests that involve a Helper, a KeyGenerator, # or the control.furl . -import os +import os, shutil + from zope.interface import implements from twisted.application import service from twisted.internet import defer, reactor from twisted.python.failure import Failure from foolscap.api import Referenceable, fireEventually, RemoteException from base64 import b32encode + from allmydata import uri as tahoe_uri from allmydata.client import Client -from allmydata.storage.server import StorageServer, storage_index_to_dir -from allmydata.util import fileutil, idlib, hashutil +from allmydata.storage.server import StorageServer +from allmydata.storage.backends.disk.disk_backend import DiskBackend +from allmydata.util import fileutil, idlib, hashutil, log from allmydata.util.hashutil import sha1 from allmydata.test.common_web import HTTPClientGETFactory from allmydata.interfaces import IStorageBroker, IServer from allmydata.test.common import TEST_RSA_KEY_SIZE +PRINT_TRACEBACKS = False + class IntentionalError(Exception): pass @@ -87,23 +92,34 @@ class LocalWrapper: return d2 return _really_call() + if PRINT_TRACEBACKS: + import traceback + tb = traceback.extract_stack() d = fireEventually() d.addCallback(lambda res: _call()) def _wrap_exception(f): + if PRINT_TRACEBACKS and not f.check(NameError): + print ">>>" + ">>>".join(traceback.format_list(tb)) + print "+++ %s%r %r: %s" % (methname, args, kwargs, f) + #f.printDetailedTraceback() return Failure(RemoteException(f)) d.addErrback(_wrap_exception) def _return_membrane(res): - # rather than complete the difficult task of building a + # Rather than complete the difficult task of building a # fully-general Membrane (which would locate all Referenceable # objects that cross the simulated wire and replace them with # wrappers), we special-case certain methods that we happen to # know will return Referenceables. + # The outer return value of such a method may be Deferred, but + # its components must not be. if methname == "allocate_buckets": (alreadygot, allocated) = res for shnum in allocated: + assert not isinstance(allocated[shnum], defer.Deferred), (methname, allocated) allocated[shnum] = LocalWrapper(allocated[shnum]) if methname == "get_buckets": for shnum in res: + assert not isinstance(res[shnum], defer.Deferred), (methname, res) res[shnum] = LocalWrapper(res[shnum]) return res d.addCallback(_return_membrane) @@ -168,11 +184,20 @@ class NoNetworkStorageBroker: seed = server.get_permutation_seed() return sha1(peer_selection_index + seed).digest() return sorted(self.get_connected_servers(), key=_permuted) + def get_connected_servers(self): return self.client._servers + def get_nickname_for_serverid(self, serverid): return None + def get_known_servers(self): + return self.get_connected_servers() + + def get_all_serverids(self): + return self.client.get_all_serverids() + + class NoNetworkClient(Client): def create_tub(self): pass @@ -234,8 +259,8 @@ class NoNetworkGrid(service.MultiService): self.clients = [] for i in range(num_servers): - ss = self.make_server(i) - self.add_server(i, ss) + server = self.make_server(i) + self.add_server(i, server) self.rebuild_serverlist() for i in range(num_clients): @@ -266,23 +291,24 @@ class NoNetworkGrid(service.MultiService): def make_server(self, i, readonly=False): serverid = hashutil.tagged_hash("serverid", str(i))[:20] - serverdir = os.path.join(self.basedir, "servers", - idlib.shortnodeid_b2a(serverid), "storage") - 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): + storagedir = os.path.join(self.basedir, "servers", + idlib.shortnodeid_b2a(serverid), "storage") + + # The backend will make the storage directory and any necessary parents. + backend = DiskBackend(storagedir, readonly=readonly) + server = StorageServer(serverid, backend, storagedir, stats_provider=SimpleStats()) + server._no_network_server_number = i + return server + + def add_server(self, i, server): # to deal with the fact that all StorageServers are named 'storage', # we interpose a middleman middleman = service.MultiService() middleman.setServiceParent(self) - ss.setServiceParent(middleman) - serverid = ss.my_nodeid - self.servers_by_number[i] = ss - aa = ss.get_accountant().get_anonymous_account() + server.setServiceParent(middleman) + serverid = server.get_serverid() + self.servers_by_number[i] = server + aa = server.get_accountant().get_anonymous_account() wrapper = wrap_storage_server(aa) self.wrappers_by_id[serverid] = wrapper self.proxies_by_id[serverid] = NoNetworkServer(serverid, wrapper) @@ -299,14 +325,14 @@ class NoNetworkGrid(service.MultiService): def remove_server(self, serverid): # it's enough to remove the server from c._servers (we don't actually # have to detach and stopService it) - for i,ss in self.servers_by_number.items(): - if ss.my_nodeid == serverid: + for i, server in self.servers_by_number.items(): + if server.get_serverid() == serverid: del self.servers_by_number[i] break del self.wrappers_by_id[serverid] del self.proxies_by_id[serverid] self.rebuild_serverlist() - return ss + return server def break_server(self, serverid, count=True): # mark the given server as broken, so it will throw exceptions when @@ -316,16 +342,16 @@ class NoNetworkGrid(service.MultiService): def hang_server(self, serverid): # hang the given server - ss = self.wrappers_by_id[serverid] - assert ss.hung_until is None - ss.hung_until = defer.Deferred() + server = self.wrappers_by_id[serverid] + assert server.hung_until is None + server.hung_until = defer.Deferred() def unhang_server(self, serverid): # unhang the given server - ss = self.wrappers_by_id[serverid] - assert ss.hung_until is not None - ss.hung_until.callback(None) - ss.hung_until = None + server = self.wrappers_by_id[serverid] + assert server.hung_until is not None + server.hung_until.callback(None) + server.hung_until = None def nuke_from_orbit(self): """Empty all share directories in this grid. It's the only way to be sure ;-) @@ -361,72 +387,123 @@ class GridTestMixin: def get_clientdir(self, i=0): return self.g.clients[i].basedir + def get_server(self, i): + return self.g.servers_by_number[i] + def get_serverdir(self, i): - return self.g.servers_by_number[i].storedir + return self.g.servers_by_number[i].backend._storedir + + def remove_server(self, i): + self.g.remove_server(self.g.servers_by_number[i].get_serverid()) def iterate_servers(self): for i in sorted(self.g.servers_by_number.keys()): - ss = self.g.servers_by_number[i] - yield (i, ss, ss.storedir) + server = self.g.servers_by_number[i] + yield (i, server, server.backend._storedir) def find_uri_shares(self, uri): si = tahoe_uri.from_string(uri).get_storage_index() - prefixdir = storage_index_to_dir(si) - shares = [] - for i,ss in self.g.servers_by_number.items(): - serverid = ss.my_nodeid - basedir = os.path.join(ss.sharedir, prefixdir) - if not os.path.exists(basedir): - continue - for f in os.listdir(basedir): - try: - shnum = int(f) - shares.append((shnum, serverid, os.path.join(basedir, f))) - except ValueError: - pass - return sorted(shares) + sharelist = [] + d = defer.succeed(None) + for i, server in self.g.servers_by_number.items(): + d.addCallback(lambda ign, server=server: server.backend.get_shareset(si).get_shares()) + def _append_shares( (shares_for_server, corrupted), server=server): + assert len(corrupted) == 0, (shares_for_server, corrupted) + for share in shares_for_server: + assert not isinstance(share, defer.Deferred), share + sharelist.append( (share.get_shnum(), server.get_serverid(), share._get_path()) ) + d.addCallback(_append_shares) + + d.addCallback(lambda ign: sorted(sharelist)) + return d + + def add_server(self, server_number, readonly=False): + assert self.g, "I tried to find a grid at self.g, but failed" + ss = self.g.make_server(server_number, readonly) + log.msg("just created a server, number: %s => %s" % (server_number, ss,)) + self.g.add_server(server_number, ss) + + def add_server_with_share(self, uri, server_number, share_number=None, + readonly=False): + self.add_server(server_number, readonly) + if share_number is not None: + self.copy_share_to_server(uri, server_number, share_number) + + def copy_share_to_server(self, uri, server_number, share_number): + ss = self.g.servers_by_number[server_number] + self.copy_share(self.shares[share_number], uri, ss) def copy_shares(self, uri): shares = {} - for (shnum, serverid, sharefile) in self.find_uri_shares(uri): - shares[sharefile] = open(sharefile, "rb").read() - return shares + d = self.find_uri_shares(uri) + def _got_shares(sharelist): + for (shnum, serverid, sharefile) in sharelist: + shares[sharefile] = fileutil.read(sharefile) + + return shares + d.addCallback(_got_shares) + return d + + def copy_share(self, from_share, uri, to_server): + si = tahoe_uri.from_string(uri).get_storage_index() + (i_shnum, i_serverid, i_sharefile) = from_share + shares_dir = to_server.backend.get_shareset(si)._get_sharedir() + new_sharefile = os.path.join(shares_dir, str(i_shnum)) + fileutil.make_dirs(shares_dir) + if os.path.normpath(i_sharefile) != os.path.normpath(new_sharefile): + shutil.copy(i_sharefile, new_sharefile) def restore_all_shares(self, shares): for sharefile, data in shares.items(): - open(sharefile, "wb").write(data) + fileutil.write(sharefile, data) def delete_share(self, (shnum, serverid, sharefile)): - os.unlink(sharefile) + fileutil.remove(sharefile) def delete_shares_numbered(self, uri, shnums): - for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri): - if i_shnum in shnums: - os.unlink(i_sharefile) + d = self.find_uri_shares(uri) + def _got_shares(sharelist): + for (i_shnum, i_serverid, i_sharefile) in sharelist: + if i_shnum in shnums: + fileutil.remove(i_sharefile) + d.addCallback(_got_shares) + return d + + def delete_all_shares(self, uri): + d = self.find_uri_shares(uri) + def _got_shares(shares): + for sh in shares: + self.delete_share(sh) + d.addCallback(_got_shares) + return d - def delete_all_shares(self, serverdir): + def delete_all_shares_in_serverdir(self, serverdir): sharedir = os.path.join(serverdir, "shares") for prefixdir in os.listdir(sharedir): if prefixdir != 'incoming': fileutil.rm_dir(os.path.join(sharedir, prefixdir)) - def corrupt_share(self, (shnum, serverid, sharefile), corruptor_function): - sharedata = open(sharefile, "rb").read() - corruptdata = corruptor_function(sharedata) - open(sharefile, "wb").write(corruptdata) + def corrupt_share(self, (shnum, serverid, sharefile), corruptor_function, debug=False): + sharedata = fileutil.read(sharefile) + corruptdata = corruptor_function(sharedata, debug=debug) + fileutil.write(sharefile, corruptdata) def corrupt_shares_numbered(self, uri, shnums, corruptor, debug=False): - for (i_shnum, i_serverid, i_sharefile) in self.find_uri_shares(uri): - if i_shnum in shnums: - sharedata = open(i_sharefile, "rb").read() - corruptdata = corruptor(sharedata, debug=debug) - open(i_sharefile, "wb").write(corruptdata) + d = self.find_uri_shares(uri) + def _got_shares(sharelist): + for (i_shnum, i_serverid, i_sharefile) in sharelist: + if i_shnum in shnums: + self.corrupt_share((i_shnum, i_serverid, i_sharefile), corruptor, debug=debug) + d.addCallback(_got_shares) + return d 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) + d = self.find_uri_shares(uri) + def _got_shares(sharelist): + for (i_shnum, i_serverid, i_sharefile) in sharelist: + self.corrupt_share((i_shnum, i_serverid, i_sharefile), corruptor, debug=debug) + d.addCallback(_got_shares) + return d def GET(self, urlpath, followRedirect=False, return_response=False, method="GET", clientnum=0, **kwargs): diff --git a/src/allmydata/test/test_upload.py b/src/allmydata/test/test_upload.py index 090fa3bb..f8c1a3c5 100644 --- a/src/allmydata/test/test_upload.py +++ b/src/allmydata/test/test_upload.py @@ -1887,7 +1887,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin, self._add_server_with_share(server_number=3, share_number=1) # Copy shares self._copy_share_to_server(3, 1) - self.delete_all_shares(self.get_serverdir(0)) + self.delete_all_shares_in_serverdir(self.get_serverdir(0)) client = self.g.clients[0] client.encoding_params['happy'] = 4 return client @@ -1925,7 +1925,7 @@ class EncodingParameters(GridTestMixin, unittest.TestCase, SetDEPMixin, # Copy shares self._copy_share_to_server(3, 1) #Remove shares from server 0 - self.delete_all_shares(self.get_serverdir(0)) + self.delete_all_shares_in_serverdir(self.get_serverdir(0)) client = self.g.clients[0] client.encoding_params['happy'] = 4 return client -- 2.45.2