Skip to content

Commit

Permalink
Allow setting custom submit_time for results
Browse files Browse the repository at this point in the history
This enables submitting results out-of-order based on incoming messages.

JIRA: RHELWF-7390
  • Loading branch information
hluk committed Aug 31, 2022
1 parent b7f9530 commit cd3d9a8
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 5 deletions.
5 changes: 3 additions & 2 deletions APIDOCS.apiary
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ An additional available parameter is `_distinct_on`, if specified allows the use
To create new `Result`, simply provide a JSON object containing the `outcome` and `testcase` fields.
Should you want to store more information, you can make use of `groups`, `note`, `ref_url` and `data` (the key-value store).

When a new `Result` is created, it is assigned an unique `id` and `submit_time` (UTC time of the `Result` submission) by the API.
When a new `Result` is created, it is assigned an unique `id` and `submit_time` (UTC time of the `Result` submission, unless it is overridden in the request) by the API.

+ Attributes (Result POST)

Expand Down Expand Up @@ -549,7 +549,7 @@ get updated with the new `ref_url`, if set in the JSON data.
- outcome (OUTCOMES, required) - Represents the outcome of the testing.
- note: `0 errors, 30 warnings` (string, optional, nullable) - Should be used as a _short_ summary of important information about the result. Detailed hints/logs should be accessible at the `ref_url` URL.
- `ref_url`: `https://taskotron-dev.fedoraproject.org/artifacts/all/27f94e36-62ec-11e6-83fd-525400d7d6a4/task_output/koschei-1.7.2-1.fc24.log` (string, optional) - Use as a pointer to logs/artifacts/detailed information about the result.
- submit_time: `2016-08-15T13:29:06` (string) - UTC time of the `Result` submission in ISO8601 format. Assigned by API at the moment of creation.
- submit_time: `2016-08-15T13:29:06` (string) - UTC time of the result creation in ISO8601 format.
- groups (array) - List of `Groups`'s UUIDs the result is part of.
- `27f94e36-62ec-11e6-83fd-525400d7d6a4` (string)
- data (object) - Any number of key-value pairs. Used to store any additional information. In Taskotron `item` and `type` are the most common keys used to represent "what was tested".
Expand Down Expand Up @@ -586,6 +586,7 @@ get updated with the new `ref_url`, if set in the JSON data.
- *key* (enum)
- `foo` (string) - Single value
- `foo`, `bar` (array[string]) - List of values
- `submit_time` (string/number, optional, nullable): UTC time of the result creation in ISO8601 format (YYYY-MM-DDTHH:MM:SS.ffffff) or a number of milliseconds since the Epoch. Defaults to the time of the `Result` submission.
- _auth (object, nullable, optional) - Placeholder for the future implemantation of Authentication/Authorization


Expand Down
13 changes: 11 additions & 2 deletions resultsdb/controllers/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from resultsdb.models.results import Group, Result, Testcase, ResultData
from resultsdb.models.results import RESULT_OUTCOME
from resultsdb.messaging import load_messaging_plugin, create_message, publish_taskotron_message
from resultsdb.lib.helpers import non_empty, dict_or_string, list_or_none
from resultsdb.lib.helpers import non_empty, dict_or_string, list_or_none, submit_time

QUERY_LIMIT = 20

Expand Down Expand Up @@ -600,6 +600,7 @@ def get_result(result_id):
RP['create_result'].add_argument('note', location='json')
RP['create_result'].add_argument('data', type=dict, location='json')
RP['create_result'].add_argument('ref_url', location='json')
RP['create_result'].add_argument('submit_time', type=submit_time, location='json')


@api.route('/results', methods=['POST'])
Expand Down Expand Up @@ -663,7 +664,15 @@ def create_result():
db.session.add(group)
groups.append(group)

result = Result(testcase, outcome, groups, args['ref_url'], args['note'])
result = Result(
testcase,
outcome,
groups,
args['ref_url'],
args['note'],
args['submit_time'],
)

# Convert result_data
# for each key-value pair in args['data']
# convert keys to unicode
Expand Down
33 changes: 33 additions & 0 deletions resultsdb/lib/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numbers
from datetime import datetime, timezone

try:
basestring
Expand Down Expand Up @@ -42,3 +43,35 @@ def list_or_none(value, *args, **kwargs):
if value is None:
return value
raise ValueError("Expected list or None, got %r" % type(value))


def time_from_milliseconds(value):
seconds, milliseconds = divmod(value, 1000)
time = datetime.fromtimestamp(seconds, tz=timezone.utc)
return time.replace(microsecond=milliseconds * 1000)


def submit_time(value, *args, **kwargs):
if args or kwargs:
raise TypeError("Unexpected arguments")
if isinstance(value, datetime):
return value
if value is None:
return value
if isinstance(value, numbers.Number):
return time_from_milliseconds(value)
if isinstance(value, str):
try:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%f')
except ValueError:
pass

try:
return time_from_milliseconds(int(value))
except ValueError:
pass
raise ValueError(
"Expected timestamp in milliseconds or datetime"
" (in format YYYY-MM-DDTHH:MM:SS.ffffff),"
" got %r" % type(value)
)
3 changes: 2 additions & 1 deletion resultsdb/models/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,13 @@ class Result(db.Model, DBSerialize):
),
)

def __init__(self, testcase, outcome, groups=None, ref_url=None, note=None):
def __init__(self, testcase, outcome, groups=None, ref_url=None, note=None, submit_time=None):
self.testcase = testcase
self.outcome = outcome
self.ref_url = ref_url
self.note = note
self.groups = groups
self.submit_time = submit_time


class ResultData(db.Model, DBSerialize):
Expand Down
52 changes: 52 additions & 0 deletions testing/functest_api_v20.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,58 @@ def test_create_result_invalid_data(self):
assert r.status_code == 400
assert data['message'].startswith("Colon not allowed in key name:")

def test_create_result_submit_time_as_number(self):
ref_data = json.dumps(dict(
outcome=self.ref_result_outcome,
testcase=self.ref_testcase,
submit_time=1661324097123,
))

r = self.app.post('/api/v2.0/results', data=ref_data, content_type='application/json')
data = json.loads(r.data)

assert r.status_code == 201
assert data['submit_time'] == '2022-08-24T06:54:57.123000'

def test_create_result_submit_time_as_number_string(self):
ref_data = json.dumps(dict(
outcome=self.ref_result_outcome,
testcase=self.ref_testcase,
submit_time="1661324097123",
))

r = self.app.post('/api/v2.0/results', data=ref_data, content_type='application/json')
data = json.loads(r.data)

assert r.status_code == 201
assert data['submit_time'] == '2022-08-24T06:54:57.123000'

def test_create_result_submit_time_as_datetime(self):
ref_data = json.dumps(dict(
outcome=self.ref_result_outcome,
testcase=self.ref_testcase,
submit_time='2022-08-24T06:54:57.123456',
))

r = self.app.post('/api/v2.0/results', data=ref_data, content_type='application/json')
data = json.loads(r.data)

assert r.status_code == 201
assert data['submit_time'] == '2022-08-24T06:54:57.123456'

def test_create_result_submit_time_as_invalid(self):
ref_data = json.dumps(dict(
outcome=self.ref_result_outcome,
testcase=self.ref_testcase,
submit_time='now',
))

r = self.app.post('/api/v2.0/results', data=ref_data, content_type='application/json')
data = json.loads(r.data)

assert r.status_code == 400
assert data['message'].startswith('Malformed Request')

def test_get_result(self):
self.test_create_result()

Expand Down

0 comments on commit cd3d9a8

Please sign in to comment.