httpie-cli/tests/utils.py

242 lines
6.2 KiB
Python
Raw Normal View History

2014-04-26 19:47:14 +02:00
# coding=utf-8
"""Utilities used by HTTPie tests.
"""
2014-04-24 14:07:31 +02:00
import os
import sys
import time
import json
import shutil
import tempfile
import httpie
from httpie.context import Environment
2014-04-24 14:07:31 +02:00
from httpie.core import main
from httpie.compat import bytes, str
2014-04-24 14:07:31 +02:00
2014-04-24 17:08:40 +02:00
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
2014-04-24 14:07:31 +02:00
CRLF = '\r\n'
2014-04-24 15:48:01 +02:00
COLOR = '\x1b['
HTTP_OK = '200 OK'
2014-04-24 15:48:01 +02:00
HTTP_OK_COLOR = (
2014-04-24 14:07:31 +02:00
'HTTP\x1b[39m\x1b[38;5;245m/\x1b[39m\x1b'
'[38;5;37m1.1\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;37m200'
'\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;136mOK'
)
def no_content_type(headers):
return (
'Content-Type' not in headers
# We need to do also this because of this issue:
# <https://github.com/kevin1024/pytest-httpbin/issues/5>
# TODO: remove this function once the issue is if fixed
or headers['Content-Type'] == 'text/plain'
)
2014-04-24 15:48:01 +02:00
2014-04-25 13:10:01 +02:00
def add_auth(url, auth):
proto, rest = url.split('://', 1)
return proto + '://' + auth + '@' + rest
2014-04-24 14:07:31 +02:00
class TestEnvironment(Environment):
2014-04-24 15:48:01 +02:00
"""
Environment subclass with reasonable defaults suitable for testing.
"""
2014-04-24 14:07:31 +02:00
colors = 0
stdin_isatty = True,
stdout_isatty = True
is_windows = False
2014-04-24 15:48:01 +02:00
_shutil_rmtree = shutil.rmtree # needed by __del__ (would get gc'd)
2014-04-24 14:07:31 +02:00
def __init__(self, **kwargs):
if 'stdout' not in kwargs:
kwargs['stdout'] = tempfile.TemporaryFile('w+b')
if 'stderr' not in kwargs:
kwargs['stderr'] = tempfile.TemporaryFile('w+t')
self.delete_config_dir = False
if 'config_dir' not in kwargs:
kwargs['config_dir'] = mk_config_dir()
self.delete_config_dir = True
super(TestEnvironment, self).__init__(**kwargs)
def __del__(self):
if self.delete_config_dir:
self._shutil_rmtree(self.config_dir)
2014-04-24 14:07:31 +02:00
def http(*args, **kwargs):
"""
2014-04-25 12:42:50 +02:00
Run HTTPie and capture stderr/out and exit status.
2014-04-25 12:18:35 +02:00
2014-04-24 14:07:31 +02:00
Invoke `httpie.core.main()` with `args` and `kwargs`,
2014-04-25 12:18:35 +02:00
and return a `CLIResponse` subclass instance.
2014-04-25 12:18:35 +02:00
The return value is either a `StrCLIResponse`, or `BytesCLIResponse`
if unable to decode the output.
2014-04-24 14:07:31 +02:00
The response has the following attributes:
2014-04-25 12:18:35 +02:00
`stdout` is represented by the instance itself (print r)
2014-04-24 14:07:31 +02:00
`stderr`: text written to stderr
`exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated.
If you pass ``error_exit_ok=True``, then error exit statuses
won't result into an exception.
Example:
2014-04-24 14:07:31 +02:00
2014-04-25 12:18:35 +02:00
$ http --auth=user:password GET httpbin.org/basic-auth/user/password
>>> r = http('-a', 'user:pw', 'httpbin.org/basic-auth/user/pw')
>>> type(r) == StrCLIResponse
True
>>> r.exit_status
0
>>> r.stderr
''
>>> 'HTTP/1.1 200 OK' in r
True
2014-04-25 12:18:35 +02:00
>>> r.json == {'authenticated': True, 'user': 'user'}
True
2014-04-25 12:18:35 +02:00
2014-04-24 14:07:31 +02:00
"""
error_exit_ok = kwargs.pop('error_exit_ok', False)
2014-04-24 14:07:31 +02:00
env = kwargs.get('env')
if not env:
env = kwargs['env'] = TestEnvironment()
stdout = env.stdout
stderr = env.stderr
2014-04-25 12:42:50 +02:00
args = list(args)
2014-04-24 18:32:15 +02:00
if '--debug' not in args and '--traceback' not in args:
args = ['--traceback'] + args
2014-04-25 12:42:50 +02:00
def dump_stderr():
stderr.seek(0)
sys.stderr.write(stderr.read())
2014-04-24 18:32:15 +02:00
try:
2014-04-24 14:07:31 +02:00
try:
2014-04-24 18:32:15 +02:00
exit_status = main(args=args, **kwargs)
2014-04-24 14:07:31 +02:00
if '--download' in args:
# Let the progress reporter thread finish.
time.sleep(.5)
except SystemExit:
if error_exit_ok:
exit_status = httpie.ExitStatus.ERROR
else:
dump_stderr()
raise
2014-04-24 14:07:31 +02:00
except Exception:
stderr.seek(0)
2014-04-24 14:07:31 +02:00
sys.stderr.write(stderr.read())
raise
else:
if exit_status != httpie.ExitStatus.OK and not error_exit_ok:
dump_stderr()
raise Exception('Unexpected exit status: %s', exit_status)
2014-04-24 14:07:31 +02:00
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try:
2014-04-25 12:42:50 +02:00
output = output.decode('utf8')
2014-04-24 14:07:31 +02:00
except UnicodeDecodeError:
# noinspection PyArgumentList
r = BytesCLIResponse(output)
2014-04-25 12:42:50 +02:00
else:
# noinspection PyArgumentList
r = StrCLIResponse(output)
2014-04-24 14:07:31 +02:00
r.stderr = stderr.read()
r.exit_status = exit_status
if r.exit_status != httpie.ExitStatus.OK:
sys.stderr.write(r.stderr)
2014-04-24 14:07:31 +02:00
return r
finally:
stdout.close()
stderr.close()
2014-04-24 15:48:01 +02:00
class BaseCLIResponse(object):
"""
Represents the result of simulated `$ http' invocation via `http()`.
Holds and provides access to:
- stdout output: print(self)
- stderr output: print(self.stderr)
- exit_status output: print(self.exit_status)
"""
stderr = None
json = None
exit_status = None
class BytesCLIResponse(bytes, BaseCLIResponse):
"""
Used as a fallback when a StrCLIResponse cannot be used.
2014-04-25 12:42:50 +02:00
E.g. when the output contains binary data or when it is colorized.
`.json` will always be None.
"""
class StrCLIResponse(str, BaseCLIResponse):
@property
def json(self):
2014-04-25 12:42:50 +02:00
"""
Return deserialized JSON body, if one included in the output
and is parseable.
"""
if not hasattr(self, '_json'):
self._json = None
# De-serialize JSON body if possible.
if COLOR in self:
# Colorized output cannot be parsed.
pass
elif self.strip().startswith('{'):
# Looks like JSON body.
self._json = json.loads(self)
elif (self.count('Content-Type:') == 1
and 'application/json' in self):
2014-04-25 12:42:50 +02:00
# Looks like a whole JSON HTTP message,
# try to extract its body.
try:
j = self.strip()[self.strip().rindex('\r\n\r\n'):]
except ValueError:
pass
else:
try:
self._json = json.loads(j)
except ValueError:
pass
return self._json
2014-04-25 12:18:35 +02:00
def mk_config_dir():
return tempfile.mkdtemp(prefix='httpie_test_config_dir_')