Skip to content

Commit 3028acd

Browse files
authored
Extend spec coverage file with concrete requests and responses. (#170)
This change adds an example of a concrete request for which the data in the spec coverage file is reported. The goal of this change is that information in the spec coverage file can be used to troubleshoot failing requests using only: - the spec coverage file - backend logs for the service The diff speccov script has been modified to avoid diffing the concrete data that is now in the coverage file.
1 parent 9e9360c commit 3028acd

File tree

10 files changed

+134
-6
lines changed

10 files changed

+134
-6
lines changed

docs/user-guide/Testing.md

+20
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,24 @@ Each request is represented by a hash of its definition.
7171
"status_code": "400",
7272
"status_text": "BAD REQUEST",
7373
"error_message": "{\n \"errors\": {\n \"id\": \"'5882' is not of type 'integer'\"\n },\n \"message\": \"Input payload validation failed\"\n}\n",
74+
"sample_request": {
75+
"request_sent_timestamp": null,
76+
"response_received_timestamp": "2021-03-31 18:20:14",
77+
"request_uri": "/api/blog/posts/5882",
78+
"request_headers": [
79+
"Accept: application/json",
80+
"Host: localhost:8888",
81+
"Content-Type: application/json"
82+
],
83+
"request_body": "{\n \"id\":\"5882\",\n \"checksum\":\"fuzzstring\",\n \"body\":\"fuzzstring\"}\r\n",
84+
"response_headers": [
85+
"Content-Type: application/json",
86+
"Content-Length: 124",
87+
"Server: Werkzeug/0.16.0 Python/3.7.8",
88+
"Date: Wed, 31 Mar 2021 18:20:14 GMT"
89+
],
90+
"response_body": "{\n \"errors\": {\n \"id\": \"'5882' is not of type 'integer'\"\n },\n \"message\": \"Input payload validation failed\"\n}\n"
91+
},
7492
"request_order": 4
7593
},
7694
```
@@ -92,6 +110,8 @@ the appropriate __"invalid_due_to_..."__ value will be set to 1.
92110
but there was a failure while parsing the response data.
93111
* "500" will be set if a 5xx bug was detected.
94112
* The __"status_code"__ and __"status_text"__ values are the response values received from the server.
113+
* The __"sample_request"__ contains the concrete values of the sent request and received response for which
114+
the coverage data is being reported.
95115
* The __"error_message"__ value will be set to the response body if the request was not "valid".
96116
* The __"request_order"__ value is the 0 indexed order that the request was sent.
97117
* Requests sent during "preprocessing" or "postprocessing" will explicitely say so.

restler/engine/core/driver.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ def render_request(request, seq):
365365
366366
Side effects: request.stats.status_code updated
367367
request.stats.status_text updated
368-
368+
request.stats updated with concrete response and request text
369+
(valid request or last combination)
369370
@return: Tuple containing rendered sequence object, response body, and
370371
failure information enum.
371372
@rtype : Tuple(RenderedSequence, str, FailureInformation)
@@ -387,8 +388,15 @@ def render_request(request, seq):
387388
# will be returned from seq.render if all request combinations are
388389
# exhausted prior to getting a valid status code.
389390
if renderings.final_request_response:
391+
390392
request.stats.status_code = renderings.final_request_response.status_code
391393
request.stats.status_text = renderings.final_request_response.status_text
394+
# Get the last rendered request. The corresponding response should be
395+
# the last received response.
396+
request.stats.sample_request.set_request_stats(
397+
renderings.sequence.sent_request_data_list[-1].rendered_data)
398+
request.stats.sample_request.set_response_stats(renderings.final_request_response,
399+
renderings.final_response_datetime)
392400
response_body = renderings.final_request_response.body
393401

394402
apply_checkers(checkers, renderings, global_lock)

restler/engine/core/fuzzer.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import engine.core.driver as driver
77
import utils.logger as logger
8+
import traceback
89

910
from engine.core.fuzzing_monitor import Monitor
1011
from engine.core.requests import GrammarRequestCollection
@@ -52,7 +53,7 @@ def run(self):
5253
except InvalidDictionaryException:
5354
pass
5455
except Exception as err:
55-
self._exception = str(err)
56+
self._exception = traceback.format_exc()
5657

5758
def join(self, *args):
5859
""" Overrides thread join function

restler/engine/core/postprocessing.py

+10
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ def delete_create_once_resources(destructors, fuzzing_requests):
5454
destructor.stats.valid = 1
5555
destructor.stats.status_code = renderings.final_request_response.status_code
5656
destructor.stats.status_text = renderings.final_request_response.status_text
57+
58+
destructor.stats.sample_request.set_request_stats(
59+
renderings.sequence.sent_request_data_list[-1].rendered_data)
60+
destructor.stats.sample_request.set_response_stats(renderings.final_request_response,
61+
renderings.final_response_datetime)
62+
5763
except Exception as error:
5864
msg = f"Failed to delete create_once resource: {error!s}"
5965
logger.raw_network_logging(msg)
@@ -65,6 +71,10 @@ def delete_create_once_resources(destructors, fuzzing_requests):
6571
destructor.stats.status_code = renderings.final_request_response.status_code
6672
destructor.stats.status_text = renderings.final_request_response.status_text
6773
destructor.stats.error_msg = renderings.final_request_response.body
74+
destructor.stats.sample_request.set_request_stats(
75+
renderings.sequence.sent_request_data_list[-1].rendered_data)
76+
destructor.stats.sample_request.set_response_stats(renderings.final_request_response, renderings.final_response_datetime)
77+
6878
pass
6979

7080
Monitor().current_fuzzing_generation += 1

restler/engine/core/preprocessing.py

+5
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ def exclude_requests(pre_reqs, post_reqs):
211211
resource_gen_req.stats.valid = 1
212212
resource_gen_req.stats.status_code = renderings.final_request_response.status_code
213213
resource_gen_req.stats.status_text = renderings.final_request_response.status_text
214+
resource_gen_req.stats.sample_request.set_request_stats(
215+
renderings.sequence.sent_request_data_list[-1].rendered_data)
216+
resource_gen_req.stats.sample_request.set_response_stats(renderings.final_request_response,
217+
renderings.final_response_datetime)
218+
214219
if req.is_destructor():
215220
# Add destructors to the destructor list that will be returned
216221
destructors.add(req)

restler/engine/core/requests.py

+56
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import engine.dependencies as dependencies
2323
import engine.mime.multipart_formdata as multipart_formdata
2424
from enum import Enum
25+
from engine.transport_layer import messaging
2526

2627
class EmptyRequestException(Exception):
2728
pass
@@ -35,17 +36,72 @@ class FailureInformation(Enum):
3536
PARSER = 3
3637
BUG = 4
3738

39+
class RenderedRequestStats(object):
40+
""" Class used for encapsulating data about a specific rendered request and its response.
41+
This data is included in the spec coverage report.
42+
However, this data includes run-specific information
43+
and should not be used for diffing spec coverage. """
44+
def __init__(self):
45+
self.request_sent_timestamp = None
46+
self.response_received_timestamp = None
47+
48+
self.request_uri = None
49+
self.request_headers = None
50+
self.request_body = None
51+
52+
self.response_headers = None
53+
self.response_body = None
54+
55+
def set_request_stats(self, request_text):
56+
""" Helper to set the request statistics from the text.
57+
Parses the request text and initializes headers, uri, and body
58+
separately.
59+
60+
@return: None
61+
@rtype : None
62+
63+
"""
64+
try:
65+
split_body = request_text.split(messaging.DELIM)
66+
split_headers = split_body[0].split("\r\n")
67+
self.request_uri = split_headers[0].split(" ")[1]
68+
self.request_headers = split_headers[1:]
69+
70+
if len(split_body) > 0 and split_body[1]:
71+
self.request_body = split_body[1]
72+
except:
73+
logger.write_to_main(
74+
f"Error setting request stats for text: {request_text}",
75+
print_to_console=True
76+
)
77+
pass
78+
79+
def set_response_stats(self, final_request_response, final_response_datetime):
80+
""" Helper to set the response headers and body.
81+
82+
@return: None
83+
@rtype : None
84+
85+
"""
86+
self.response_headers = final_request_response.headers
87+
self.response_body = final_request_response.body
88+
self.response_received_timestamp = final_response_datetime
89+
90+
3891
class SmokeTestStats(object):
3992
""" Class used for logging stats during directed-smoke-test """
4093
def __init__(self):
4194
self.request_order = -1
4295
self.matching_prefix = {} # {"id": <prefix_hex>, "valid": <0/1>}
4396
self.valid = 0
4497
self.failure = None
98+
4599
self.error_msg = None
46100
self.status_code = None
47101
self.status_text = None
48102

103+
self.sample_request = RenderedRequestStats()
104+
49105
class Request(object):
50106
""" Request Class. """
51107
def __init__(self, definition=[], requestId=None):

restler/engine/core/sequences.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import copy
99
import time
1010
import json
11+
import datetime
1112

1213
import engine.core.async_request_utilities as async_request_utilities
1314
import engine.core.request_utilities as request_utilities
@@ -42,7 +43,8 @@ def __init__(self, rendered_data, parser, response="", producer_timing_delay=0,
4243

4344
class RenderedSequence(object):
4445
""" RenderedSequence class """
45-
def __init__(self, sequence=None, valid=False, failure_info=None, final_request_response=None):
46+
def __init__(self, sequence=None, valid=False, failure_info=None, final_request_response=None,
47+
response_datetime=None):
4648
""" Initializes RenderedSequence object
4749
4850
@param sequence: The sequence that was rendered
@@ -59,6 +61,7 @@ def __init__(self, sequence=None, valid=False, failure_info=None, final_request_
5961
self.valid = valid
6062
self.failure_info = failure_info
6163
self.final_request_response = final_request_response
64+
self.final_response_datetime = response_datetime
6265

6366
class Sequence(object):
6467
""" Implements basic sequence logic. """
@@ -413,7 +416,9 @@ def render(self, candidate_values_pool, lock, preprocessing=False, postprocessin
413416
and not resource_error\
414417
and response.has_valid_code()
415418
# register latest client/server interaction and add to the status codes list
416-
timestamp_micro = int(time.time()*10**6)
419+
response_datetime = datetime.datetime.now(datetime.timezone.utc)
420+
timestamp_micro = int(response_datetime.timestamp()*10**6)
421+
417422
self.status_codes.append(status_codes_monitor.RequestExecutionStatus(timestamp_micro,
418423
request.hex_definition,
419424
status_code,
@@ -449,9 +454,11 @@ def render(self, candidate_values_pool, lock, preprocessing=False, postprocessin
449454
if lock is not None:
450455
lock.release()
451456

457+
datetime_format = "%Y-%m-%d %H:%M:%S"
452458
# return a rendered clone if response indicates a valid status code
453459
if rendering_is_valid or Settings().ignore_feedback:
454-
return RenderedSequence(duplicate, valid=True, final_request_response=response)
460+
return RenderedSequence(duplicate, valid=True, final_request_response=response,
461+
response_datetime=response_datetime.strftime(datetime_format))
455462
else:
456463
information = None
457464
if response.has_valid_code():
@@ -461,7 +468,9 @@ def render(self, candidate_values_pool, lock, preprocessing=False, postprocessin
461468
information = FailureInformation.RESOURCE_CREATION
462469
elif response.has_bug_code():
463470
information = FailureInformation.BUG
464-
return RenderedSequence(duplicate, valid=False, failure_info=information, final_request_response=response)
471+
return RenderedSequence(duplicate, valid=False, failure_info=information,
472+
final_request_response=response,
473+
response_datetime=response_datetime.strftime(datetime_format))
465474

466475
return RenderedSequence(None)
467476

restler/engine/transport_layer/response.py

+15
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ def body(self):
6666
except:
6767
return None
6868

69+
@property
70+
def headers(self):
71+
""" The headers of the response
72+
73+
@return: The headers
74+
@rtype : List[Str]
75+
76+
"""
77+
try:
78+
response_without_body = self._str.split(DELIM)[0]
79+
# assumed format: HTTP/1.1 STATUS_CODE STATUS TEXT\r\nresponse...
80+
return response_without_body.split(" ", 2)[2].split('\r\n')[1:]
81+
except:
82+
return None
83+
6984
@property
7085
def json_body(self):
7186
""" The json portion of the body if exists.

restler/utils/logger.py

+1
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,7 @@ def print_spec_coverage(fuzzing_requests):
890890
req_spec['invalid_due_to_500'] = 1
891891
req_spec['status_code'] = req.stats.status_code
892892
req_spec['status_text'] = req.stats.status_text
893+
req_spec['sample_request'] = vars(req.stats.sample_request)
893894
req_spec['error_message'] = req.stats.error_msg
894895
req_spec['request_order'] = req.stats.request_order
895896

utilities/speccovparsing/diff_speccov.py

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ def diff_reqs(left_req, right_req):
2828
# NOTE: Any differences in keys here will either cause a failure or be
2929
# ignored. The files are expected to have matching formats.
3030
for key in left_req.keys():
31+
# Skip the sample request, which contains concrete values in paths, timestamps, etc.
32+
if key == "sample_request":
33+
continue
3134
try:
3235
if (type(left_req[key]) != type(right_req[key]))\
3336
or left_req[key] != right_req[key]:

0 commit comments

Comments
 (0)