diff --git a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml index b457dddab3..092014e610 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 93918231c0..0f868a9336 100644 --- a/tempest/cmd/subunit_describe_calls.py +++ b/tempest/cmd/subunit_describe_calls.py @@ -21,13 +21,14 @@ API calls are made inside of a test and in what order they are called. 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 @@ used by different services 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 ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -64,7 +66,11 @@ Output file JSON structure "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" } ] } @@ -75,6 +81,7 @@ import io import json import os import re +import sys import subunit import testtools @@ -91,6 +98,9 @@ class UrlParser(testtools.TestResult): '(?P\w*) (?P.*) .*') port_re = re.compile(r'.*:(?P\d+).*') path_re = re.compile(r'http[s]?://[^/]*/(?P.*)') + request_re = re.compile(r'.* Request - Headers: (?P.*)') + response_re = re.compile(r'.* Response - Headers: (?P.*)') + body_re = re.compile(r'.*Body: (?P.*)') # Based on mitaka defaults: # http://docs.openstack.org/mitaka/config-reference/ @@ -151,15 +161,46 @@ class UrlParser(testtools.TestResult): 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 @@ -206,8 +247,9 @@ class ArgumentParser(argparse.ArgumentParser): self.prog = "subunit-describe-calls" self.add_argument( - "-s", "--subunit", metavar="", required=True, - default=None, help="The path to the subunit output file.") + "-s", "--subunit", metavar="", + nargs="?", type=argparse.FileType('rb'), default=sys.stdin, + help="The path to the subunit output file.") self.add_argument( "-n", "--non-subunit-name", metavar="", @@ -216,19 +258,18 @@ class ArgumentParser(argparse.ArgumentParser): self.add_argument( "-o", "--output-file", metavar="", 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="", 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) @@ -248,8 +289,21 @@ def parse(subunit_file, non_subunit_name, ports): 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 43b417ab94..1c24c378b2 100644 --- a/tempest/tests/cmd/test_subunit_describe_calls.py +++ b/tempest/tests/cmd/test_subunit_describe_calls.py @@ -38,46 +38,159 @@ class TestSubunitDescribeCalls(base.TestCase): 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//os-agents', - 'verb': 'POST'}, - {'name': 'AgentsAdminTestJSON:test_create_agent', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents', - 'verb': 'POST'}, - {'name': 'AgentsAdminTestJSON:tearDown', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents/1', - 'verb': 'DELETE'}, - {'name': 'AgentsAdminTestJSON:_run_cleanups', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents/2', - 'verb': 'DELETE'}], - 'foo': [{'name': 'AgentsAdminTestJSON:setUp', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents', - 'verb': 'POST'}, - {'name': 'AgentsAdminTestJSON:test_delete_agent', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents/3', - 'verb': 'DELETE'}, - {'name': 'AgentsAdminTestJSON:test_delete_agent', - 'service': 'Nova', - 'status_code': '200', - 'url': 'v2.1//os-agents', - 'verb': 'GET'}, - {'name': 'AgentsAdminTestJSON:tearDown', - 'service': 'Nova', - 'status_code': '404', - 'url': 'v2.1//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': ''}", + '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//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': ''}", + '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//os-agents', + 'verb': 'POST'}, { + 'name': 'AgentsAdminTestJSON:tearDown', + 'request_body': 'None', + 'request_headers': "{'Content-Type': 'application/json', " + "'Accept': 'application/json', 'X-Auth-Token': ''}", + '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//os-agents/1', + 'verb': 'DELETE'}, { + 'name': 'AgentsAdminTestJSON:_run_cleanups', + 'request_body': 'None', + 'request_headers': "{'Content-Type': 'application/json', " + "'Accept': 'application/json', 'X-Auth-Token': ''}", + '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//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': ''}", + '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//os-agents', + 'verb': 'POST'}, { + 'name': 'AgentsAdminTestJSON:test_delete_agent', + 'request_body': 'None', + 'request_headers': "{'Content-Type': 'application/json', " + "'Accept': 'application/json', 'X-Auth-Token': ''}", + '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//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': ''}", + '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//os-agents', + 'verb': 'GET'}, { + 'name': 'AgentsAdminTestJSON:tearDown', + 'request_body': 'None', + 'request_headers': "{'Content-Type': 'application/json', " + "'Accept': 'application/json', 'X-Auth-Token': ''}", + '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//os-agents/3', + 'verb': 'DELETE'}]} + self.assertEqual(expected_result, parser.test_logs)