blob: cf9dc48710e8bd5a66085d0e334141c3ef12b701 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions for making lists of tests, and an AJAX endpoint to list tests.
This module contains functions for listing:
- Sub-tests for a given test suite (in a tree structure).
- Tests which match a given test path pattern.
"""
import json
from google.appengine.ext import ndb
from dashboard import layered_cache
from dashboard import request_handler
from dashboard import utils
from dashboard.models import graph_data
class ListTestsHandler(request_handler.RequestHandler):
"""URL endpoint for AJAX requests to list masters, bots, and tests."""
def post(self):
"""Outputs a JSON string of the requested list.
Request parameters:
type: Type of list to make, one of "suite", "sub_tests" or "pattern".
suite: Test suite name (applies only if type is "sub_tests").
bots: Comma-separated bots name (applies only if type is "sub_tests").
p: Test path pattern (applies only if type is "pattern").
has_rows: "1" if the requester wants to list only list tests that
have points (applies only if type is "pattern").
Outputs:
A data structure with test names in JSON format, or nothing.
"""
self.response.headers.add_header('Access-Control-Allow-Origin', '*')
list_type = self.request.get('type')
# TODO(qyearsley): Separate these into two different handlers.
if list_type == 'sub_tests':
suite_name = self.request.get('suite')
bot_names = self.request.get('bots').split(',')
test_list = GetSubTests(suite_name, bot_names)
self.response.out.write(json.dumps(test_list))
if list_type == 'pattern':
pattern = self.request.get('p')
only_with_rows = self.request.get('has_rows') == '1'
test_list = GetTestsMatchingPattern(
pattern, only_with_rows=only_with_rows)
self.response.out.write(json.dumps(test_list))
def GetSubTests(suite_name, bot_names):
"""Gets the entire tree of subtests for the suite with the given name.
Each bot may have different sub-tests available, but there is one combined
sub-tests dict returned for all the bots specified.
This method is used by the test-picker select menus to display what tests
are available; only tests that are not deprecated should be listed.
Args:
suite_name: Top level test name.
bot_names: List of master/bot names in the form "<master>/<platform>".
Returns:
A dict mapping test names to dicts to entries which have the keys
"has_rows" (boolean) and "sub_tests", which is another sub-tests dict.
This forms a tree structure which matches the tree structure of the
Test entities in the datastore.
"""
# For some bots, there may be cached data; First collect and combine this.
combined = {}
for bot_name in bot_names:
master, bot = bot_name.split('/')
suite_key = ndb.Key('Master', master, 'Bot', bot, 'Test', suite_name)
cached = layered_cache.Get(_ListSubTestCacheKey(suite_key))
if cached:
combined = _MergeSubTestsDict(combined, cached)
else:
# Faster to fetch by keys than by projections.
sub_test_paths = _FetchSubTestPaths(suite_key, False)
deprecated_sub_test_paths = _FetchSubTestPaths(suite_key, True)
sub_tests = _MergeSubTestsDict(
_SubTestsDict(sub_test_paths, False),
_SubTestsDict(deprecated_sub_test_paths, True))
layered_cache.Set(_ListSubTestCacheKey(suite_key), sub_tests)
combined = _MergeSubTestsDict(combined, sub_tests)
return combined
def _FetchSubTestPaths(test_key, deprecated):
"""Makes a list of partial test paths for descendants of a test suite.
Args:
test_key: A ndb.Key object for a Test entity.
deprecated: Whether or not to fetch deprecated tests.
Returns:
A list of test paths for all descendant Test entities that have associated
Row entities. These test paths omit the Master/bot/suite part.
"""
query = graph_data.Test.query(ancestor=test_key)
query = query.filter(graph_data.Test.has_rows == True,
graph_data.Test.deprecated == deprecated)
keys = query.fetch(keys_only=True)
return map(_SubTestPath, keys)
def _SubTestPath(test_key):
"""Returns the part of a test path starting from after the test suite."""
full_test_path = utils.TestPath(test_key)
parts = full_test_path.split('/')
assert len(parts) > 3
return '/'.join(parts[3:])
def _SubTestsDict(paths, deprecated):
"""Constructs a sub-test dict from a list of test paths.
Args:
paths: An iterable of test paths for which there are points. Each test
path is of the form "Master/bot/benchmark/chart/...". Each test path
corresponds to a Test entity for which has_rows is set to True.
deprecated: Whether test are deprecated.
Returns:
A recursively nested dict of sub-tests, as returned by GetSubTests.
"""
sub_tests = {}
top_level = set(p.split('/')[0] for p in paths if p)
for name in top_level:
sub_test_paths = _SubPaths(paths, name)
has_rows = name in paths
sub_tests[name] = _SubTestsDictEntry(sub_test_paths, has_rows, deprecated)
return sub_tests
def _SubPaths(paths, first_part):
"""Returns paths of sub-tests that start with some name."""
assert first_part
return ['/'.join(p.split('/')[1:]) for p in paths
if '/' in p and p.split('/')[0] == first_part]
def _SubTestsDictEntry(sub_test_paths, has_rows, deprecated):
"""Recursively gets an entry in a sub-tests dict."""
entry = {
'has_rows': has_rows,
'sub_tests': _SubTestsDict(sub_test_paths, deprecated)
}
if deprecated:
entry['deprecated'] = True
return entry
def _ListSubTestCacheKey(test_key):
"""Returns the sub-tests list cache key for a test suite."""
parts = utils.TestPath(test_key).split('/')
master, bot, suite = parts[0:3]
return graph_data.LIST_TESTS_SUBTEST_CACHE_KEY % (master, bot, suite)
def _MergeSubTestsDict(a, b):
"""Merges two sub-tests dicts together."""
sub_tests = {}
a_names, b_names = set(a), set(b)
for name in a_names & b_names:
sub_tests[name] = _MergeSubTestsDictEntry(a[name], b[name])
for name in a_names - b_names:
sub_tests[name] = a[name]
for name in b_names - a_names:
sub_tests[name] = b[name]
return sub_tests
def _MergeSubTestsDictEntry(a, b):
"""Merges two corresponding sub-tests dict entries together."""
assert a and b
deprecated = a.get('deprecated', False) and b.get('deprecated', False)
entry = {
'has_rows': a['has_rows'] or b['has_rows'],
'sub_tests': _MergeSubTestsDict(a['sub_tests'], b['sub_tests'])
}
if deprecated:
entry['deprecated'] = True
return entry
def GetTestsMatchingPattern(pattern, only_with_rows=False, list_entities=False):
"""Gets the Test entities or keys which match |pattern|.
For this function, it's assumed that a test path should only have up to seven
parts. In theory, tests can be arbitrarily nested, but in practice, tests
are usually structured as master/bot/suite/graph/trace, and only a few have
seven parts.
Args:
pattern: /-separated string of '*' wildcard and Test string_ids.
only_with_rows: If True, only return Test entities which have data points.
list_entities: If True, return entities. If false, return keys (faster).
Returns:
A list of test paths, or test entities if list_entities is True.
"""
property_names = [
'master_name', 'bot_name', 'suite_name', 'test_part1_name',
'test_part2_name', 'test_part3_name', 'test_part4_name']
pattern_parts = pattern.split('/')
if len(pattern_parts) > 7:
return []
# Below, we first build a list of (property_name, value) pairs to filter on.
query_filters = []
for index, part in enumerate(pattern_parts):
if '*' not in part:
query_filters.append((property_names[index], part))
for index in range(len(pattern_parts), 7):
# Tests longer than the desired pattern will have non-empty property names,
# so they can be filtered out by matching against an empty string.
query_filters.append((property_names[index], ''))
# Query tests based on the above filters. Pattern parts with * won't be
# filtered here; the set of tests queried is a superset of the matching tests.
query = graph_data.Test.query()
for f in query_filters:
query = query.filter(
graph_data.Test._properties[f[0]] == f[1]) # pylint: disable=protected-access
query = query.order(graph_data.Test.key)
if only_with_rows:
query = query.filter(
graph_data.Test.has_rows == True)
test_keys = query.fetch(keys_only=True)
# Filter to include only tests that match the pattern.
test_keys = [k for k in test_keys if utils.TestMatchesPattern(k, pattern)]
if list_entities:
return ndb.get_multi(test_keys)
return [utils.TestPath(k) for k in test_keys]