mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2024-12-21 05:53:12 +00:00
Merge remote-tracking branch 'origin/master' into 3374.codec-monitor-python-3-take-2
This commit is contained in:
commit
6f2f460bf3
@ -1,95 +0,0 @@
|
||||
# adapted from https://packaging.python.org/en/latest/appveyor/
|
||||
|
||||
environment:
|
||||
|
||||
matrix:
|
||||
|
||||
# For Python versions available on Appveyor, see
|
||||
# http://www.appveyor.com/docs/installed-software#python
|
||||
- PYTHON: "C:\\Python27"
|
||||
- PYTHON: "C:\\Python27-x64"
|
||||
# DISTUTILS_USE_SDK: "1"
|
||||
# TOX_TESTENV_PASSENV: "DISTUTILS_USE_SDK INCLUDE LIB"
|
||||
|
||||
install:
|
||||
- |
|
||||
%PYTHON%\python.exe -m pip install -U pip
|
||||
%PYTHON%\python.exe -m pip install wheel tox==3.9.0 virtualenv
|
||||
|
||||
# note:
|
||||
# %PYTHON% has: python.exe
|
||||
# %PYTHON%\Scripts has: pip.exe, tox.exe (and others installed by bare pip)
|
||||
|
||||
# We have a custom "build" system. We don't need MSBuild or whatever.
|
||||
build: off
|
||||
|
||||
# Do not build feature branch with open pull requests. This is documented but
|
||||
# it's not clear it does anything.
|
||||
skip_branch_with_pr: true
|
||||
|
||||
# This, perhaps, is effective.
|
||||
branches:
|
||||
# whitelist
|
||||
only:
|
||||
- 'master'
|
||||
|
||||
skip_commits:
|
||||
files:
|
||||
# The Windows builds are unaffected by news fragments.
|
||||
- 'newsfragments/*'
|
||||
# Also, all this build junk.
|
||||
- '.circleci/*'
|
||||
- '.lgtm.yml'
|
||||
- '.travis.yml'
|
||||
|
||||
# we run from C:\projects\tahoe-lafs
|
||||
|
||||
test_script:
|
||||
# Put your test command here.
|
||||
# Note that you must use the environment variable %PYTHON% to refer to
|
||||
# the interpreter you're using - Appveyor does not do anything special
|
||||
# to put the Python version you want to use on PATH.
|
||||
- |
|
||||
%PYTHON%\Scripts\tox.exe -e coverage
|
||||
%PYTHON%\Scripts\tox.exe -e pyinstaller
|
||||
# To verify that the resultant PyInstaller-generated binary executes
|
||||
# cleanly (i.e., that it terminates with an exit code of 0 and isn't
|
||||
# failing due to import/packaging-related errors, etc.).
|
||||
- dist\Tahoe-LAFS\tahoe.exe --version
|
||||
|
||||
after_test:
|
||||
# This builds the main tahoe wheel, and wheels for all dependencies.
|
||||
# Again, you only need build.cmd if you're building C extensions for
|
||||
# 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct
|
||||
# interpreter. If _trial_temp still exists, the "pip wheel" fails on
|
||||
# _trial_temp\local_dir (not sure why).
|
||||
- |
|
||||
copy _trial_temp\test.log trial_test_log.txt
|
||||
rd /s /q _trial_temp
|
||||
%PYTHON%\python.exe setup.py bdist_wheel
|
||||
%PYTHON%\python.exe -m pip wheel -w dist .
|
||||
- |
|
||||
%PYTHON%\python.exe -m pip install codecov "coverage ~= 4.5"
|
||||
%PYTHON%\python.exe -m coverage xml -o coverage.xml -i
|
||||
%PYTHON%\python.exe -m codecov -X search -X gcov -f coverage.xml
|
||||
|
||||
artifacts:
|
||||
# bdist_wheel puts your built wheel in the dist directory
|
||||
# "pip wheel -w dist ." puts all the dependency wheels there too
|
||||
# this gives us a zipfile with everything
|
||||
- path: 'dist\*'
|
||||
- path: trial_test_log.txt
|
||||
name: Trial test.log
|
||||
- path: eliot.log
|
||||
name: Eliot test log
|
||||
|
||||
on_failure:
|
||||
# Artifacts are not normally uploaded when the job fails. To get the test
|
||||
# logs, we have to push them ourselves.
|
||||
- ps: Push-AppveyorArtifact _trial_temp\test.log -Filename trial.log
|
||||
- ps: Push-AppveyorArtifact eliot.log -Filename eliot.log
|
||||
|
||||
#on_success:
|
||||
# You can use this step to upload your artifacts to a public website.
|
||||
# See Appveyor's documentation for more details. Or you can simply
|
||||
# access your wheels from the Appveyor "artifacts" tab for your build.
|
@ -285,7 +285,7 @@ jobs:
|
||||
# this reporter on Python 3. So drop that and just specify the
|
||||
# reporter.
|
||||
TAHOE_LAFS_TRIAL_ARGS: "--reporter=subunitv2-file"
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py36"
|
||||
TAHOE_LAFS_TOX_ENVIRONMENT: "py36-coverage"
|
||||
|
||||
|
||||
ubuntu-20.04:
|
||||
|
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -49,8 +49,8 @@ jobs:
|
||||
- name: Display tool versions
|
||||
run: python misc/build_helpers/show-tool-versions.py
|
||||
|
||||
- name: Run "tox -e coverage"
|
||||
run: tox -e coverage
|
||||
- name: Run "tox -e py27-coverage"
|
||||
run: tox -e py27-coverage
|
||||
|
||||
- name: Upload eliot.log in case of failure
|
||||
uses: actions/upload-artifact@v1
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
venv
|
||||
venv*
|
||||
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
@ -36,7 +36,7 @@ people are Release Maintainers:
|
||||
- [ ] documentation is ready (see above)
|
||||
- [ ] (Release Maintainer): git tag -s -u 0xE34E62D06D0E69CFCA4179FFBDE0D31D68666A7A -m "release Tahoe-LAFS-X.Y.Z" tahoe-lafs-X.Y.Z
|
||||
- [ ] build code locally:
|
||||
tox -e py27,codechecks,coverage,deprecations,docs,integration,upcoming-deprecations
|
||||
tox -e py27,codechecks,deprecations,docs,integration,upcoming-deprecations
|
||||
- [ ] created tarballs (they'll be in dist/ for later comparison)
|
||||
tox -e tarballs
|
||||
- [ ] release version is reporting itself as intended version
|
||||
|
1
newsfragments/3355.other
Normal file
1
newsfragments/3355.other
Normal file
@ -0,0 +1 @@
|
||||
The "coverage" tox environment has been replaced by the "py27-coverage" and "py36-coverage" environments.
|
0
newsfragments/3377.minor
Normal file
0
newsfragments/3377.minor
Normal file
0
newsfragments/3381.minor
Normal file
0
newsfragments/3381.minor
Normal file
0
newsfragments/3387.minor
Normal file
0
newsfragments/3387.minor
Normal file
0
newsfragments/3395.minor
Normal file
0
newsfragments/3395.minor
Normal file
@ -1,3 +1,13 @@
|
||||
from __future__ import division
|
||||
from __future__ import absolute_import
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
# We omit anything that might end up in pickle, just in case.
|
||||
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, range, str, max, min # noqa: F401
|
||||
|
||||
import time, os, pickle, struct
|
||||
from allmydata.storage.crawler import ShareCrawler
|
||||
from allmydata.storage.shares import get_share_file
|
||||
|
@ -48,8 +48,9 @@ class MutableShareFile(object):
|
||||
# our sharefiles share with a recognizable string, plus some random
|
||||
# binary data to reduce the chance that a regular text file will look
|
||||
# like a sharefile.
|
||||
MAGIC = "Tahoe mutable container v1\n" + "\x75\x09\x44\x03\x8e"
|
||||
MAGIC = b"Tahoe mutable container v1\n" + b"\x75\x09\x44\x03\x8e"
|
||||
assert len(MAGIC) == 32
|
||||
assert isinstance(MAGIC, bytes)
|
||||
MAX_SIZE = MAX_MUTABLE_SHARE_SIZE
|
||||
# TODO: decide upon a policy for max share size
|
||||
|
||||
@ -86,7 +87,7 @@ class MutableShareFile(object):
|
||||
self.MAGIC, my_nodeid, write_enabler,
|
||||
data_length, extra_lease_offset,
|
||||
)
|
||||
leases = ("\x00" * self.LEASE_SIZE) * 4
|
||||
leases = (b"\x00" * self.LEASE_SIZE) * 4
|
||||
f.write(header + leases)
|
||||
# data goes here, empty after creation
|
||||
f.write(struct.pack(">L", num_extra_leases))
|
||||
@ -154,7 +155,7 @@ class MutableShareFile(object):
|
||||
# Zero out the old lease info (in order to minimize the chance that
|
||||
# it could accidentally be exposed to a reader later, re #1528).
|
||||
f.seek(old_extra_lease_offset)
|
||||
f.write('\x00' * leases_size)
|
||||
f.write(b'\x00' * leases_size)
|
||||
f.flush()
|
||||
|
||||
# An interrupt here will corrupt the leases.
|
||||
@ -193,7 +194,7 @@ class MutableShareFile(object):
|
||||
# Fill any newly exposed empty space with 0's.
|
||||
if offset > data_length:
|
||||
f.seek(self.DATA_OFFSET+data_length)
|
||||
f.write('\x00'*(offset - data_length))
|
||||
f.write(b'\x00'*(offset - data_length))
|
||||
f.flush()
|
||||
|
||||
new_data_length = offset+length
|
||||
@ -325,10 +326,10 @@ class MutableShareFile(object):
|
||||
modified = 0
|
||||
remaining = 0
|
||||
blank_lease = LeaseInfo(owner_num=0,
|
||||
renew_secret="\x00"*32,
|
||||
cancel_secret="\x00"*32,
|
||||
renew_secret=b"\x00"*32,
|
||||
cancel_secret=b"\x00"*32,
|
||||
expiration_time=0,
|
||||
nodeid="\x00"*20)
|
||||
nodeid=b"\x00"*20)
|
||||
with open(self.home, 'rb+') as f:
|
||||
for (leasenum,lease) in self._enumerate_leases(f):
|
||||
accepting_nodeids.add(lease.nodeid)
|
||||
|
@ -6,6 +6,8 @@ from twisted.python import usage
|
||||
from allmydata.util import configutil
|
||||
from ..common_util import run_cli, parse_cli
|
||||
from ...scripts import create_node
|
||||
from ... import client
|
||||
|
||||
|
||||
def read_config(basedir):
|
||||
tahoe_cfg = os.path.join(basedir, "tahoe.cfg")
|
||||
@ -33,6 +35,31 @@ class Config(unittest.TestCase):
|
||||
e = self.assertRaises(usage.UsageError, parse_cli, verb, *args)
|
||||
self.assertIn("option %s not recognized" % (option,), str(e))
|
||||
|
||||
def test_create_client_config(self):
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
opts = {"nickname": "nick",
|
||||
"webport": "tcp:3456",
|
||||
"hide-ip": False,
|
||||
"listen": "none",
|
||||
"shares-needed": "1",
|
||||
"shares-happy": "1",
|
||||
"shares-total": "1",
|
||||
}
|
||||
create_node.write_node_config(f, opts)
|
||||
create_node.write_client_config(f, opts)
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
# should succeed, no exceptions
|
||||
configutil.validate_config(
|
||||
fname,
|
||||
config,
|
||||
client._valid_config(),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_client(self):
|
||||
basedir = self.mktemp()
|
||||
|
@ -1,14 +1,26 @@
|
||||
"""
|
||||
Tests for allmydata.util.configutil.
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
# Omitted dict, cause worried about interactions.
|
||||
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, list, object, range, str, max, min # noqa: F401
|
||||
|
||||
import os.path
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from allmydata.util import configutil
|
||||
from allmydata.test.no_network import GridTestMixin
|
||||
from ..scripts import create_node
|
||||
from .. import client
|
||||
|
||||
|
||||
class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
class ConfigUtilTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
super(ConfigUtilTests, self).setUp()
|
||||
self.static_valid_config = configutil.ValidConfiguration(
|
||||
@ -20,10 +32,22 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
lambda section_name, item_name: (section_name, item_name) == ("node", "valid"),
|
||||
)
|
||||
|
||||
def create_tahoe_cfg(self, cfg):
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
with open(fname, "w") as f:
|
||||
f.write(cfg)
|
||||
return fname
|
||||
|
||||
def test_config_utils(self):
|
||||
self.basedir = "cli/ConfigUtilTests/test-config-utils"
|
||||
self.set_up_grid(oneshare=True)
|
||||
tahoe_cfg = os.path.join(self.get_clientdir(i=0), "tahoe.cfg")
|
||||
tahoe_cfg = self.create_tahoe_cfg("""\
|
||||
[node]
|
||||
nickname = client-0
|
||||
web.port = adopt-socket:fd=5
|
||||
[storage]
|
||||
enabled = false
|
||||
""")
|
||||
|
||||
# test that at least one option was read correctly
|
||||
config = configutil.get_config(tahoe_cfg)
|
||||
@ -45,12 +69,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
self.failUnlessEqual(config.get("node", "descriptor"), descriptor)
|
||||
|
||||
def test_config_validation_success(self):
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
# should succeed, no exceptions
|
||||
@ -66,12 +85,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
validation but are matched by the dynamic validation is considered
|
||||
valid.
|
||||
"""
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
# should succeed, no exceptions
|
||||
@ -82,12 +96,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_config_validation_invalid_item(self):
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\ninvalid = foo\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\ninvalid = foo\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
e = self.assertRaises(
|
||||
@ -103,12 +112,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
A configuration with a section that is matched by neither the static nor
|
||||
dynamic validators is rejected.
|
||||
"""
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\n[invalid]\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\n[invalid]\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
e = self.assertRaises(
|
||||
@ -124,12 +128,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
A configuration with a section that is matched by neither the static nor
|
||||
dynamic validators is rejected.
|
||||
"""
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\n[invalid]\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\n[invalid]\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
e = self.assertRaises(
|
||||
@ -145,12 +144,7 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
A configuration with a section, item pair that is matched by neither the
|
||||
static nor dynamic validators is rejected.
|
||||
"""
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
f.write('[node]\nvalid = foo\ninvalid = foo\n')
|
||||
fname = self.create_tahoe_cfg('[node]\nvalid = foo\ninvalid = foo\n')
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
e = self.assertRaises(
|
||||
@ -160,28 +154,3 @@ class ConfigUtilTests(GridTestMixin, unittest.TestCase):
|
||||
self.dynamic_valid_config,
|
||||
)
|
||||
self.assertIn("section [node] contains unknown option 'invalid'", str(e))
|
||||
|
||||
def test_create_client_config(self):
|
||||
d = self.mktemp()
|
||||
os.mkdir(d)
|
||||
fname = os.path.join(d, 'tahoe.cfg')
|
||||
|
||||
with open(fname, 'w') as f:
|
||||
opts = {"nickname": "nick",
|
||||
"webport": "tcp:3456",
|
||||
"hide-ip": False,
|
||||
"listen": "none",
|
||||
"shares-needed": "1",
|
||||
"shares-happy": "1",
|
||||
"shares-total": "1",
|
||||
}
|
||||
create_node.write_node_config(f, opts)
|
||||
create_node.write_client_config(f, opts)
|
||||
|
||||
config = configutil.get_config(fname)
|
||||
# should succeed, no exceptions
|
||||
configutil.validate_config(
|
||||
fname,
|
||||
config,
|
||||
client._valid_config(),
|
||||
)
|
||||
|
122
src/allmydata/test/test_connection_status.py
Normal file
122
src/allmydata/test/test_connection_status.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""
|
||||
Tests for allmydata.util.connection_status.
|
||||
|
||||
Port to Python 3.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
|
||||
|
||||
import mock
|
||||
|
||||
from twisted.trial import unittest
|
||||
|
||||
from ..util import connection_status
|
||||
|
||||
class Status(unittest.TestCase):
|
||||
def test_hint_statuses(self):
|
||||
ncs = connection_status._hint_statuses(["h2","h1"],
|
||||
{"h1": "hand1", "h4": "hand4"},
|
||||
{"h1": "st1", "h2": "st2",
|
||||
"h3": "st3"})
|
||||
self.assertEqual(ncs, {"h1 via hand1": "st1",
|
||||
"h2": "st2"})
|
||||
|
||||
def test_reconnector_connected(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.winningHint = "h1"
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected to h1 via hand1")
|
||||
self.assertEqual(cs.non_connected_statuses, {})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connected_others(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.winningHint = "h1"
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected to h1 via hand1")
|
||||
self.assertEqual(cs.non_connected_statuses, {"h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connected_listener(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.listenerStatus = ("listener1", "successful")
|
||||
ci.winningHint = None
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected via listener (listener1)")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connecting(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ri = mock.Mock()
|
||||
ri.state = "connecting"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, False)
|
||||
self.assertEqual(cs.summary, "Trying to connect")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, None)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_waiting(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ri = mock.Mock()
|
||||
ri.state = "waiting"
|
||||
ri.lastAttempt = 10
|
||||
ri.nextAttempt = 20
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
with mock.patch("time.time", return_value=12):
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 5)
|
||||
self.assertEqual(cs.connected, False)
|
||||
self.assertEqual(cs.summary,
|
||||
"Reconnecting in 8 seconds (last attempt 2s ago)")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, None)
|
||||
self.assertEqual(cs.last_received_time, 5)
|
@ -7,7 +7,6 @@ from foolscap.connections import tcp
|
||||
from ..node import PrivacyError, config_from_string
|
||||
from ..node import create_connection_handlers
|
||||
from ..node import create_main_tub, _tub_portlocation
|
||||
from ..util import connection_status
|
||||
from ..util.i2p_provider import create as create_i2p_provider
|
||||
from ..util.tor_provider import create as create_tor_provider
|
||||
|
||||
@ -463,106 +462,3 @@ class Privacy(unittest.TestCase):
|
||||
str(ctx.exception),
|
||||
"tub.location includes tcp: hint",
|
||||
)
|
||||
|
||||
class Status(unittest.TestCase):
|
||||
def test_hint_statuses(self):
|
||||
ncs = connection_status._hint_statuses(["h2","h1"],
|
||||
{"h1": "hand1", "h4": "hand4"},
|
||||
{"h1": "st1", "h2": "st2",
|
||||
"h3": "st3"})
|
||||
self.assertEqual(ncs, {"h1 via hand1": "st1",
|
||||
"h2": "st2"})
|
||||
|
||||
def test_reconnector_connected(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.winningHint = "h1"
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected to h1 via hand1")
|
||||
self.assertEqual(cs.non_connected_statuses, {})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connected_others(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.winningHint = "h1"
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected to h1 via hand1")
|
||||
self.assertEqual(cs.non_connected_statuses, {"h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connected_listener(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ci.listenerStatus = ("listener1", "successful")
|
||||
ci.winningHint = None
|
||||
ci.establishedAt = 120
|
||||
ri = mock.Mock()
|
||||
ri.state = "connected"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, True)
|
||||
self.assertEqual(cs.summary, "Connected via listener (listener1)")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, 120)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_connecting(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ri = mock.Mock()
|
||||
ri.state = "connecting"
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 123)
|
||||
self.assertEqual(cs.connected, False)
|
||||
self.assertEqual(cs.summary, "Trying to connect")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, None)
|
||||
self.assertEqual(cs.last_received_time, 123)
|
||||
|
||||
def test_reconnector_waiting(self):
|
||||
ci = mock.Mock()
|
||||
ci.connectorStatuses = {"h1": "st1", "h2": "st2"}
|
||||
ci.connectionHandlers = {"h1": "hand1"}
|
||||
ri = mock.Mock()
|
||||
ri.state = "waiting"
|
||||
ri.lastAttempt = 10
|
||||
ri.nextAttempt = 20
|
||||
ri.connectionInfo = ci
|
||||
rc = mock.Mock
|
||||
rc.getReconnectionInfo = mock.Mock(return_value=ri)
|
||||
with mock.patch("time.time", return_value=12):
|
||||
cs = connection_status.from_foolscap_reconnector(rc, 5)
|
||||
self.assertEqual(cs.connected, False)
|
||||
self.assertEqual(cs.summary,
|
||||
"Reconnecting in 8 seconds (last attempt 2s ago)")
|
||||
self.assertEqual(cs.non_connected_statuses,
|
||||
{"h1 via hand1": "st1", "h2": "st2"})
|
||||
self.assertEqual(cs.last_connection_time, None)
|
||||
self.assertEqual(cs.last_received_time, 5)
|
||||
|
@ -1,8 +1,19 @@
|
||||
"""
|
||||
Tests for twisted.storage that uses Web APIs.
|
||||
|
||||
Partially ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
# Omitted list sinc it broke a test on Python 2. Shouldn't require further
|
||||
# work, when we switch to Python 3 we'll be dropping this, anyway.
|
||||
from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, object, range, str, max, min # noqa: F401
|
||||
|
||||
import time
|
||||
import os.path
|
||||
@ -18,7 +29,10 @@ from twisted.web.template import flattenString
|
||||
# We need to use `nevow.inevow.IRequest` for now for compatibility
|
||||
# with the code in web/common.py. Once nevow bits are gone from
|
||||
# web/common.py, we can use `twisted.web.iweb.IRequest` here.
|
||||
from nevow.inevow import IRequest
|
||||
if PY2:
|
||||
from nevow.inevow import IRequest
|
||||
else:
|
||||
from twisted.web.iweb import IRequest
|
||||
|
||||
from twisted.web.server import Request
|
||||
from twisted.web.test.requesthelper import DummyChannel
|
||||
@ -36,11 +50,11 @@ from allmydata.web.storage import (
|
||||
StorageStatusElement,
|
||||
remove_prefix
|
||||
)
|
||||
from .test_storage import FakeCanary
|
||||
from .common_py3 import FakeCanary
|
||||
|
||||
def remove_tags(s):
|
||||
s = re.sub(r'<[^>]*>', ' ', s)
|
||||
s = re.sub(r'\s+', ' ', s)
|
||||
s = re.sub(br'<[^>]*>', b' ', s)
|
||||
s = re.sub(br'\s+', b' ', s)
|
||||
return s
|
||||
|
||||
def renderSynchronously(ss):
|
||||
@ -89,6 +103,7 @@ class MyStorageServer(StorageServer):
|
||||
self.bucket_counter = MyBucketCountingCrawler(self, statefile)
|
||||
self.bucket_counter.setServiceParent(self)
|
||||
|
||||
|
||||
class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
|
||||
def setUp(self):
|
||||
@ -100,7 +115,7 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
def test_bucket_counter(self):
|
||||
basedir = "storage/BucketCounter/bucket_counter"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
# to make sure we capture the bucket-counting-crawler in the middle
|
||||
# of a cycle, we reach in and reduce its maximum slice time to 0. We
|
||||
# also make it start sooner than usual.
|
||||
@ -113,12 +128,12 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
|
||||
# this sample is before the crawler has started doing anything
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Accepting new shares: Yes", s)
|
||||
self.failUnlessIn("Reserved space: - 0 B (0)", s)
|
||||
self.failUnlessIn("Total buckets: Not computed yet", s)
|
||||
self.failUnlessIn("Next crawl in", s)
|
||||
self.failUnlessIn(b"Accepting new shares: Yes", s)
|
||||
self.failUnlessIn(b"Reserved space: - 0 B (0)", s)
|
||||
self.failUnlessIn(b"Total buckets: Not computed yet", s)
|
||||
self.failUnlessIn(b"Next crawl in", s)
|
||||
|
||||
# give the bucket-counting-crawler one tick to get started. The
|
||||
# cpu_slice=0 will force it to yield right after it processes the
|
||||
@ -137,8 +152,8 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
ss.bucket_counter.cpu_slice = 100.0 # finish as fast as possible
|
||||
html = renderSynchronously(w)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn(" Current crawl ", s)
|
||||
self.failUnlessIn(" (next work in ", s)
|
||||
self.failUnlessIn(b" Current crawl ", s)
|
||||
self.failUnlessIn(b" (next work in ", s)
|
||||
d.addCallback(_check)
|
||||
|
||||
# now give it enough time to complete a full cycle
|
||||
@ -149,15 +164,15 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
ss.bucket_counter.cpu_slice = orig_cpu_slice
|
||||
html = renderSynchronously(w)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Total buckets: 0 (the number of", s)
|
||||
self.failUnless("Next crawl in 59 minutes" in s or "Next crawl in 60 minutes" in s, s)
|
||||
self.failUnlessIn(b"Total buckets: 0 (the number of", s)
|
||||
self.failUnless(b"Next crawl in 59 minutes" in s or "Next crawl in 60 minutes" in s, s)
|
||||
d.addCallback(_check2)
|
||||
return d
|
||||
|
||||
def test_bucket_counter_cleanup(self):
|
||||
basedir = "storage/BucketCounter/bucket_counter_cleanup"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
# to make sure we capture the bucket-counting-crawler in the middle
|
||||
# of a cycle, we reach in and reduce its maximum slice time to 0.
|
||||
ss.bucket_counter.slow_start = 0
|
||||
@ -190,16 +205,16 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
def _check2(ignored):
|
||||
ss.bucket_counter.cpu_slice = orig_cpu_slice
|
||||
s = ss.bucket_counter.get_state()
|
||||
self.failIf(-12 in s["bucket-counts"], s["bucket-counts"].keys())
|
||||
self.failIf(-12 in s["bucket-counts"], list(s["bucket-counts"].keys()))
|
||||
self.failIf("bogusprefix!" in s["storage-index-samples"],
|
||||
s["storage-index-samples"].keys())
|
||||
list(s["storage-index-samples"].keys()))
|
||||
d.addCallback(_check2)
|
||||
return d
|
||||
|
||||
def test_bucket_counter_eta(self):
|
||||
basedir = "storage/BucketCounter/bucket_counter_eta"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = MyStorageServer(basedir, "\x00" * 20)
|
||||
ss = MyStorageServer(basedir, b"\x00" * 20)
|
||||
ss.bucket_counter.slow_start = 0
|
||||
# these will be fired inside finished_prefix()
|
||||
hooks = ss.bucket_counter.hook_ds = [defer.Deferred() for i in range(3)]
|
||||
@ -211,20 +226,20 @@ class BucketCounter(unittest.TestCase, pollmixin.PollMixin):
|
||||
# no ETA is available yet
|
||||
html = renderSynchronously(w)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("complete (next work", s)
|
||||
self.failUnlessIn(b"complete (next work", s)
|
||||
|
||||
def _check_2(ignored):
|
||||
# one prefix has finished, so an ETA based upon that elapsed time
|
||||
# should be available.
|
||||
html = renderSynchronously(w)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("complete (ETA ", s)
|
||||
self.failUnlessIn(b"complete (ETA ", s)
|
||||
|
||||
def _check_3(ignored):
|
||||
# two prefixes have finished
|
||||
html = renderSynchronously(w)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("complete (ETA ", s)
|
||||
self.failUnlessIn(b"complete (ETA ", s)
|
||||
d.callback("done")
|
||||
|
||||
hooks[0].addCallback(_check_1).addErrback(d.errback)
|
||||
@ -275,27 +290,27 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
|
||||
def make_shares(self, ss):
|
||||
def make(si):
|
||||
return (si, hashutil.tagged_hash("renew", si),
|
||||
hashutil.tagged_hash("cancel", si))
|
||||
return (si, hashutil.tagged_hash(b"renew", si),
|
||||
hashutil.tagged_hash(b"cancel", si))
|
||||
def make_mutable(si):
|
||||
return (si, hashutil.tagged_hash("renew", si),
|
||||
hashutil.tagged_hash("cancel", si),
|
||||
hashutil.tagged_hash("write-enabler", si))
|
||||
return (si, hashutil.tagged_hash(b"renew", si),
|
||||
hashutil.tagged_hash(b"cancel", si),
|
||||
hashutil.tagged_hash(b"write-enabler", si))
|
||||
def make_extra_lease(si, num):
|
||||
return (hashutil.tagged_hash("renew-%d" % num, si),
|
||||
hashutil.tagged_hash("cancel-%d" % num, si))
|
||||
return (hashutil.tagged_hash(b"renew-%d" % num, si),
|
||||
hashutil.tagged_hash(b"cancel-%d" % num, si))
|
||||
|
||||
immutable_si_0, rs0, cs0 = make("\x00" * 16)
|
||||
immutable_si_1, rs1, cs1 = make("\x01" * 16)
|
||||
immutable_si_0, rs0, cs0 = make(b"\x00" * 16)
|
||||
immutable_si_1, rs1, cs1 = make(b"\x01" * 16)
|
||||
rs1a, cs1a = make_extra_lease(immutable_si_1, 1)
|
||||
mutable_si_2, rs2, cs2, we2 = make_mutable("\x02" * 16)
|
||||
mutable_si_3, rs3, cs3, we3 = make_mutable("\x03" * 16)
|
||||
mutable_si_2, rs2, cs2, we2 = make_mutable(b"\x02" * 16)
|
||||
mutable_si_3, rs3, cs3, we3 = make_mutable(b"\x03" * 16)
|
||||
rs3a, cs3a = make_extra_lease(mutable_si_3, 1)
|
||||
sharenums = [0]
|
||||
canary = FakeCanary()
|
||||
# note: 'tahoe debug dump-share' will not handle this file, since the
|
||||
# inner contents are not a valid CHK share
|
||||
data = "\xff" * 1000
|
||||
data = b"\xff" * 1000
|
||||
|
||||
a,w = ss.remote_allocate_buckets(immutable_si_0, rs0, cs0, sharenums,
|
||||
1000, canary)
|
||||
@ -322,7 +337,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
def test_basic(self):
|
||||
basedir = "storage/LeaseCrawler/basic"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = InstrumentedStorageServer(basedir, "\x00" * 20)
|
||||
ss = InstrumentedStorageServer(basedir, b"\x00" * 20)
|
||||
# make it start sooner than usual.
|
||||
lc = ss.lease_checker
|
||||
lc.slow_start = 0
|
||||
@ -339,7 +354,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
storage_index_to_dir(immutable_si_0),
|
||||
"not-a-share")
|
||||
f = open(fn, "wb")
|
||||
f.write("I am not a share.\n")
|
||||
f.write(b"I am not a share.\n")
|
||||
f.close()
|
||||
|
||||
# this is before the crawl has started, so we're not in a cycle yet
|
||||
@ -398,25 +413,25 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html_in_cycle(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("So far, this cycle has examined "
|
||||
"1 shares in 1 buckets (0 mutable / 1 immutable) ", s)
|
||||
self.failUnlessIn("and has recovered: "
|
||||
"0 shares, 0 buckets (0 mutable / 0 immutable), "
|
||||
"0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn("If expiration were enabled, "
|
||||
"we would have recovered: "
|
||||
"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
" 0 B (0 B / 0 B) by now", s)
|
||||
self.failUnlessIn("and the remainder of this cycle "
|
||||
"would probably recover: "
|
||||
"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
" 0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn("and the whole cycle would probably recover: "
|
||||
"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
" 0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn("if we were strictly using each lease's default "
|
||||
"31-day lease lifetime", s)
|
||||
self.failUnlessIn("this cycle would be expected to recover: ", s)
|
||||
self.failUnlessIn(b"So far, this cycle has examined "
|
||||
b"1 shares in 1 buckets (0 mutable / 1 immutable) ", s)
|
||||
self.failUnlessIn(b"and has recovered: "
|
||||
b"0 shares, 0 buckets (0 mutable / 0 immutable), "
|
||||
b"0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn(b"If expiration were enabled, "
|
||||
b"we would have recovered: "
|
||||
b"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
b" 0 B (0 B / 0 B) by now", s)
|
||||
self.failUnlessIn(b"and the remainder of this cycle "
|
||||
b"would probably recover: "
|
||||
b"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
b" 0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn(b"and the whole cycle would probably recover: "
|
||||
b"0 shares, 0 buckets (0 mutable / 0 immutable),"
|
||||
b" 0 B (0 B / 0 B)", s)
|
||||
self.failUnlessIn(b"if we were strictly using each lease's default "
|
||||
b"31-day lease lifetime", s)
|
||||
self.failUnlessIn(b"this cycle would be expected to recover: ", s)
|
||||
d.addCallback(_check_html_in_cycle)
|
||||
|
||||
# wait for the crawler to finish the first cycle. Nothing should have
|
||||
@ -473,11 +488,11 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("recovered: 0 shares, 0 buckets "
|
||||
"(0 mutable / 0 immutable), 0 B (0 B / 0 B) ", s)
|
||||
self.failUnlessIn("and saw a total of 4 shares, 4 buckets "
|
||||
"(2 mutable / 2 immutable),", s)
|
||||
self.failUnlessIn("but expiration was not enabled", s)
|
||||
self.failUnlessIn(b"recovered: 0 shares, 0 buckets "
|
||||
b"(0 mutable / 0 immutable), 0 B (0 B / 0 B) ", s)
|
||||
self.failUnlessIn(b"and saw a total of 4 shares, 4 buckets "
|
||||
b"(2 mutable / 2 immutable),", s)
|
||||
self.failUnlessIn(b"but expiration was not enabled", s)
|
||||
d.addCallback(_check_html)
|
||||
d.addCallback(lambda ign: renderJSON(webstatus))
|
||||
def _check_json(raw):
|
||||
@ -505,7 +520,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
fileutil.make_dirs(basedir)
|
||||
# setting expiration_time to 2000 means that any lease which is more
|
||||
# than 2000s old will be expired.
|
||||
ss = InstrumentedStorageServer(basedir, "\x00" * 20,
|
||||
ss = InstrumentedStorageServer(basedir, b"\x00" * 20,
|
||||
expiration_enabled=True,
|
||||
expiration_mode="age",
|
||||
expiration_override_lease_duration=2000)
|
||||
@ -578,11 +593,11 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
# predictor thinks we'll have 5 shares and that we'll delete them
|
||||
# all. This part of the test depends upon the SIs landing right
|
||||
# where they do now.
|
||||
self.failUnlessIn("The remainder of this cycle is expected to "
|
||||
"recover: 4 shares, 4 buckets", s)
|
||||
self.failUnlessIn("The whole cycle is expected to examine "
|
||||
"5 shares in 5 buckets and to recover: "
|
||||
"5 shares, 5 buckets", s)
|
||||
self.failUnlessIn(b"The remainder of this cycle is expected to "
|
||||
b"recover: 4 shares, 4 buckets", s)
|
||||
self.failUnlessIn(b"The whole cycle is expected to examine "
|
||||
b"5 shares in 5 buckets and to recover: "
|
||||
b"5 shares, 5 buckets", s)
|
||||
d.addCallback(_check_html_in_cycle)
|
||||
|
||||
# wait for the crawler to finish the first cycle. Two shares should
|
||||
@ -632,9 +647,9 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Expiration Enabled: expired leases will be removed", s)
|
||||
self.failUnlessIn("Leases created or last renewed more than 33 minutes ago will be considered expired.", s)
|
||||
self.failUnlessIn(" recovered: 2 shares, 2 buckets (1 mutable / 1 immutable), ", s)
|
||||
self.failUnlessIn(b"Expiration Enabled: expired leases will be removed", s)
|
||||
self.failUnlessIn(b"Leases created or last renewed more than 33 minutes ago will be considered expired.", s)
|
||||
self.failUnlessIn(b" recovered: 2 shares, 2 buckets (1 mutable / 1 immutable), ", s)
|
||||
d.addCallback(_check_html)
|
||||
return d
|
||||
|
||||
@ -645,7 +660,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
# is more than 2000s old will be expired.
|
||||
now = time.time()
|
||||
then = int(now - 2000)
|
||||
ss = InstrumentedStorageServer(basedir, "\x00" * 20,
|
||||
ss = InstrumentedStorageServer(basedir, b"\x00" * 20,
|
||||
expiration_enabled=True,
|
||||
expiration_mode="cutoff-date",
|
||||
expiration_cutoff_date=then)
|
||||
@ -722,11 +737,11 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
# predictor thinks we'll have 5 shares and that we'll delete them
|
||||
# all. This part of the test depends upon the SIs landing right
|
||||
# where they do now.
|
||||
self.failUnlessIn("The remainder of this cycle is expected to "
|
||||
"recover: 4 shares, 4 buckets", s)
|
||||
self.failUnlessIn("The whole cycle is expected to examine "
|
||||
"5 shares in 5 buckets and to recover: "
|
||||
"5 shares, 5 buckets", s)
|
||||
self.failUnlessIn(b"The remainder of this cycle is expected to "
|
||||
b"recover: 4 shares, 4 buckets", s)
|
||||
self.failUnlessIn(b"The whole cycle is expected to examine "
|
||||
b"5 shares in 5 buckets and to recover: "
|
||||
b"5 shares, 5 buckets", s)
|
||||
d.addCallback(_check_html_in_cycle)
|
||||
|
||||
# wait for the crawler to finish the first cycle. Two shares should
|
||||
@ -778,12 +793,13 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Expiration Enabled:"
|
||||
" expired leases will be removed", s)
|
||||
date = time.strftime("%Y-%m-%d (%d-%b-%Y) UTC", time.gmtime(then))
|
||||
substr = "Leases created or last renewed before %s will be considered expired." % date
|
||||
self.failUnlessIn(b"Expiration Enabled:"
|
||||
b" expired leases will be removed", s)
|
||||
date = time.strftime(
|
||||
u"%Y-%m-%d (%d-%b-%Y) UTC", time.gmtime(then)).encode("ascii")
|
||||
substr =b"Leases created or last renewed before %s will be considered expired." % date
|
||||
self.failUnlessIn(substr, s)
|
||||
self.failUnlessIn(" recovered: 2 shares, 2 buckets (1 mutable / 1 immutable), ", s)
|
||||
self.failUnlessIn(b" recovered: 2 shares, 2 buckets (1 mutable / 1 immutable), ", s)
|
||||
d.addCallback(_check_html)
|
||||
return d
|
||||
|
||||
@ -792,7 +808,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
fileutil.make_dirs(basedir)
|
||||
now = time.time()
|
||||
then = int(now - 2000)
|
||||
ss = StorageServer(basedir, "\x00" * 20,
|
||||
ss = StorageServer(basedir, b"\x00" * 20,
|
||||
expiration_enabled=True,
|
||||
expiration_mode="cutoff-date",
|
||||
expiration_cutoff_date=then,
|
||||
@ -840,7 +856,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("The following sharetypes will be expired: immutable.", s)
|
||||
self.failUnlessIn(b"The following sharetypes will be expired: immutable.", s)
|
||||
d.addCallback(_check_html)
|
||||
return d
|
||||
|
||||
@ -849,7 +865,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
fileutil.make_dirs(basedir)
|
||||
now = time.time()
|
||||
then = int(now - 2000)
|
||||
ss = StorageServer(basedir, "\x00" * 20,
|
||||
ss = StorageServer(basedir, b"\x00" * 20,
|
||||
expiration_enabled=True,
|
||||
expiration_mode="cutoff-date",
|
||||
expiration_cutoff_date=then,
|
||||
@ -897,7 +913,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
d.addCallback(lambda ign: renderDeferred(webstatus))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("The following sharetypes will be expired: mutable.", s)
|
||||
self.failUnlessIn(b"The following sharetypes will be expired: mutable.", s)
|
||||
d.addCallback(_check_html)
|
||||
return d
|
||||
|
||||
@ -905,14 +921,14 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
basedir = "storage/LeaseCrawler/bad_mode"
|
||||
fileutil.make_dirs(basedir)
|
||||
e = self.failUnlessRaises(ValueError,
|
||||
StorageServer, basedir, "\x00" * 20,
|
||||
StorageServer, basedir, b"\x00" * 20,
|
||||
expiration_mode="bogus")
|
||||
self.failUnlessIn("GC mode 'bogus' must be 'age' or 'cutoff-date'", str(e))
|
||||
|
||||
def test_limited_history(self):
|
||||
basedir = "storage/LeaseCrawler/limited_history"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
# make it start sooner than usual.
|
||||
lc = ss.lease_checker
|
||||
lc.slow_start = 0
|
||||
@ -944,7 +960,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
def test_unpredictable_future(self):
|
||||
basedir = "storage/LeaseCrawler/unpredictable_future"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
# make it start sooner than usual.
|
||||
lc = ss.lease_checker
|
||||
lc.slow_start = 0
|
||||
@ -1007,7 +1023,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
def test_no_st_blocks(self):
|
||||
basedir = "storage/LeaseCrawler/no_st_blocks"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = No_ST_BLOCKS_StorageServer(basedir, "\x00" * 20,
|
||||
ss = No_ST_BLOCKS_StorageServer(basedir, b"\x00" * 20,
|
||||
expiration_mode="age",
|
||||
expiration_override_lease_duration=-1000)
|
||||
# a negative expiration_time= means the "configured-"
|
||||
@ -1046,7 +1062,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
]
|
||||
basedir = "storage/LeaseCrawler/share_corruption"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = InstrumentedStorageServer(basedir, "\x00" * 20)
|
||||
ss = InstrumentedStorageServer(basedir, b"\x00" * 20)
|
||||
w = StorageStatus(ss)
|
||||
# make it start sooner than usual.
|
||||
lc = ss.lease_checker
|
||||
@ -1064,7 +1080,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
fn = os.path.join(ss.sharedir, storage_index_to_dir(first), "0")
|
||||
f = open(fn, "rb+")
|
||||
f.seek(0)
|
||||
f.write("BAD MAGIC")
|
||||
f.write(b"BAD MAGIC")
|
||||
f.close()
|
||||
# if get_share_file() doesn't see the correct mutable magic, it
|
||||
# assumes the file is an immutable share, and then
|
||||
@ -1073,7 +1089,7 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
# UnknownImmutableContainerVersionError.
|
||||
|
||||
# also create an empty bucket
|
||||
empty_si = base32.b2a("\x04"*16)
|
||||
empty_si = base32.b2a(b"\x04"*16)
|
||||
empty_bucket_dir = os.path.join(ss.sharedir,
|
||||
storage_index_to_dir(empty_si))
|
||||
fileutil.make_dirs(empty_bucket_dir)
|
||||
@ -1094,7 +1110,9 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
rec = so_far["space-recovered"]
|
||||
self.failUnlessEqual(rec["examined-buckets"], 1)
|
||||
self.failUnlessEqual(rec["examined-shares"], 0)
|
||||
self.failUnlessEqual(so_far["corrupt-shares"], [(first_b32, 0)])
|
||||
[(actual_b32, i)] = so_far["corrupt-shares"]
|
||||
actual_b32 = actual_b32.encode("ascii")
|
||||
self.failUnlessEqual((actual_b32, i), (first_b32, 0))
|
||||
d.addCallback(_after_first_bucket)
|
||||
|
||||
d.addCallback(lambda ign: renderJSON(w))
|
||||
@ -1103,13 +1121,15 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
# grr. json turns all dict keys into strings.
|
||||
so_far = data["lease-checker"]["cycle-to-date"]
|
||||
corrupt_shares = so_far["corrupt-shares"]
|
||||
# it also turns all tuples into lists
|
||||
self.failUnlessEqual(corrupt_shares, [[first_b32, 0]])
|
||||
# it also turns all tuples into lists, and result is unicode:
|
||||
[(actual_b32, i)] = corrupt_shares
|
||||
actual_b32 = actual_b32.encode("ascii")
|
||||
self.failUnlessEqual([actual_b32, i], [first_b32, 0])
|
||||
d.addCallback(_check_json)
|
||||
d.addCallback(lambda ign: renderDeferred(w))
|
||||
def _check_html(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Corrupt shares: SI %s shnum 0" % first_b32, s)
|
||||
self.failUnlessIn(b"Corrupt shares: SI %s shnum 0" % first_b32, s)
|
||||
d.addCallback(_check_html)
|
||||
|
||||
def _wait():
|
||||
@ -1122,19 +1142,22 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin):
|
||||
rec = last["space-recovered"]
|
||||
self.failUnlessEqual(rec["examined-buckets"], 5)
|
||||
self.failUnlessEqual(rec["examined-shares"], 3)
|
||||
self.failUnlessEqual(last["corrupt-shares"], [(first_b32, 0)])
|
||||
[(actual_b32, i)] = last["corrupt-shares"]
|
||||
actual_b32 = actual_b32.encode("ascii")
|
||||
self.failUnlessEqual((actual_b32, i), (first_b32, 0))
|
||||
d.addCallback(_after_first_cycle)
|
||||
d.addCallback(lambda ign: renderJSON(w))
|
||||
def _check_json_history(raw):
|
||||
data = json.loads(raw)
|
||||
last = data["lease-checker"]["history"]["0"]
|
||||
corrupt_shares = last["corrupt-shares"]
|
||||
self.failUnlessEqual(corrupt_shares, [[first_b32, 0]])
|
||||
[(actual_b32, i)] = last["corrupt-shares"]
|
||||
actual_b32 = actual_b32.encode("ascii")
|
||||
self.failUnlessEqual([actual_b32, i], [first_b32, 0])
|
||||
d.addCallback(_check_json_history)
|
||||
d.addCallback(lambda ign: renderDeferred(w))
|
||||
def _check_html_history(html):
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Corrupt shares: SI %s shnum 0" % first_b32, s)
|
||||
self.failUnlessIn(b"Corrupt shares: SI %s shnum 0" % first_b32, s)
|
||||
d.addCallback(_check_html_history)
|
||||
|
||||
def _cleanup(res):
|
||||
@ -1156,23 +1179,23 @@ class WebStatus(unittest.TestCase, pollmixin.PollMixin):
|
||||
def test_no_server(self):
|
||||
w = StorageStatus(None)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>No Storage Server Running</h1>", html)
|
||||
self.failUnlessIn(b"<h1>No Storage Server Running</h1>", html)
|
||||
|
||||
def test_status(self):
|
||||
basedir = "storage/WebStatus/status"
|
||||
fileutil.make_dirs(basedir)
|
||||
nodeid = "\x00" * 20
|
||||
nodeid = b"\x00" * 20
|
||||
ss = StorageServer(basedir, nodeid)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss, "nickname")
|
||||
d = renderDeferred(w)
|
||||
def _check_html(html):
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Server Nickname: nickname", s)
|
||||
self.failUnlessIn("Server Nodeid: %s" % base32.b2a(nodeid), s)
|
||||
self.failUnlessIn("Accepting new shares: Yes", s)
|
||||
self.failUnlessIn("Reserved space: - 0 B (0)", s)
|
||||
self.failUnlessIn(b"Server Nickname: nickname", s)
|
||||
self.failUnlessIn(b"Server Nodeid: %s" % base32.b2a(nodeid), s)
|
||||
self.failUnlessIn(b"Accepting new shares: Yes", s)
|
||||
self.failUnlessIn(b"Reserved space: - 0 B (0)", s)
|
||||
d.addCallback(_check_html)
|
||||
d.addCallback(lambda ign: renderJSON(w))
|
||||
def _check_json(raw):
|
||||
@ -1195,15 +1218,15 @@ class WebStatus(unittest.TestCase, pollmixin.PollMixin):
|
||||
# (test runs on all platforms).
|
||||
basedir = "storage/WebStatus/status_no_disk_stats"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Accepting new shares: Yes", s)
|
||||
self.failUnlessIn("Total disk space: ?", s)
|
||||
self.failUnlessIn("Space Available to Tahoe: ?", s)
|
||||
self.failUnlessIn(b"Accepting new shares: Yes", s)
|
||||
self.failUnlessIn(b"Total disk space: ?", s)
|
||||
self.failUnlessIn(b"Space Available to Tahoe: ?", s)
|
||||
self.failUnless(ss.get_available_space() is None)
|
||||
|
||||
def test_status_bad_disk_stats(self):
|
||||
@ -1215,15 +1238,15 @@ class WebStatus(unittest.TestCase, pollmixin.PollMixin):
|
||||
# show that no shares will be accepted, and get_available_space() should be 0.
|
||||
basedir = "storage/WebStatus/status_bad_disk_stats"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20)
|
||||
ss = StorageServer(basedir, b"\x00" * 20)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Accepting new shares: No", s)
|
||||
self.failUnlessIn("Total disk space: ?", s)
|
||||
self.failUnlessIn("Space Available to Tahoe: ?", s)
|
||||
self.failUnlessIn(b"Accepting new shares: No", s)
|
||||
self.failUnlessIn(b"Total disk space: ?", s)
|
||||
self.failUnlessIn(b"Space Available to Tahoe: ?", s)
|
||||
self.failUnlessEqual(ss.get_available_space(), 0)
|
||||
|
||||
def test_status_right_disk_stats(self):
|
||||
@ -1235,7 +1258,7 @@ class WebStatus(unittest.TestCase, pollmixin.PollMixin):
|
||||
|
||||
basedir = "storage/WebStatus/status_right_disk_stats"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20, reserved_space=reserved)
|
||||
ss = StorageServer(basedir, b"\x00" * 20, reserved_space=reserved)
|
||||
expecteddir = ss.sharedir
|
||||
|
||||
def call_get_disk_stats(whichdir, reserved_space=0):
|
||||
@ -1256,48 +1279,48 @@ class WebStatus(unittest.TestCase, pollmixin.PollMixin):
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Total disk space: 5.00 GB", s)
|
||||
self.failUnlessIn("Disk space used: - 1.00 GB", s)
|
||||
self.failUnlessIn("Disk space free (root): 4.00 GB", s)
|
||||
self.failUnlessIn("Disk space free (non-root): 3.00 GB", s)
|
||||
self.failUnlessIn("Reserved space: - 1.00 GB", s)
|
||||
self.failUnlessIn("Space Available to Tahoe: 2.00 GB", s)
|
||||
self.failUnlessIn(b"Total disk space: 5.00 GB", s)
|
||||
self.failUnlessIn(b"Disk space used: - 1.00 GB", s)
|
||||
self.failUnlessIn(b"Disk space free (root): 4.00 GB", s)
|
||||
self.failUnlessIn(b"Disk space free (non-root): 3.00 GB", s)
|
||||
self.failUnlessIn(b"Reserved space: - 1.00 GB", s)
|
||||
self.failUnlessIn(b"Space Available to Tahoe: 2.00 GB", s)
|
||||
self.failUnlessEqual(ss.get_available_space(), 2*GB)
|
||||
|
||||
def test_readonly(self):
|
||||
basedir = "storage/WebStatus/readonly"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20, readonly_storage=True)
|
||||
ss = StorageServer(basedir, b"\x00" * 20, readonly_storage=True)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Accepting new shares: No", s)
|
||||
self.failUnlessIn(b"Accepting new shares: No", s)
|
||||
|
||||
def test_reserved(self):
|
||||
basedir = "storage/WebStatus/reserved"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20, reserved_space=10e6)
|
||||
ss = StorageServer(basedir, b"\x00" * 20, reserved_space=10e6)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Reserved space: - 10.00 MB (10000000)", s)
|
||||
self.failUnlessIn(b"Reserved space: - 10.00 MB (10000000)", s)
|
||||
|
||||
def test_huge_reserved(self):
|
||||
basedir = "storage/WebStatus/reserved"
|
||||
fileutil.make_dirs(basedir)
|
||||
ss = StorageServer(basedir, "\x00" * 20, reserved_space=10e6)
|
||||
ss = StorageServer(basedir, b"\x00" * 20, reserved_space=10e6)
|
||||
ss.setServiceParent(self.s)
|
||||
w = StorageStatus(ss)
|
||||
html = renderSynchronously(w)
|
||||
self.failUnlessIn("<h1>Storage Server Status</h1>", html)
|
||||
self.failUnlessIn(b"<h1>Storage Server Status</h1>", html)
|
||||
s = remove_tags(html)
|
||||
self.failUnlessIn("Reserved space: - 10.00 MB (10000000)", s)
|
||||
self.failUnlessIn(b"Reserved space: - 10.00 MB (10000000)", s)
|
||||
|
||||
def test_util(self):
|
||||
w = StorageStatusElement(None, None)
|
||||
|
@ -36,6 +36,7 @@ PORTED_MODULES = [
|
||||
"allmydata.interfaces",
|
||||
"allmydata.monitor",
|
||||
"allmydata.storage.crawler",
|
||||
"allmydata.storage.expirer",
|
||||
"allmydata.test.common_py3",
|
||||
"allmydata.uri",
|
||||
"allmydata.util._python3",
|
||||
@ -43,6 +44,8 @@ PORTED_MODULES = [
|
||||
"allmydata.util.assertutil",
|
||||
"allmydata.util.base32",
|
||||
"allmydata.util.base62",
|
||||
"allmydata.util.configutil",
|
||||
"allmydata.util.connection_status",
|
||||
"allmydata.util.deferredutil",
|
||||
"allmydata.util.fileutil",
|
||||
"allmydata.util.dictutil",
|
||||
@ -69,6 +72,8 @@ PORTED_TEST_MODULES = [
|
||||
"allmydata.test.test_base32",
|
||||
"allmydata.test.test_base62",
|
||||
"allmydata.test.test_codec",
|
||||
"allmydata.test.test_configutil",
|
||||
"allmydata.test.test_connection_status",
|
||||
"allmydata.test.test_crawler",
|
||||
"allmydata.test.test_crypto",
|
||||
"allmydata.test.test_deferredutil",
|
||||
@ -87,6 +92,7 @@ PORTED_TEST_MODULES = [
|
||||
"allmydata.test.test_python3",
|
||||
"allmydata.test.test_spans",
|
||||
"allmydata.test.test_statistics",
|
||||
"allmydata.test.test_storage_web",
|
||||
"allmydata.test.test_time_format",
|
||||
"allmydata.test.test_uri",
|
||||
"allmydata.test.test_util",
|
||||
|
@ -1,8 +1,32 @@
|
||||
"""
|
||||
Read/write config files.
|
||||
|
||||
from ConfigParser import SafeConfigParser
|
||||
Configuration is returned as native strings.
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
# We don't do open(), because we want files to read/write native strs when
|
||||
# we do "r" or "w".
|
||||
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
|
||||
|
||||
if PY2:
|
||||
# In theory on Python 2 configparser also works, but then code gets the
|
||||
# wrong exceptions and they don't get handled. So just use native parser
|
||||
# for now.
|
||||
from ConfigParser import SafeConfigParser
|
||||
else:
|
||||
from configparser import SafeConfigParser
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
class UnknownConfigError(Exception):
|
||||
"""
|
||||
An unknown config item was found.
|
||||
@ -12,11 +36,16 @@ class UnknownConfigError(Exception):
|
||||
|
||||
|
||||
def get_config(tahoe_cfg):
|
||||
"""Load the config, returning a SafeConfigParser.
|
||||
|
||||
Configuration is returned as native strings.
|
||||
"""
|
||||
config = SafeConfigParser()
|
||||
with open(tahoe_cfg, "rb") as f:
|
||||
# Skip any initial Byte Order Mark. Since this is an ordinary file, we
|
||||
# don't need to handle incomplete reads, and can assume seekability.
|
||||
if f.read(3) != '\xEF\xBB\xBF':
|
||||
with open(tahoe_cfg, "r") as f:
|
||||
# On Python 2, where we read in bytes, skip any initial Byte Order
|
||||
# Mark. Since this is an ordinary file, we don't need to handle
|
||||
# incomplete reads, and can assume seekability.
|
||||
if PY2 and f.read(3) != b'\xEF\xBB\xBF':
|
||||
f.seek(0)
|
||||
config.readfp(f)
|
||||
return config
|
||||
@ -28,7 +57,7 @@ def set_config(config, section, option, value):
|
||||
assert config.get(section, option) == value
|
||||
|
||||
def write_config(tahoe_cfg, config):
|
||||
with open(tahoe_cfg, "wb") as f:
|
||||
with open(tahoe_cfg, "w") as f:
|
||||
config.write(f)
|
||||
|
||||
def validate_config(fname, cfg, valid_config):
|
||||
|
@ -1,3 +1,18 @@
|
||||
"""
|
||||
Parse connection status from Foolscap.
|
||||
|
||||
Ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
if PY2:
|
||||
from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401
|
||||
|
||||
import time
|
||||
from zope.interface import implementer
|
||||
from ..interfaces import IConnectionStatus
|
||||
@ -37,9 +52,12 @@ def _hint_statuses(which, handlers, statuses):
|
||||
|
||||
def from_foolscap_reconnector(rc, last_received):
|
||||
ri = rc.getReconnectionInfo()
|
||||
# See foolscap/reconnector.py, ReconnectionInfo, for details about
|
||||
# possible states.
|
||||
# See foolscap/reconnector.py, ReconnectionInfo, for details about possible
|
||||
# states. The returned result is a native string, it seems, so convert to
|
||||
# unicode.
|
||||
state = ri.state
|
||||
if isinstance(state, bytes): # Python 2
|
||||
state = str(state, "ascii")
|
||||
if state == "unstarted":
|
||||
return ConnectionStatus.unstarted()
|
||||
|
||||
|
@ -15,11 +15,15 @@ from allmydata.interfaces import ExistingChildError, NoSuchChildError, \
|
||||
EmptyPathnameComponentError, MustBeDeepImmutableError, \
|
||||
MustBeReadonlyError, MustNotBeUnknownRWError, SDMF_VERSION, MDMF_VERSION
|
||||
from allmydata.mutable.common import UnrecoverableFileError
|
||||
from allmydata.util import abbreviate
|
||||
from allmydata.util.hashutil import timing_safe_compare
|
||||
from allmydata.util.time_format import format_time, format_delta
|
||||
from allmydata.util.encodingutil import to_bytes, quote_output
|
||||
|
||||
# Originally part of this module, so still part of its API:
|
||||
from .common_py3 import ( # noqa: F401
|
||||
get_arg, abbreviate_time, MultiFormatResource, WebError
|
||||
)
|
||||
|
||||
|
||||
def get_filenode_metadata(filenode):
|
||||
metadata = {'mutable': filenode.is_mutable()}
|
||||
@ -104,24 +108,6 @@ def get_root(ctx_or_req):
|
||||
link = "/".join([".."] * depth)
|
||||
return link
|
||||
|
||||
def get_arg(ctx_or_req, argname, default=None, multiple=False):
|
||||
"""Extract an argument from either the query args (req.args) or the form
|
||||
body fields (req.fields). If multiple=False, this returns a single value
|
||||
(or the default, which defaults to None), and the query args take
|
||||
precedence. If multiple=True, this returns a tuple of arguments (possibly
|
||||
empty), starting with all those in the query args.
|
||||
"""
|
||||
req = IRequest(ctx_or_req)
|
||||
results = []
|
||||
if argname in req.args:
|
||||
results.extend(req.args[argname])
|
||||
if req.fields and argname in req.fields:
|
||||
results.append(req.fields[argname].value)
|
||||
if multiple:
|
||||
return tuple(results)
|
||||
if results:
|
||||
return results[0]
|
||||
return default
|
||||
|
||||
def convert_children_json(nodemaker, children_json):
|
||||
"""I convert the JSON output of GET?t=json into the dict-of-nodes input
|
||||
@ -141,20 +127,6 @@ def convert_children_json(nodemaker, children_json):
|
||||
children[namex] = (childnode, metadata)
|
||||
return children
|
||||
|
||||
def abbreviate_time(data):
|
||||
# 1.23s, 790ms, 132us
|
||||
if data is None:
|
||||
return ""
|
||||
s = float(data)
|
||||
if s >= 10:
|
||||
return abbreviate.abbreviate_time(data)
|
||||
if s >= 1.0:
|
||||
return "%.2fs" % s
|
||||
if s >= 0.01:
|
||||
return "%.0fms" % (1000*s)
|
||||
if s >= 0.001:
|
||||
return "%.1fms" % (1000*s)
|
||||
return "%.0fus" % (1000000*s)
|
||||
|
||||
def compute_rate(bytes, seconds):
|
||||
if bytes is None:
|
||||
@ -219,10 +191,6 @@ def render_time(t):
|
||||
def render_time_attr(t):
|
||||
return format_time(time.localtime(t))
|
||||
|
||||
class WebError(Exception):
|
||||
def __init__(self, text, code=http.BAD_REQUEST):
|
||||
self.text = text
|
||||
self.code = code
|
||||
|
||||
# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
|
||||
# Internal Server Error, we either need to do that ICanHandleException trick,
|
||||
@ -421,62 +389,6 @@ class MultiFormatPage(Page):
|
||||
return lambda ctx: renderer(IRequest(ctx))
|
||||
|
||||
|
||||
class MultiFormatResource(resource.Resource, object):
|
||||
"""
|
||||
``MultiFormatResource`` is a ``resource.Resource`` that can be rendered in
|
||||
a number of different formats.
|
||||
|
||||
Rendered format is controlled by a query argument (given by
|
||||
``self.formatArgument``). Different resources may support different
|
||||
formats but ``json`` is a pretty common one. ``html`` is the default
|
||||
format if nothing else is given as the ``formatDefault``.
|
||||
"""
|
||||
formatArgument = "t"
|
||||
formatDefault = None
|
||||
|
||||
def render(self, req):
|
||||
"""
|
||||
Dispatch to a renderer for a particular format, as selected by a query
|
||||
argument.
|
||||
|
||||
A renderer for the format given by the query argument matching
|
||||
``formatArgument`` will be selected and invoked. render_HTML will be
|
||||
used as a default if no format is selected (either by query arguments
|
||||
or by ``formatDefault``).
|
||||
|
||||
:return: The result of the selected renderer.
|
||||
"""
|
||||
t = get_arg(req, self.formatArgument, self.formatDefault)
|
||||
renderer = self._get_renderer(t)
|
||||
return renderer(req)
|
||||
|
||||
def _get_renderer(self, fmt):
|
||||
"""
|
||||
Get the renderer for the indicated format.
|
||||
|
||||
:param str fmt: The format. If a method with a prefix of ``render_``
|
||||
and a suffix of this format (upper-cased) is found, it will be
|
||||
used.
|
||||
|
||||
:return: A callable which takes a twisted.web Request and renders a
|
||||
response.
|
||||
"""
|
||||
renderer = None
|
||||
|
||||
if fmt is not None:
|
||||
try:
|
||||
renderer = getattr(self, "render_{}".format(fmt.upper()))
|
||||
except AttributeError:
|
||||
raise WebError(
|
||||
"Unknown {} value: {!r}".format(self.formatArgument, fmt),
|
||||
)
|
||||
|
||||
if renderer is None:
|
||||
renderer = self.render_HTML
|
||||
|
||||
return renderer
|
||||
|
||||
|
||||
class SlotsSequenceElement(template.Element):
|
||||
"""
|
||||
``SlotsSequenceElement` is a minimal port of nevow's sequence renderer for
|
||||
|
120
src/allmydata/web/common_py3.py
Normal file
120
src/allmydata/web/common_py3.py
Normal file
@ -0,0 +1,120 @@
|
||||
"""
|
||||
Common utilities that are available from Python 3.
|
||||
|
||||
Can eventually be merged back into allmydata.web.common.
|
||||
"""
|
||||
|
||||
from future.utils import PY2
|
||||
|
||||
if PY2:
|
||||
from nevow.inevow import IRequest as INevowRequest
|
||||
else:
|
||||
INevowRequest = None
|
||||
|
||||
from twisted.web import resource, http
|
||||
from twisted.web.iweb import IRequest
|
||||
|
||||
from allmydata.util import abbreviate
|
||||
|
||||
|
||||
class WebError(Exception):
|
||||
def __init__(self, text, code=http.BAD_REQUEST):
|
||||
self.text = text
|
||||
self.code = code
|
||||
|
||||
|
||||
def get_arg(ctx_or_req, argname, default=None, multiple=False):
|
||||
"""Extract an argument from either the query args (req.args) or the form
|
||||
body fields (req.fields). If multiple=False, this returns a single value
|
||||
(or the default, which defaults to None), and the query args take
|
||||
precedence. If multiple=True, this returns a tuple of arguments (possibly
|
||||
empty), starting with all those in the query args.
|
||||
"""
|
||||
results = []
|
||||
if PY2:
|
||||
req = INevowRequest(ctx_or_req)
|
||||
if argname in req.args:
|
||||
results.extend(req.args[argname])
|
||||
if req.fields and argname in req.fields:
|
||||
results.append(req.fields[argname].value)
|
||||
else:
|
||||
req = IRequest(ctx_or_req)
|
||||
if argname in req.args:
|
||||
results.extend(req.args[argname])
|
||||
if multiple:
|
||||
return tuple(results)
|
||||
if results:
|
||||
return results[0]
|
||||
return default
|
||||
|
||||
|
||||
class MultiFormatResource(resource.Resource, object):
|
||||
"""
|
||||
``MultiFormatResource`` is a ``resource.Resource`` that can be rendered in
|
||||
a number of different formats.
|
||||
|
||||
Rendered format is controlled by a query argument (given by
|
||||
``self.formatArgument``). Different resources may support different
|
||||
formats but ``json`` is a pretty common one. ``html`` is the default
|
||||
format if nothing else is given as the ``formatDefault``.
|
||||
"""
|
||||
formatArgument = "t"
|
||||
formatDefault = None
|
||||
|
||||
def render(self, req):
|
||||
"""
|
||||
Dispatch to a renderer for a particular format, as selected by a query
|
||||
argument.
|
||||
|
||||
A renderer for the format given by the query argument matching
|
||||
``formatArgument`` will be selected and invoked. render_HTML will be
|
||||
used as a default if no format is selected (either by query arguments
|
||||
or by ``formatDefault``).
|
||||
|
||||
:return: The result of the selected renderer.
|
||||
"""
|
||||
t = get_arg(req, self.formatArgument, self.formatDefault)
|
||||
renderer = self._get_renderer(t)
|
||||
return renderer(req)
|
||||
|
||||
def _get_renderer(self, fmt):
|
||||
"""
|
||||
Get the renderer for the indicated format.
|
||||
|
||||
:param str fmt: The format. If a method with a prefix of ``render_``
|
||||
and a suffix of this format (upper-cased) is found, it will be
|
||||
used.
|
||||
|
||||
:return: A callable which takes a twisted.web Request and renders a
|
||||
response.
|
||||
"""
|
||||
renderer = None
|
||||
|
||||
if fmt is not None:
|
||||
try:
|
||||
renderer = getattr(self, "render_{}".format(fmt.upper()))
|
||||
except AttributeError:
|
||||
raise WebError(
|
||||
"Unknown {} value: {!r}".format(self.formatArgument, fmt),
|
||||
)
|
||||
|
||||
if renderer is None:
|
||||
renderer = self.render_HTML
|
||||
|
||||
return renderer
|
||||
|
||||
|
||||
def abbreviate_time(data):
|
||||
# 1.23s, 790ms, 132us
|
||||
if data is None:
|
||||
return ""
|
||||
s = float(data)
|
||||
if s >= 10:
|
||||
return abbreviate.abbreviate_time(data)
|
||||
if s >= 1.0:
|
||||
return "%.2fs" % s
|
||||
if s >= 0.01:
|
||||
return "%.0fms" % (1000*s)
|
||||
if s >= 0.001:
|
||||
return "%.1fms" % (1000*s)
|
||||
return "%.0fus" % (1000000*s)
|
@ -53,7 +53,6 @@ from allmydata.web.common import (
|
||||
get_mutable_type,
|
||||
get_filenode_metadata,
|
||||
render_time,
|
||||
MultiFormatPage,
|
||||
MultiFormatResource,
|
||||
SlotsSequenceElement,
|
||||
)
|
||||
@ -1213,7 +1212,7 @@ class ManifestElement(ReloadableMonitorElement):
|
||||
|
||||
class ManifestResults(MultiFormatResource, ReloadMixin):
|
||||
|
||||
# Control MultiFormatPage
|
||||
# Control MultiFormatResource
|
||||
formatArgument = "output"
|
||||
formatDefault = "html"
|
||||
|
||||
@ -1268,8 +1267,9 @@ class ManifestResults(MultiFormatResource, ReloadMixin):
|
||||
return json.dumps(status, indent=1)
|
||||
|
||||
|
||||
class DeepSizeResults(MultiFormatPage):
|
||||
# Control MultiFormatPage
|
||||
class DeepSizeResults(MultiFormatResource):
|
||||
|
||||
# Control MultiFormatResource
|
||||
formatArgument = "output"
|
||||
formatDefault = "html"
|
||||
|
||||
|
@ -8,7 +8,7 @@ from twisted.web.template import (
|
||||
renderer,
|
||||
renderElement
|
||||
)
|
||||
from allmydata.web.common import (
|
||||
from allmydata.web.common_py3 import (
|
||||
abbreviate_time,
|
||||
MultiFormatResource
|
||||
)
|
||||
|
68
tox.ini
68
tox.ini
@ -44,36 +44,42 @@ usedevelop = False
|
||||
# We use extras=test to get things like "mock" that are required for our unit
|
||||
# tests.
|
||||
extras = test
|
||||
commands =
|
||||
trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors} {posargs:allmydata}
|
||||
tahoe --version
|
||||
|
||||
[testenv:py36]
|
||||
setenv =
|
||||
# Define TEST_SUITE in the environment as an aid to constructing the
|
||||
# correct test command below.
|
||||
!py36: TEST_SUITE = allmydata
|
||||
py36: TEST_SUITE = allmydata.test.python3_tests
|
||||
|
||||
commands =
|
||||
trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors} {posargs:allmydata.test.python3_tests}
|
||||
# As an aid to debugging, dump all of the Python packages and their
|
||||
# versions that are installed in the test environment. This is
|
||||
# particularly useful to get from CI runs - though hopefully the
|
||||
# version pinning we do limits the variability of this output
|
||||
pip freeze
|
||||
|
||||
# The tahoe script isn't sufficiently ported for this to succeed on
|
||||
# Python 3.x yet.
|
||||
!py36: tahoe --version
|
||||
|
||||
!coverage: trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors} {posargs:{env:TEST_SUITE}}
|
||||
|
||||
# measuring coverage is somewhat slower than not measuring coverage
|
||||
# so only do it on request.
|
||||
coverage: coverage run -m twisted.trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors --reporter=timing} {posargs:{env:TEST_SUITE}}
|
||||
coverage: coverage combine
|
||||
coverage: coverage xml
|
||||
|
||||
|
||||
[testenv:integration]
|
||||
setenv =
|
||||
COVERAGE_PROCESS_START=.coveragerc
|
||||
commands =
|
||||
# NOTE: 'run with "py.test --keep-tempdir -s -v integration/" to debug failures'
|
||||
py.test --coverage -v {posargs:integration}
|
||||
# NOTE: 'run with "py.test --keep-tempdir -s -v integration/" to debug failures'
|
||||
py.test --coverage -v {posargs:integration}
|
||||
coverage combine
|
||||
coverage report
|
||||
|
||||
[testenv:coverage]
|
||||
# coverage (with --branch) takes about 65% longer to run
|
||||
commands =
|
||||
# As an aid to debugging, dump all of the Python packages and their
|
||||
# versions that are installed in the test environment. This is
|
||||
# particularly useful to get from CI runs - though hopefully the
|
||||
# version pinning we do limits the variability of this output
|
||||
# somewhat.
|
||||
pip freeze
|
||||
tahoe --version
|
||||
coverage run --branch -m twisted.trial {env:TAHOE_LAFS_TRIAL_ARGS:--rterrors --reporter=timing} {posargs:allmydata}
|
||||
coverage combine
|
||||
coverage xml
|
||||
|
||||
[testenv:codechecks]
|
||||
# On macOS, git inside of towncrier needs $HOME.
|
||||
@ -87,11 +93,11 @@ commands =
|
||||
python misc/coding_tools/find-trailing-spaces.py -r src static misc setup.py
|
||||
python misc/coding_tools/check-miscaptures.py
|
||||
|
||||
# If towncrier.check fails, you forgot to add a towncrier news
|
||||
# fragment explaining the change in this branch. Create one at
|
||||
# `newsfragments/<ticket>.<change type>` with some text for the news
|
||||
# file. See pyproject.toml for legal <change type> values.
|
||||
python -m towncrier.check --pyproject towncrier.pyproject.toml
|
||||
# If towncrier.check fails, you forgot to add a towncrier news
|
||||
# fragment explaining the change in this branch. Create one at
|
||||
# `newsfragments/<ticket>.<change type>` with some text for the news
|
||||
# file. See pyproject.toml for legal <change type> values.
|
||||
python -m towncrier.check --pyproject towncrier.pyproject.toml
|
||||
|
||||
[testenv:draftnews]
|
||||
passenv = TAHOE_LAFS_* PIP_* SUBUNITREPORTER_* USERPROFILE HOMEDRIVE HOMEPATH
|
||||
@ -110,9 +116,9 @@ commands =
|
||||
#
|
||||
# Some discussion is available at
|
||||
# https://github.com/pypa/pip/issues/5696
|
||||
#
|
||||
# towncrier post 19.2 (unreleased as of this writing) adds a --config
|
||||
# option that can be used instead of this file shuffling.
|
||||
#
|
||||
# towncrier post 19.2 (unreleased as of this writing) adds a --config
|
||||
# option that can be used instead of this file shuffling.
|
||||
mv towncrier.pyproject.toml pyproject.toml
|
||||
|
||||
# towncrier 19.2 + works with python2.7
|
||||
@ -138,9 +144,9 @@ commands =
|
||||
#
|
||||
# Some discussion is available at
|
||||
# https://github.com/pypa/pip/issues/5696
|
||||
#
|
||||
# towncrier post 19.2 (unreleased as of this writing) adds a --config
|
||||
# option that can be used instead of this file shuffling.
|
||||
#
|
||||
# towncrier post 19.2 (unreleased as of this writing) adds a --config
|
||||
# option that can be used instead of this file shuffling.
|
||||
mv towncrier.pyproject.toml pyproject.toml
|
||||
|
||||
# towncrier 19.2 + works with python2.7
|
||||
|
Loading…
Reference in New Issue
Block a user