Commit 936745d2 authored by Juan Antonio Navarro Perez's avatar Juan Antonio Navarro Perez Committed by Commit Bot

[flakiness_cli] Convert test results response into data frame.

Add a function to convert the response from a test results request
into a data frame for convenient access of the data.

Also add corresponding unit tests.

Bug: 875251
Change-Id: I396a5c209e37a470ab305edbee82ccc407472766
Reviewed-on: https://chromium-review.googlesource.com/1185184Reviewed-by: default avatarNed Nguyen <nednguyen@google.com>
Commit-Queue: Juan Antonio Navarro Pérez <perezju@chromium.org>
Cr-Commit-Position: refs/heads/master@{#585525}
parent c40ba7f2
...@@ -30,7 +30,8 @@ def main(): ...@@ -30,7 +30,8 @@ def main():
data = api.GetBuilders() data = api.GetBuilders()
print frames.BuildersDataFrame(data) print frames.BuildersDataFrame(data)
elif args.action == 'results': elif args.action == 'results':
print api.GetTestResults(args.master, args.builder, args.test_type) data = api.GetTestResults(args.master, args.builder, args.test_type)
print frames.TestResultsDataFrame(data)
else: else:
raise NotImplementedError(args.action) raise NotImplementedError(args.action)
......
...@@ -10,7 +10,13 @@ except ImportError: ...@@ -10,7 +10,13 @@ except ImportError:
pandas = None pandas = None
TEST_RESULTS_COLUMNS = (
'timestamp', 'builder', 'build_number', 'commit_pos', 'test_suite',
'test_case', 'result', 'time')
def BuildersDataFrame(data): def BuildersDataFrame(data):
"""Convert a builders request response into a data frame."""
def iter_rows(): def iter_rows():
for master in data['masters']: for master in data['masters']:
for test_type, builders in master['tests'].iteritems(): for test_type, builders in master['tests'].iteritems():
...@@ -19,3 +25,134 @@ def BuildersDataFrame(data): ...@@ -19,3 +25,134 @@ def BuildersDataFrame(data):
return pandas.DataFrame.from_records( return pandas.DataFrame.from_records(
iter_rows(), columns=('master', 'builder', 'test_type')) iter_rows(), columns=('master', 'builder', 'test_type'))
def _RunLengthDecode(count_value_pairs):
"""Iterator to expand a run length encoded sequence.
The test results dashboard compresses some long lists using "run length
encoding", for example:
['F', 'F', 'F', 'P', 'P', 'P', 'P', 'F', 'F']
becomes:
[[3, 'F'], [4, 'P'], [2, 'F']]
This function takes the encoded version and returns an iterator that yields
the elements of the expanded one.
Args:
count_value_pairs: A list of [count, value] pairs.
Yields:
Each value of the expanded sequence, one at a time.
"""
for count, value in count_value_pairs:
for _ in xrange(count):
yield value
def _IterTestResults(tests_dict, test_path=None):
"""Parse and iterate over the "tests" section of a test results response.
The test results dashboard supports multiple levels of "subtests" organised
as a tree of nested dicts. The leafs of the tree are "results" dicts.
This iterator "flattens out" the test path names, splitting them into a
test_suite (the top level name) and test_case (all other "sub" names, if any).
For example:
{
'A': {
'1': {'results': [...]},
'2': {'results': [...]}
},
'B': {
'3': {
'a': {'results': [...]},
'b': {'results': [...]},
'c': {'results': [...]}
}
},
'C': {'results': [...]}
}
Will generate 6 responses when iterated over:
('A', '1', {'results': [...]})
('A', '2', {'results': [...]})
('B', '3/a', {'results': [...]})
('B', '3/b', {'results': [...]})
('B', '3/c', {'results': [...]})
('C', '', {'results': [...]})
Args:
tests_dict: The 'tests' dictionary as contained in a test results response.
test_path: An optional prefix for the test path. (Not meant to be used
directly, needed for the recursive implementation.)
Yields:
A tripe (test_suite, test_case, test_results) for each test path contained
in the input.
"""
if 'results' in tests_dict:
assert test_path # Should not be missing or empty.
yield test_path[0], '/'.join(test_path[1:]), tests_dict
else:
if test_path is None:
test_path = []
for test_name, subtests_dict in tests_dict.iteritems():
test_path.append(test_name)
for test_row in _IterTestResults(subtests_dict, test_path):
yield test_row
assert test_path.pop() == test_name
def _AddDataFrameColumn(df, col, values, fill_value=0):
"""Add a sequence of values as a new column, filling values if needed.
Args:
df: A data frame on which to add the column, it is modified in place.
col: A string with the name for the new column.
values: A sequence of values for the column.
fill_value: If the sequence of values is shorter that the current number
of rows in the df, pad the sequence with extra copies of `fill_value`
to make the number of rows match.
"""
df[col] = pandas.Series(list(values)).reindex(df.index, fill_value=fill_value)
def TestResultsDataFrame(data):
"""Convert a test results request response into a data frame."""
assert data['version'] == 4
dfs = []
for builder, builder_data in data.iteritems():
if builder == 'version':
continue # Skip, not a builder.
builds = pandas.DataFrame()
builds['timestamp'] = pandas.to_datetime(
builder_data['secondsSinceEpoch'], unit='s')
builds['builder'] = builder
builds['build_number'] = builder_data['buildNumbers']
_AddDataFrameColumn(builds, 'commit_pos', builder_data['chromeRevision'])
for test_suite, test_case, test_results in _IterTestResults(
builder_data['tests']):
df = builds.copy()
df['test_suite'] = test_suite
df['test_case'] = test_case
_AddDataFrameColumn(df, 'result', _RunLengthDecode(
test_results['results']), fill_value='N')
_AddDataFrameColumn(df, 'time', _RunLengthDecode(test_results['times']))
dfs.append(df)
if dfs:
df = pandas.concat(dfs, ignore_index=True)
assert tuple(df.columns) == TEST_RESULTS_COLUMNS
else:
# Return an empty data frame with the right column names otherwise.
df = pandas.DataFrame(columns=TEST_RESULTS_COLUMNS)
return df
...@@ -64,3 +64,110 @@ class TestDataFrames(unittest.TestCase): ...@@ -64,3 +64,110 @@ class TestDataFrames(unittest.TestCase):
self.assertItemsEqual( self.assertItemsEqual(
df[df['builder'] == 'my-new-android-bot']['master'].unique(), df[df['builder'] == 'my-new-android-bot']['master'].unique(),
['chromium.perf.fyi']) ['chromium.perf.fyi'])
def testRunLengthDecode(self):
encoded = [[3, 'F'], [4, 'P'], [2, 'F']]
decoded = ['F', 'F', 'F', 'P', 'P', 'P', 'P', 'F', 'F']
# pylint: disable=protected-access
self.assertSequenceEqual(
list(frames._RunLengthDecode(encoded)), decoded)
def testIterTestResults(self):
tests_dict = {
'A': {
'1': {'results': 'A/1'},
'2': {'results': 'A/2'}
},
'B': {
'3': {
'a': {'results': 'B/3/a'},
'b': {'results': 'B/3/b'},
'c': {'results': 'B/3/c'}
}
},
'C': {'results': 'C'}
}
expected = [
('A', '1', {'results': 'A/1'}),
('A', '2', {'results': 'A/2'}),
('B', '3/a', {'results': 'B/3/a'}),
('B', '3/b', {'results': 'B/3/b'}),
('B', '3/c', {'results': 'B/3/c'}),
('C', '', {'results': 'C'}),
]
# pylint: disable=protected-access
self.assertItemsEqual(
list(frames._IterTestResults(tests_dict)), expected)
def testTestResultsDataFrame(self):
data = {
'android-bot': {
'secondsSinceEpoch': [1234567892, 1234567891, 1234567890],
'buildNumbers': [42, 41, 40],
'chromeRevision': [1234, 1233, 1232],
'tests': {
'some_benchmark': {
'story_1': {
'results': [[3, 'P']],
'times': [[3, 1]]
},
'story_2': {
'results': [[2, 'Q']],
'times': [[2, 5]]
}
},
'another_benchmark': {
'story_3': {
'results': [[1, 'Q'], [2, 'P']],
'times': [[1, 3], [1, 2], [1, 3]]
}
}
}
},
'version': 4
}
df = frames.TestResultsDataFrame(data)
# Poke and check a few simple facts about our sample data.
# We have data from 3 builds x 3 stories:
self.assertEqual(len(df), 9)
# All run on the same bot.
self.assertTrue((df['builder'] == 'android-bot').all())
# The most recent build number was 42.
self.assertEqual(df['build_number'].max(), 42)
# some_benchmark/story_1 passed on all builds.
selection = df[df['test_case'] == 'story_1']
self.assertTrue((selection['test_suite'] == 'some_benchmark').all())
self.assertTrue((selection['result'] == 'P').all())
# There was no data for story_2 on build 40.
selection = df[(df['test_case'] == 'story_2') & (df['build_number'] == 40)]
self.assertEqual(len(selection), 1)
self.assertTrue(selection.iloc[0]['result'], 'N')
def testTestResultsDataFrame_empty(self):
data = {
'android-bot': {
'secondsSinceEpoch': [1234567892, 1234567891, 1234567890],
'buildNumbers': [42, 41, 40],
'chromeRevision': [1234, 1233, 1232],
'tests': {}
},
'version': 4
}
df = frames.TestResultsDataFrame(data)
# The data frame is empty.
self.assertTrue(df.empty)
# Column names are still defined (although of course empty).
self.assertItemsEqual(df['test_case'].unique(), [])
def testTestResultsDataFrame_wrongVersionRejected(self):
data = {
'android-bot': {
'some': ['new', 'fancy', 'results', 'encoding']
},
'version': 5
}
with self.assertRaises(AssertionError):
frames.TestResultsDataFrame(data)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment