diff --git a/.circleci/config.yml b/.circleci/config.yml index 9cd2b20cbaae07e50e7cea351c05ed72183d46b4..ae156c793dd77f514f21c158fbb99b409bb8ff92 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -53,7 +53,6 @@ jobs: - image: "nixorg/nix:circleci" environment: - CODECOV_TOKEN: "cc6e4697-4337-4506-88af-92b8f8ca6b22" # Specify a revision of NixOS/nixpkgs to run against. This essentially # pins the majority of the software involved in the build. This # revision is selected arbitrarily. It's somewhat current as of the @@ -144,7 +143,7 @@ jobs: - run: name: "Report Coverage" command: | - nix-shell -p 'python.withPackages (ps: [ ps.codecov ])' --keep CODECOV_TOKEN --run \ + nix-shell -p 'python.withPackages (ps: [ ps.codecov ])' --run \ 'codecov --file ./result-doc/share/doc/*/.coverage' workflows: diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000000000000000000000000000000000..ec19cb88f397c327bac1afa6a195073f305e3e3d --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +source = + _zkapauthorizer + twisted.plugins.zkapauthorizer + +branch = True diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0b6af690f7c47e638d89a962ad98f8fa01c479d1 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,77 @@ +name: "ci" + +on: + - "push" + +jobs: + unit-tests: + runs-on: "windows-latest" + strategy: + matrix: + python-version: + - "2.7" + + steps: + # Avoid letting Windows newlines confusing milksnake. + - run: "git config --global core.autocrlf false" + + - uses: actions/checkout@v2 + with: + fetch-depth: "0" + + # Get tags not fetched by the checkout action, needed for auto-versioning. + - run: "git fetch origin +refs/tags/*:refs/tags/*" + + # Get MS VC++ 9 aka Visual Studio 2008, required to build Python 2.7 + # extensions (zfec via Tahoe-LAFS). + - uses: "crazy-max/ghaction-chocolatey@v1" + with: + args: "install vcpython27" + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + + - name: "Upgrade Pip" + run: | + python -m pip install --upgrade pip + + - name: "Install CI Dependencies" + run: | + python -m pip install wheel coverage + + - name: "Install PrivacyPass master@HEAD" + run: | + python -m pip install git+https://github.com/LeastAuthority/privacypass@master#egg=privacypass + + - name: "Install Tahoe-LAFS master@HEAD" + run: | + python -m pip install git+https://github.com/tahoe-lafs/tahoe-lafs@master#egg=tahoe-lafs + + - name: "Install Test Dependencies" + run: | + python -m pip install -r test-requirements.txt + + - name: "Install ZKAPAuthorizer" + run: | + python -m pip install ./ + + - name: "Dump Python Environment" + run: | + pip freeze + + - name: "Run Tests" + env: + MAGIC_FOLDER_HYPOTHESIS_PROFILE: "ci" + run: | + python -m coverage run -m twisted.trial _zkapauthorizer + + + - name: "Convert Coverage" + run: | + coverage xml + + - uses: codecov/codecov-action@v1 + with: + file: "./coverage.xml" diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000000000000000000000000000000000..40b059fae03e3f7df4c945b9c5ceb9e5983b1d08 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +codecov: + token: "cc6e4697-4337-4506-88af-92b8f8ca6b22" diff --git a/setup.cfg b/setup.cfg index c35d4a8495fbc3b6fc4086c05fa1b148c8867463..659b2061f2a7287abe7c405f454363626f5930e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,8 @@ install_requires = zope.interface aniso8601 privacypass - twisted + # Inherit our Twisted dependency from tahoe-lafs so we don't accidentally + # get the extras wrong here and break stuff. tahoe-lafs treq diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index c06dea97b62eff7bd897f2cc6fb01e6ed129e40d..8c832827a38d0f44a023140b5b256eb6d852ce33 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -26,6 +26,7 @@ from __future__ import ( from struct import ( unpack, + calcsize, ) from errno import ( @@ -507,10 +508,23 @@ def get_storage_index_share_size(sharepath): :return int: The data size of the share in bytes. """ - with open(sharepath) as share_file: - share_data_length_bytes = share_file.read(8)[4:] - (share_data_length,) = unpack('>L', share_data_length_bytes) - return share_data_length + # Note Tahoe-LAFS immutable/layout.py makes some claims about how the + # share data is structured. A lot of this seems to be wrong. + # storage/immutable.py appears to have the correct information. + fmt = ">LL" + with open(sharepath, "rb") as share_file: + header = share_file.read(calcsize(fmt)) + + if len(header) != calcsize(fmt): + raise ValueError( + "Tried to read {} bytes of share file header, got {!r} instead.".format( + calcsize(fmt), + header, + ), + ) + + version, share_data_length = unpack(fmt, header) + return share_data_length def get_lease_expiration(get_leases, storage_index_or_slot): @@ -556,7 +570,7 @@ def get_slot_share_size(sharepath): :return int: The data size of the share in bytes. """ - with open(sharepath) as share_file: + with open(sharepath, "rb") as share_file: share_data_length_bytes = share_file.read(92)[-8:] (share_data_length,) = unpack('>Q', share_data_length_bytes) return share_data_length @@ -584,7 +598,7 @@ def get_stat(sharepath): This is necessary to differentiate between buckets and slots. """ # Figure out if it is a storage index or a slot. - with open(sharepath) as share_file: + with open(sharepath, "rb") as share_file: magic = share_file.read(32) if magic == "Tahoe mutable container v1\n" + "\x75\x09\x44\x03\x8e": return stat_slot diff --git a/src/_zkapauthorizer/tests/__init__.py b/src/_zkapauthorizer/tests/__init__.py index 1fd9a9da7f721537646cd78176a3c063909b2101..0f9529a87a1b9837c7b34798450815918ee5a9f5 100644 --- a/src/_zkapauthorizer/tests/__init__.py +++ b/src/_zkapauthorizer/tests/__init__.py @@ -28,24 +28,36 @@ def _configure_hypothesis(): settings, ) - settings.register_profile( - "ci", + base = dict( suppress_health_check=[ - # CPU resources available to CI builds typically varies - # significantly from run to run making it difficult to determine - # if "too slow" data generation is a result of the code or the - # execution environment. Prevent these checks from - # (intermittently) failing tests that are otherwise fine. + # CPU resources available to builds typically varies significantly + # from run to run making it difficult to determine if "too slow" + # data generation is a result of the code or the execution + # environment. Prevent these checks from (intermittently) failing + # tests that are otherwise fine. HealthCheck.too_slow, ], # With the same reasoning, disable the test deadline. deadline=None, ) + settings.register_profile( + "default", + **base + ) + + settings.register_profile( + "ci", + # Make CI runs a little more aggressive in amount of coverage they try + # to provide. + max_examples=200, + **base + ) + settings.register_profile( "big", max_examples=10000, - deadline=None, + **base ) profile_name = environ.get("ZKAPAUTHORIZER_HYPOTHESIS_PROFILE", "default") diff --git a/src/_zkapauthorizer/tests/common.py b/src/_zkapauthorizer/tests/common.py new file mode 100644 index 0000000000000000000000000000000000000000..39aa9119e10c22670c2948b4f53a7dbb22dc4f6e --- /dev/null +++ b/src/_zkapauthorizer/tests/common.py @@ -0,0 +1,41 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Testing functionality that is specifically related to the test harness +itself. +""" + + +def skipIf(condition, reason): + """ + Create a decorate a function to be skipped if the given condition is true. + + :param bool condition: The condition under which to skip. + :param unicode reason: A reason for the skip. + + :return: A function decorator which will skip the test if the given + condition is true. + """ + if condition: + return _skipper(reason) + return lambda x: x + + +def _skipper(reason): + def wrapper(f): + def skipIt(self, *a, **kw): + self.skipTest(reason) + return skipIt + return wrapper diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index a4f4eadd80644a13a045015aa8fbf21d0f3cc043..0da7adf84af23d599a7ef8f4556d9d5a28bd2c10 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -488,10 +488,10 @@ def sizes(): return integers( # Size 0 data isn't data, it's nothing. min_value=1, - # For the moment there are some assumptions in the test suite that - # limit us to an amount of storage that can be paid for with one ZKAP. - # That will be fixed eventually. For now, keep the sizes pretty low. - max_value=2 ** 16, + # Let this be larger than a single segment (2 ** 17) in case that + # matters to Tahoe-LAFS storage at all. I don't think it does, + # though. + max_value=2 ** 18, ) diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 9e7b779f7a842022437085b7082b5732873b2b3f..348c0cf0f2258389d9516eb0fa1da1b5e9bb86be 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -31,6 +31,10 @@ from datetime import ( timedelta, ) +from unittest import ( + skipIf, +) + from testtools import ( TestCase, ) @@ -61,6 +65,9 @@ from hypothesis.strategies import ( integers, ) +from twisted.python.runtime import ( + platform, +) from twisted.python.filepath import ( FilePath, ) @@ -182,6 +189,7 @@ class VoucherStoreTests(TestCase): )), ) + @skipIf(platform.isWindows(), "Hard to prevent directory creation on Windows") @given(tahoe_configs(), datetimes()) def test_uncreateable_store_directory(self, get_config, now): """ @@ -222,6 +230,7 @@ class VoucherStoreTests(TestCase): ) + @skipIf(platform.isWindows(), "Hard to prevent database from being opened on Windows") @given(tahoe_configs(), datetimes()) def test_unopenable_store(self, get_config, now): """ diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 7d9de02808dde0a547ded59ad4cd3592ff5fdcb1..569f852561669862a88ca277254822d3b93df3b3 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -54,6 +54,9 @@ from hypothesis.strategies import ( integers, ) +from twisted.python.runtime import ( + platform, +) from twisted.python.filepath import ( FilePath, ) @@ -67,6 +70,10 @@ from privacypass import ( random_signing_key, ) +from .common import ( + skipIf, +) + from .privacypass import ( make_passes, ) @@ -151,7 +158,6 @@ class RequiredPassesTests(TestCase): ) - class ShareTests(TestCase): """ Tests for interaction with shares. @@ -395,6 +401,7 @@ class ShareTests(TestCase): ) + @skipIf(platform.isWindows(), "Storage server miscomputes slot size on Windows") @given( storage_index=storage_indexes(), secrets=tuples( @@ -462,6 +469,10 @@ class ShareTests(TestCase): ) + @skipIf( + platform.isWindows(), + "StorageServer fails to create necessary directory for corruption advisories in Windows.", + ) @given( storage_index=storage_indexes(), renew_secret=lease_renew_secrets(), diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 4cef248a8ae6b09f38d7968c220903fa45480b42..c00b4a9e04399f82b89069e5986e90c0ba6baf6e 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -27,6 +27,7 @@ from time import ( from random import ( shuffle, ) + from testtools import ( TestCase, ) @@ -52,6 +53,9 @@ from privacypass import ( random_signing_key, ) +from twisted.python.runtime import ( + platform, +) from twisted.internet.task import ( Clock, ) @@ -60,6 +64,9 @@ from foolscap.referenceable import ( LocalReferenceable, ) +from .common import ( + skipIf, +) from .privacypass import ( make_passes, ) @@ -100,6 +107,7 @@ class PassValidationTests(TestCase): """ Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``. """ + @skipIf(platform.isWindows(), "Storage server is not supported on Windows") def setUp(self): super(PassValidationTests, self).setUp() self.clock = Clock() diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..68868665b6a9b214a73a65c3376781d1eda91cc6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +fixtures +testtools +hypothesis