Add request/response to subunit-describe-calls
Adds new functionality to subunit-describe-calls. Request & Response
headers + body information now included in JSON output. Makes
-o/--output-file parameter optional, if not specified a shortened output
will be created. Changes the -s/--subunit parameter to not allow for data
to be passed in via stdin.
Change-Id: I44c6b7f9adef7e5be2039c7201f17485f2a46077
diff --git a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
index b457ddd..092014e 100644
--- a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
+++ b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
@@ -1,4 +1,8 @@
---
features:
- - Adds subunit-describe-calls. A parser for subunit streams to determine what
+ - |
+ Adds subunit-describe-calls. A parser for subunit streams to determine what
REST API calls are made inside of a test and in what order they are called.
+
+ * Input can be piped in or a file can be specified
+ * Output is shortened for stdout, the output file has more information
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index c990add..535fe71 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -21,13 +21,14 @@
Runtime Arguments
-----------------
-**--subunit, -s**: (Required) The path to the subunit file being parsed
+**--subunit, -s**: (Optional) The path to the subunit file being parsed,
+defaults to stdin
**--non-subunit-name, -n**: (Optional) The file_name that the logs are being
stored in
-**--output-file, -o**: (Required) The path where the JSON output will be
-written to
+**--output-file, -o**: (Optional) The path where the JSON output will be
+written to. This contains more information than is present in stdout.
**--ports, -p**: (Optional) The path to a JSON file describing the ports being
used by different services
@@ -35,13 +36,14 @@
Usage
-----
-subunit-describe-calls will take in a file path via the --subunit parameter
-which contains either a subunit v1 or v2 stream. This is then parsed checking
-for details contained in the file_bytes of the --non-subunit-name parameter
-(the default is pythonlogging which is what Tempest uses to store logs). By
-default the OpenStack Kilo release port defaults (http://bit.ly/22jpF5P)
-are used unless a file is provided via the --ports option. The resulting output
-is dumped in JSON output to the path provided in the --output-file option.
+subunit-describe-calls will take in either stdin subunit v1 or v2 stream or a
+file path which contains either a subunit v1 or v2 stream passed via the
+--subunit parameter. This is then parsed checking for details contained in the
+file_bytes of the --non-subunit-name parameter (the default is pythonlogging
+which is what Tempest uses to store logs). By default the OpenStack Kilo
+release port defaults (http://bit.ly/22jpF5P) are used unless a file is
+provided via the --ports option. The resulting output is dumped in JSON output
+to the path provided in the --output-file option.
Ports file JSON structure
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -61,7 +63,11 @@
"verb": "HTTP Verb",
"service": "Name of the service",
"url": "A shortened version of the URL called",
- "status_code": "The status code of the response"
+ "status_code": "The status code of the response",
+ "request_headers": "The headers of the request",
+ "request_body": "The body of the request",
+ "response_headers": "The headers of the response",
+ "response_body": "The body of the response"
}
]
}
@@ -72,6 +78,7 @@
import json
import os
import re
+import sys
import subunit
import testtools
@@ -88,6 +95,9 @@
'(?P<verb>\w*) (?P<url>.*) .*')
port_re = re.compile(r'.*:(?P<port>\d+).*')
path_re = re.compile(r'http[s]?://[^/]*/(?P<path>.*)')
+ request_re = re.compile(r'.* Request - Headers: (?P<headers>.*)')
+ response_re = re.compile(r'.* Response - Headers: (?P<headers>.*)')
+ body_re = re.compile(r'.*Body: (?P<body>.*)')
# Based on mitaka defaults:
# http://docs.openstack.org/mitaka/config-reference/
@@ -148,15 +158,46 @@
calls = []
for _, detail in details.items():
+ in_request = False
+ in_response = False
+ current_call = {}
for line in detail.as_text().split("\n"):
- match = self.url_re.match(line)
- if match is not None:
- calls.append({
- "name": match.group("name"),
- "verb": match.group("verb"),
- "status_code": match.group("code"),
- "service": self.get_service(match.group("url")),
- "url": self.url_path(match.group("url"))})
+ url_match = self.url_re.match(line)
+ request_match = self.request_re.match(line)
+ response_match = self.response_re.match(line)
+ body_match = self.body_re.match(line)
+
+ if url_match is not None:
+ if current_call != {}:
+ calls.append(current_call.copy())
+ current_call = {}
+ in_request, in_response = False, False
+ current_call.update({
+ "name": url_match.group("name"),
+ "verb": url_match.group("verb"),
+ "status_code": url_match.group("code"),
+ "service": self.get_service(url_match.group("url")),
+ "url": self.url_path(url_match.group("url"))})
+ elif request_match is not None:
+ in_request, in_response = True, False
+ current_call.update(
+ {"request_headers": request_match.group("headers")})
+ elif in_request and body_match is not None:
+ in_request = False
+ current_call.update(
+ {"request_body": body_match.group(
+ "body")})
+ elif response_match is not None:
+ in_request, in_response = False, True
+ current_call.update(
+ {"response_headers": response_match.group(
+ "headers")})
+ elif in_response and body_match is not None:
+ in_response = False
+ current_call.update(
+ {"response_body": body_match.group("body")})
+ if current_call != {}:
+ calls.append(current_call.copy())
return calls
@@ -203,8 +244,9 @@
self.prog = "Argument Parser"
self.add_argument(
- "-s", "--subunit", metavar="<subunit file>", required=True,
- default=None, help="The path to the subunit output file.")
+ "-s", "--subunit", metavar="<subunit file>",
+ nargs="?", type=argparse.FileType('rb'), default=sys.stdin,
+ help="The path to the subunit output file.")
self.add_argument(
"-n", "--non-subunit-name", metavar="<non subunit name>",
@@ -213,19 +255,18 @@
self.add_argument(
"-o", "--output-file", metavar="<output file>", default=None,
- help="The output file name for the json.", required=True)
+ help="The output file name for the json.")
self.add_argument(
"-p", "--ports", metavar="<ports file>", default=None,
help="A JSON file describing the ports for each service.")
-def parse(subunit_file, non_subunit_name, ports):
+def parse(stream, non_subunit_name, ports):
if ports is not None and os.path.exists(ports):
ports = json.loads(open(ports).read())
url_parser = UrlParser(ports)
- stream = open(subunit_file, 'rb')
suite = subunit.ByteStreamToStreamResult(
stream, non_subunit_name=non_subunit_name)
result = testtools.StreamToExtendedDecorator(url_parser)
@@ -245,8 +286,21 @@
def output(url_parser, output_file):
- with open(output_file, "w") as outfile:
- outfile.write(json.dumps(url_parser.test_logs))
+ if output_file is not None:
+ with open(output_file, "w") as outfile:
+ outfile.write(json.dumps(url_parser.test_logs))
+ return
+
+ for test_name, items in url_parser.test_logs.iteritems():
+ sys.stdout.write('{0}\n'.format(test_name))
+ if not items:
+ sys.stdout.write('\n')
+ continue
+ for item in items:
+ sys.stdout.write('\t- {0} {1} request for {2} to {3}\n'.format(
+ item.get('status_code'), item.get('verb'),
+ item.get('service'), item.get('url')))
+ sys.stdout.write('\n')
def entry_point():
diff --git a/tempest/tests/cmd/test_subunit_describe_calls.py b/tempest/tests/cmd/test_subunit_describe_calls.py
index 43b417a..1c24c37 100644
--- a/tempest/tests/cmd/test_subunit_describe_calls.py
+++ b/tempest/tests/cmd/test_subunit_describe_calls.py
@@ -38,46 +38,159 @@
os.path.dirname(os.path.abspath(__file__)),
'sample_streams/calls.subunit')
parser = subunit_describe_calls.parse(
- subunit_file, "pythonlogging", None)
+ open(subunit_file), "pythonlogging", None)
expected_result = {
- 'bar': [{'name': 'AgentsAdminTestJSON:setUp',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:test_create_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:tearDown',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/1',
- 'verb': 'DELETE'},
- {'name': 'AgentsAdminTestJSON:_run_cleanups',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/2',
- 'verb': 'DELETE'}],
- 'foo': [{'name': 'AgentsAdminTestJSON:setUp',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:test_delete_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/3',
- 'verb': 'DELETE'},
- {'name': 'AgentsAdminTestJSON:test_delete_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'GET'},
- {'name': 'AgentsAdminTestJSON:tearDown',
- 'service': 'Nova',
- 'status_code': '404',
- 'url': 'v2.1/<id>/os-agents/3',
- 'verb': 'DELETE'}]}
+ 'bar': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux", '
+ '"agent_id": 1}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-25ddaae2-0ef1-40d1-8228-59bd64a7e75b', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_create_agent',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win", '
+ '"agent_id": 2}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'195', 'x-compute-request-id': "
+ "'req-b4136f06-c015-4e7e-995f-c43831e3ecce', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-ee905fd6-a5b5-4da4-8c37-5363cb25bd9d', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/1',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:_run_cleanups',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-e912cac0-63e0-4679-a68a-b6d18ddca074', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/2',
+ 'verb': 'DELETE'}],
+ 'foo': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux", '
+ '"agent_id": 3}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-ccd2116d-04b1-4ffe-ae32-fb623f68bf1c', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-6e7fa28f-ae61-4388-9a78-947c58bc0588', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agents": []}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'14', 'content-location': "
+ "'http://23.253.76.97:8774/v2.1/"
+ "cf6b1933fe5b476fbbabb876f6d1b924/os-agents', "
+ "'x-compute-request-id': "
+ "'req-e41aa9b4-41a6-4138-ae04-220b768eb644', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'GET'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '404', 'content-length': "
+ "'82', 'x-compute-request-id': "
+ "'req-e297aeea-91cf-4f26-b49c-8f46b1b7a926', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:02 GMT', 'content-type': "
+ "'application/json; charset=UTF-8'}",
+ 'service': 'Nova',
+ 'status_code': '404',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}]}
+
self.assertEqual(expected_result, parser.test_logs)