diff --git a/src/_secureaccesstokenauthorizer/_storage_client.py b/src/_secureaccesstokenauthorizer/_storage_client.py index 9a77fbcf87a69c34fde371d6c4cea4fa7e7c3c8d..ff86d26d39ace4797923a9ff16b217e3ae4145ed 100644 --- a/src/_secureaccesstokenauthorizer/_storage_client.py +++ b/src/_secureaccesstokenauthorizer/_storage_client.py @@ -117,3 +117,31 @@ class SecureAccessTokenAuthorizerStorageClient(object): shnum, reason, ) + + def slot_testv_and_readv_and_writev( + self, + storage_index, + secrets, + tw_vectors, + r_vector, + ): + return self._rref.callRemote( + "slot_testv_and_readv_and_writev", + storage_index, + secrets, + tw_vectors, + r_vector, + ) + + def slot_readv( + self, + storage_index, + shares, + r_vector, + ): + return self._rref.callRemote( + "slot_readv", + storage_index, + shares, + r_vector, + ) diff --git a/src/_secureaccesstokenauthorizer/_storage_server.py b/src/_secureaccesstokenauthorizer/_storage_server.py index 6264b310a93065b93c112546721038d8797a87bb..78a4800b350ee9370bc11551a6c5808a5de2f9fe 100644 --- a/src/_secureaccesstokenauthorizer/_storage_server.py +++ b/src/_secureaccesstokenauthorizer/_storage_server.py @@ -126,6 +126,12 @@ class SecureAccessTokenAuthorizerStorageServer(Referenceable): def remote_advise_corrupt_share(self, *a, **kw): return self._original.remote_advise_corrupt_share(*a, **kw) + def remote_slot_testv_and_readv_and_writev(self, *a, **kw): + return self._original.remote_slot_testv_and_readv_and_writev(*a, **kw) + + def remote_slot_readv(self, *a, **kw): + return self._original.remote_slot_readv(*a, **kw) + # I don't understand why this is required. # SecureAccessTokenAuthorizerStorageServer is-a Referenceable. It seems like # the built in adapter should take care of this case. diff --git a/src/_secureaccesstokenauthorizer/tests/strategies.py b/src/_secureaccesstokenauthorizer/tests/strategies.py index 945ad614e0a1ed8f3c4022008e641e3e983f74e3..e3f5e5f15f9df710d00838156998114f1dbcc1d2 100644 --- a/src/_secureaccesstokenauthorizer/tests/strategies.py +++ b/src/_secureaccesstokenauthorizer/tests/strategies.py @@ -17,16 +17,21 @@ Hypothesis strategies for property testing. """ from hypothesis.strategies import ( + one_of, just, binary, integers, sets, + lists, + tuples, + dictionaries, ) from allmydata.interfaces import ( StorageIndex, LeaseRenewSecret, LeaseCancelSecret, + WriteEnablerSecret, ) def configurations(): @@ -66,6 +71,16 @@ def lease_cancel_secrets(): ) +def write_enabler_secrets(): + """ + Build Tahoe-LAFS write enabler secrets. + """ + return binary( + min_size=WriteEnablerSecret.minLength, + max_size=WriteEnablerSecret.maxLength, + ) + + def sharenums(): """ Build Tahoe-LAFS share numbers. @@ -91,8 +106,103 @@ def sizes(): """ Build Tahoe-LAFS share sizes. """ + return integers( + # Size 0 data isn't data, it's nothing. + min_value=1, + # Just for practical purposes... + max_value=2 ** 16, + ) + + +def offsets(): + """ + Build Tahoe-LAFS share offsets. + """ return integers( min_value=0, # Just for practical purposes... max_value=2 ** 16, ) + + +def bytes_for_share(sharenum, size): + """ + :return bytes: marginally distinctive bytes of a certain length for the + given share number + """ + if 0 <= sharenum <= 255: + return (unichr(sharenum) * size).encode("latin-1") + raise ValueError("Sharenum must be between 0 and 255 inclusive.") + + +def shares(): + """ + Build Tahoe-LAFS share data. + """ + return tuples( + sharenums(), + sizes() + ).map( + lambda (num, size): bytes_for_share(num, size), + ) + + +def data_vectors(): + """ + Build Tahoe-LAFS data vectors. + """ + return lists( + tuples( + offsets(), + shares(), + ), + # An empty data vector doesn't make much sense. If you have no data + # to write, you should probably use slot_readv instead. Also, + # Tahoe-LAFS explodes if you pass an empty data vector - + # storage/server.py, OSError(ENOENT) from `os.listdir(bucketdir)`. + min_size=1, + # Just for practical purposes... + max_size=8, + ) + + +def test_vectors(): + """ + Build Tahoe-LAFS test vectors. + """ + return lists( + # XXX TODO + just(None), + min_size=0, + max_size=0, + ) + + +def test_and_write_vectors(): + """ + Build Tahoe-LAFS test and write vectors for a single share. + """ + return tuples( + test_vectors(), + data_vectors(), + one_of( + just(None), + sizes(), + ), + ) + + +def test_and_write_vectors_for_shares(): + """ + Build Tahoe-LAFS test and write vectors for a number of shares. + """ + return dictionaries( + sharenums(), + test_and_write_vectors(), + # An empty dictionary wouldn't make much sense. And it provokes a + # NameError from Tahoe, storage/server.py:479, `new_length` referenced + # before assignment. + min_size=1, + # Just for practical purposes... + max_size=8, + ) diff --git a/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py index cc980e6854c70b274d2527909a30c5dc10fccc1e..8e17be297c48c6898e2808f5719731920cc7235e 100644 --- a/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py +++ b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py @@ -15,6 +15,7 @@ """ Tests for communication between the client and server components. """ + import attr from fixtures import ( @@ -38,6 +39,7 @@ from testtools.twistedsupport._deferred import ( from hypothesis import ( given, assume, + note, ) from hypothesis.strategies import ( tuples, @@ -62,9 +64,13 @@ from .strategies import ( storage_indexes, lease_renew_secrets, lease_cancel_secrets, + write_enabler_secrets, sharenums, sharenum_sets, sizes, + test_and_write_vectors_for_shares, + # Not really a strategy... + bytes_for_share, ) from ..api import ( @@ -72,16 +78,6 @@ from ..api import ( SecureAccessTokenAuthorizerStorageClient, ) -def bytes_for_share(sharenum, size): - """ - Generate marginally distinctive bytes of a certain length for the given - share number. - """ - if 0 <= sharenum <= 255: - return (unichr(sharenum) * size).encode("latin-1") - raise ValueError("Sharenum must be between 0 and 255 inclusive.") - - class AnonymousStorageServer(Fixture): def _setUp(self): self.tempdir = self.useFixture(TempDir()).join(b"storage") @@ -310,6 +306,88 @@ class ShareTests(TestCase): HasLength(1), ) + @given( + storage_index=storage_indexes(), + secrets=tuples( + write_enabler_secrets(), + lease_renew_secrets(), + lease_cancel_secrets(), + ), + test_and_write_vectors_for_shares=test_and_write_vectors_for_shares(), + ) + def test_create_mutable(self, storage_index, secrets, test_and_write_vectors_for_shares): + """ + Mutable share data written using *slot_testv_and_readv_and_writev* can be + read back. + """ + # Hypothesis causes our storage server to be used many times. Clean + # up between iterations. + cleanup_storage_server(self.anonymous_storage_server) + + wrote, read = extract_result( + self.client.slot_testv_and_readv_and_writev( + storage_index, + secrets=secrets, + tw_vectors=test_and_write_vectors_for_shares, + r_vector=[], + ), + ) + + self.assertThat( + wrote, + Equals(True), + u"Server rejected a write to a new mutable storage index", + ) + + self.assertThat( + read, + Equals({}), + u"Server gave back read results when we asked for none.", + ) + + for sharenum, (test_vector, write_vector, new_length) in test_and_write_vectors_for_shares.items(): + r_vector = list(map(write_vector_to_read_vector, write_vector)) + read = extract_result( + self.client.slot_readv( + storage_index, + shares=[sharenum], + r_vector=r_vector, + ), + ) + note("read vector {}".format(r_vector)) + # Create a buffer and pile up all the write operations in it. + # This lets us make correct assertions about overlapping writes. + length = max( + offset + len(data) + for (offset, data) + in write_vector + ) + expected = b"\x00" * length + for (offset, data) in write_vector: + expected = expected[:offset] + data + expected[offset + len(data):] + if new_length is not None and new_length < length: + expected = expected[:new_length] + self.assertThat( + read, + Equals({sharenum: list( + # Get the expected value out of our scratch buffer. + expected[offset:offset + len(data)] + for (offset, data) + in write_vector + )}), + u"Server didn't reliably read back data just written for share {}".format( + sharenum, + ), + ) + + +def write_vector_to_read_vector(write_vector): + """ + Create a read vector which will read back the data written by the given + write vector. + """ + return (write_vector[0], len(write_vector[1])) + def write_toy_shares( storage_server,