Skip to content

Commit

Permalink
Merge 198de5c into 7693fed
Browse files Browse the repository at this point in the history
  • Loading branch information
exarkun committed Sep 22, 2021
2 parents 7693fed + 198de5c commit 27f78d5
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 88 deletions.
Empty file added newsfragments/3525.minor
Empty file.
99 changes: 69 additions & 30 deletions src/allmydata/scripts/tahoe_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,48 @@
from future.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 os
from urllib.parse import urlencode, quote as url_quote
from sys import stdout as _sys_stdout
from urllib.parse import urlencode

import json

from .common import BaseOptions
from allmydata.scripts.common import get_default_nodedir
from allmydata.scripts.common_http import do_http, BadResponse
from allmydata.scripts.common_http import BadResponse
from allmydata.util.abbreviate import abbreviate_space, abbreviate_time
from allmydata.util.encodingutil import argv_to_abspath

_print = print
def print(*args, **kwargs):
"""
Builtin ``print``-alike that will even write unicode which cannot be
encoded using the specified output file's encoding.
def _get_json_for_fragment(options, fragment, method='GET', post_args=None):
This differs from the builtin print in that it will use the "replace"
encoding error handler and then write the result whereas builtin print
uses the "strict" encoding error handler.
"""
returns the JSON for a particular URI-fragment (to which is
pre-pended the node's URL)
from past.builtins import unicode
out = kwargs.pop("file", None)
if out is None:
out = _sys_stdout
encoding = out.encoding or "ascii"
def ensafe(o):
if isinstance(o, unicode):
return o.encode(encoding, errors="replace").decode(encoding)
return o
return _print(
*(ensafe(a) for a in args),
file=out,
**kwargs
)

def _get_request_parameters_for_fragment(options, fragment, method, post_args):
"""
Get parameters for ``do_http`` for requesting the given fragment.
:return dict: A dictionary suitable for use as keyword arguments to
``do_http``.
"""
nodeurl = options['node-url']
if nodeurl.endswith('/'):
Expand All @@ -40,7 +67,17 @@ def _get_json_for_fragment(options, fragment, method='GET', post_args=None):
body = ''
if post_args is not None:
raise ValueError("post_args= only valid for POST method")
resp = do_http(method, url, body=body.encode("utf-8"))
return dict(
method=method,
url=url,
body=body.encode("utf-8"),
)


def _handle_response_for_fragment(resp, nodeurl):
"""
Inspect an HTTP response and return the parsed payload, if possible.
"""
if isinstance(resp, BadResponse):
# specifically NOT using format_http_error() here because the
# URL is pretty sensitive (we're doing /uri/<key>).
Expand All @@ -55,12 +92,6 @@ def _get_json_for_fragment(options, fragment, method='GET', post_args=None):
return parsed


def _get_json_for_cap(options, cap):
return _get_json_for_fragment(
options,
'uri/%s?t=json' % url_quote(cap),
)

def pretty_progress(percent, size=10, output_ascii=False):
"""
Displays a unicode or ascii based progress bar of a certain
Expand Down Expand Up @@ -251,7 +282,10 @@ def render_recent(verbose, stdout, status_data):
print(u" Skipped {} non-upload/download operations; use --verbose to see".format(skipped), file=stdout)


def do_status(options):
def do_status(options, do_http=None):
if do_http is None:
from allmydata.scripts.common_http import do_http

nodedir = options["node-directory"]
with open(os.path.join(nodedir, u'private', u'api_auth_token'), 'r') as f:
token = f.read().strip()
Expand All @@ -260,25 +294,30 @@ def do_status(options):

# do *all* our data-retrievals first in case there's an error
try:
status_data = _get_json_for_fragment(
options,
'status?t=json',
method='POST',
post_args=dict(
t='json',
token=token,
)
status_data = _handle_response_for_fragment(
do_http(**_get_request_parameters_for_fragment(
options,
'status?t=json',
method='POST',
post_args=dict(
t='json',
token=token,
),
)),
options['node-url'],
)
statistics_data = _get_json_for_fragment(
options,
'statistics?t=json',
method='POST',
post_args=dict(
t='json',
token=token,
)
statistics_data = _handle_response_for_fragment(
do_http(**_get_request_parameters_for_fragment(
options,
'statistics?t=json',
method='POST',
post_args=dict(
t='json',
token=token,
),
)),
options['node-url'],
)

except Exception as e:
print(u"failed to retrieve data: %s" % str(e), file=options.stderr)
return 2
Expand Down
101 changes: 43 additions & 58 deletions src/allmydata/test/cli/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from six import ensure_text

import os
import mock
import tempfile
from io import BytesIO, StringIO
from os.path import join
Expand All @@ -22,8 +21,8 @@

from allmydata.mutable.publish import MutableData
from allmydata.scripts.common_http import BadResponse
from allmydata.scripts.tahoe_status import _get_json_for_fragment
from allmydata.scripts.tahoe_status import _get_json_for_cap
from allmydata.scripts.tahoe_status import _handle_response_for_fragment
from allmydata.scripts.tahoe_status import _get_request_parameters_for_fragment
from allmydata.scripts.tahoe_status import pretty_progress
from allmydata.scripts.tahoe_status import do_status
from allmydata.web.status import marshal_json
Expand Down Expand Up @@ -140,17 +139,12 @@ class CommandStatus(unittest.TestCase):
"""
These tests just exercise the renderers and ensure they don't
catastrophically fail.
They could be enhanced to look for "some" magic strings in the
results and assert they're in the output.
"""

def setUp(self):
self.options = _FakeOptions()

@mock.patch('allmydata.scripts.tahoe_status.do_http')
@mock.patch('sys.stdout', StringIO())
def test_no_operations(self, http):
def test_no_operations(self):
values = [
StringIO(ensure_text(json.dumps({
"active": [],
Expand All @@ -165,12 +159,11 @@ def test_no_operations(self, http):
}
}))),
]
http.side_effect = lambda *args, **kw: values.pop(0)
do_status(self.options)
def do_http(*args, **kw):
return values.pop(0)
do_status(self.options, do_http)

@mock.patch('allmydata.scripts.tahoe_status.do_http')
@mock.patch('sys.stdout', StringIO())
def test_simple(self, http):
def test_simple(self):
recent_items = active_items = [
UploadStatus(),
DownloadStatus(b"abcd", 12345),
Expand Down Expand Up @@ -201,80 +194,72 @@ def test_simple(self, http):
}
}).encode("utf-8")),
]
http.side_effect = lambda *args, **kw: values.pop(0)
do_status(self.options)
def do_http(*args, **kw):
return values.pop(0)
do_status(self.options, do_http)

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_fetch_error(self, http):

def boom(*args, **kw):
def test_fetch_error(self):
def do_http(*args, **kw):
raise RuntimeError("boom")
http.side_effect = boom
do_status(self.options)
do_status(self.options, do_http)


class JsonHelpers(unittest.TestCase):

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_bad_response(self, http):
http.return_value = BadResponse('the url', 'some err')
def test_bad_response(self):
def do_http(*args, **kw):
return
with self.assertRaises(RuntimeError) as ctx:
_get_json_for_fragment({'node-url': 'http://localhost:1234'}, '/fragment')
self.assertTrue(
"Failed to get" in str(ctx.exception)
_handle_response_for_fragment(
BadResponse('the url', 'some err'),
'http://localhost:1234',
)
self.assertIn(
"Failed to get",
str(ctx.exception),
)

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_happy_path(self, http):
http.return_value = StringIO('{"some": "json"}')
resp = _get_json_for_fragment({'node-url': 'http://localhost:1234/'}, '/fragment/')
self.assertEqual(resp, dict(some='json'))

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_happy_path_post(self, http):
http.return_value = StringIO('{"some": "json"}')
resp = _get_json_for_fragment(
{'node-url': 'http://localhost:1234/'},
'/fragment/',
method='POST',
post_args={'foo': 'bar'}
def test_happy_path(self):
resp = _handle_response_for_fragment(
StringIO('{"some": "json"}'),
'http://localhost:1234/',
)
self.assertEqual(resp, dict(some='json'))

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_happy_path_for_cap(self, http):
http.return_value = StringIO('{"some": "json"}')
resp = _get_json_for_cap({'node-url': 'http://localhost:1234'}, 'fake cap')
def test_happy_path_post(self):
resp = _handle_response_for_fragment(
StringIO('{"some": "json"}'),
'http://localhost:1234/',
)
self.assertEqual(resp, dict(some='json'))

@mock.patch('allmydata.scripts.tahoe_status.do_http')
def test_no_data_returned(self, http):
http.return_value = StringIO('null')

def test_no_data_returned(self):
with self.assertRaises(RuntimeError) as ctx:
_get_json_for_cap({'node-url': 'http://localhost:1234'}, 'fake cap')
self.assertTrue('No data from' in str(ctx.exception))
_handle_response_for_fragment(StringIO('null'), 'http://localhost:1234')
self.assertIn('No data from', str(ctx.exception))

def test_no_post_args(self):
with self.assertRaises(ValueError) as ctx:
_get_json_for_fragment(
_get_request_parameters_for_fragment(
{'node-url': 'http://localhost:1234'},
'/fragment',
method='POST',
post_args=None,
)
self.assertTrue(
"Must pass post_args" in str(ctx.exception)
self.assertIn(
"Must pass post_args",
str(ctx.exception),
)

def test_post_args_for_get(self):
with self.assertRaises(ValueError) as ctx:
_get_json_for_fragment(
_get_request_parameters_for_fragment(
{'node-url': 'http://localhost:1234'},
'/fragment',
method='GET',
post_args={'foo': 'bar'}
)
self.assertTrue(
"only valid for POST" in str(ctx.exception)
self.assertIn(
"only valid for POST",
str(ctx.exception),
)
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ commands =

tahoe --version

python -c "import sys; print('sys.stdout.encoding:', sys.stdout.encoding)"

# Run tests with -b to catch bugs like `"%s" % (some_bytes,)`. -b makes
# Python emit BytesWarnings, and warnings configuration in
# src/allmydata/tests/__init__.py turns allmydata's BytesWarnings into
Expand Down

0 comments on commit 27f78d5

Please sign in to comment.