Rework get_test_runs_for_test() rest api

This commit reworks the get_test_runs_for() rest api call and changes
it's basic behavior. The first thing done is the numeric data is split
from the non-numeric data in the response. This was done to enable
mean based resampling on the numeric data. This is necessary because
the other large change to the api is that a running mean and a running
std dev are added to the response. This was done to enable more
detailed graphs to the UI for the run_time of a single test.
To perform the running mean and std dev pandas is added to the
requirements for the rest api.

Change-Id: I3416dc0cad24c90405ac437df31c39b59f524d88
This commit is contained in:
Matthew Treinish 2015-12-04 19:20:07 -05:00
parent aed95e34a6
commit 9b0b664517
No known key found for this signature in database
GPG Key ID: FD12A0F214C9E177
4 changed files with 87 additions and 19 deletions

View File

@ -309,12 +309,24 @@ def get_test_runs_for_test(test_id):
session = Session()
start_date = _parse_datetimes(flask.request.args.get('start_date', None))
stop_date = _parse_datetimes(flask.request.args.get('stop_date', None))
datetime_resolution = flask.request.args.get('datetime_resolution', 'min')
if datetime_resolution not in ['sec', 'min', 'hour', 'day']:
message = ('Datetime resolution: %s, is not a valid'
' choice' % datetime_resolution)
status_code = 400
return abort(make_response(message, status_code))
db_test_runs = api.get_test_runs_by_test_test_id(test_id, session=session,
start_date=start_date,
stop_date=stop_date)
if not db_test_runs:
# NOTE(mtreinish) if no data is returned from the DB just return an
# empty set response, the test_run_aggregator function assumes data
# is present.
return jsonify({'numeric': {}, 'data': {}})
test_runs = test_run_aggregator.convert_test_runs_list_to_time_series_dict(
db_test_runs)
return jsonify({'test_runs': test_runs})
db_test_runs, datetime_resolution)
return jsonify(test_runs)
def main():

View File

@ -12,31 +12,78 @@
# License for the specific language governing permissions and limitations
# under the License.
import pandas as pd
from subunit2sql import read_subunit
from base_aggregator import BaseAggregator
def convert_test_runs_list_to_time_series_dict(test_runs_list):
test_runs = {}
def convert_test_runs_list_to_time_series_dict(test_runs_list, resample):
test_runs = []
for test_run in test_runs_list:
tr = test_run.to_dict()
# Populate dict
start_time = test_run.start_time
if start_time and test_run.start_time_microsecond:
start_time = start_time.replace(
microsecond=test_run.start_time_microsecond)
tr['start_time'] = start_time
tr.pop('start_time_microsecond')
if test_run.stop_time:
stop_time = test_run.stop_time
if test_run.stop_time_microsecond:
stop_time = stop_time.replace(
microsecond=test_run.stop_time_microsecond)
test_run_dict = {
'run_time': read_subunit.get_duration(start_time, stop_time),
'status': test_run.status,
'run_id': test_run.run_id
}
test_runs[start_time.isoformat()] = test_run_dict
return test_runs
tr['stop_time'] = stop_time
tr['run_time'] = read_subunit.get_duration(start_time,
tr.pop('stop_time'))
tr.pop('stop_time_microsecond')
tr.pop('id')
tr.pop('test_id')
test_runs.append(tr)
df = pd.DataFrame(test_runs).set_index('start_time')
df.index = pd.DatetimeIndex(df.index)
# Add rolling mean and std dev of run_time to datafram
df['avg_run_time'] = pd.rolling_mean(df['run_time'], 20)
df['stddev_run_time'] = pd.rolling_std(df['run_time'], 20)
# Resample numeric data for the run_time graph from successful runs
resample_matrix = {
'day': 'D',
'hour': '1H',
'min': '1T',
'sec': '1S',
}
numeric_df = df[df['status'] == 'success'].resample(
resample_matrix[resample], how='mean')
# Drop duplicate or invalid colums
del(numeric_df['run_id'])
del(df['run_time'])
del(df['avg_run_time'])
del(df['stddev_run_time'])
# Drop missing data from the resample
numeric_df = numeric_df.dropna(how='all')
# Convert the dataframes to a dict
numeric_dict = dict(
(date.isoformat(),
{
'run_time': run_time,
'avg_run_time': avg,
'std_dev_run_time': stddev,
}) for date, run_time, avg, stddev in zip(
numeric_df.index, numeric_df.run_time, numeric_df.avg_run_time,
numeric_df.stddev_run_time))
temp_dict = dict(
(date.isoformat(),
{
'run_id': run_id,
'status': status,
}) for date, run_id, status in zip(df.index, df.run_id, df.status))
return {'numeric': numeric_dict, 'data': temp_dict}
class Status(object):

View File

@ -16,6 +16,7 @@ import datetime
import json
import mock
import numpy
from subunit2sql.db import models
from openstack_health import api
@ -664,22 +665,28 @@ class TestRestAPI(base.TestCase):
@mock.patch('subunit2sql.db.api.get_test_runs_by_test_test_id',
return_value=[models.TestRun(
id='fake_id', test_id='test.id', run_id='fake_run_id',
id=2, test_id=1234, run_id=1234,
status='success', start_time=timestamp_a,
stop_time=timestamp_b)])
start_time_microsecond=0,
stop_time=timestamp_b,
stop_time_microsecond=0)])
def test_get_test_runs_for_test(self, api_mock):
api.Session = mock.MagicMock()
res = self.app.get('/test_runs/fake.test.id')
self.assertEqual(200, res.status_code)
exp_result = {'test_runs': {
exp_result = {'data': {
timestamp_a.isoformat(): {
'run_time': 1.0,
'run_id': 1234,
'status': 'success',
'run_id': 'fake_run_id',
}
}}
}}, 'numeric': {
timestamp_a.isoformat(): {
'avg_run_time': numpy.NaN,
'run_time': 1.0,
'std_dev_run_time': numpy.NaN
}}
}
response_data = json.loads(res.data)
self.assertEqual(exp_result, response_data)
numpy.testing.assert_equal(exp_result, response_data)
api_mock.assert_called_once_with('fake.test.id', start_date=None,
stop_date=None,
session=api.Session())

View File

@ -8,3 +8,5 @@ sqlalchemy
flask-jsonpify
PyMySQL>=0.6.2
python-dateutil>=2.4.2
pandas>=0.17.1
numpy>=1.7.0