• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2015 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Functions for making lists of tests, and an AJAX endpoint to list tests.
6
7This module contains functions for listing:
8 - Sub-tests for a given test suite (in a tree structure).
9 - Tests which match a given test path pattern.
10"""
11
12import json
13
14from google.appengine.ext import ndb
15
16from dashboard import layered_cache
17from dashboard import request_handler
18from dashboard import utils
19from dashboard.models import graph_data
20
21
22class ListTestsHandler(request_handler.RequestHandler):
23  """URL endpoint for AJAX requests to list masters, bots, and tests."""
24
25  def post(self):
26    """Outputs a JSON string of the requested list.
27
28    Request parameters:
29      type: Type of list to make, one of "suite", "sub_tests" or "pattern".
30      suite: Test suite name (applies only if type is "sub_tests").
31      bots: Comma-separated bots name (applies only if type is "sub_tests").
32      p: Test path pattern (applies only if type is "pattern").
33      has_rows: "1" if the requester wants to list only list tests that
34          have points (applies only if type is "pattern").
35
36    Outputs:
37      A data structure with test names in JSON format, or nothing.
38    """
39    self.response.headers.add_header('Access-Control-Allow-Origin', '*')
40    list_type = self.request.get('type')
41    # TODO(qyearsley): Separate these into two different handlers.
42
43    if list_type == 'sub_tests':
44      suite_name = self.request.get('suite')
45      bot_names = self.request.get('bots').split(',')
46      test_list = GetSubTests(suite_name, bot_names)
47      self.response.out.write(json.dumps(test_list))
48
49    if list_type == 'pattern':
50      pattern = self.request.get('p')
51      only_with_rows = self.request.get('has_rows') == '1'
52      test_list = GetTestsMatchingPattern(
53          pattern, only_with_rows=only_with_rows)
54      self.response.out.write(json.dumps(test_list))
55
56
57def GetSubTests(suite_name, bot_names):
58  """Gets the entire tree of subtests for the suite with the given name.
59
60  Each bot may have different sub-tests available, but there is one combined
61  sub-tests dict returned for all the bots specified.
62
63  This method is used by the test-picker select menus to display what tests
64  are available; only tests that are not deprecated should be listed.
65
66  Args:
67    suite_name: Top level test name.
68    bot_names: List of master/bot names in the form "<master>/<platform>".
69
70  Returns:
71    A dict mapping test names to dicts to entries which have the keys
72    "has_rows" (boolean) and "sub_tests", which is another sub-tests dict.
73    This forms a tree structure which matches the tree structure of the
74    Test entities in the datastore.
75  """
76  # For some bots, there may be cached data; First collect and combine this.
77  combined = {}
78  for bot_name in bot_names:
79    master, bot = bot_name.split('/')
80    suite_key = ndb.Key('Master', master, 'Bot', bot, 'Test', suite_name)
81    cached = layered_cache.Get(_ListSubTestCacheKey(suite_key))
82    if cached:
83      combined = _MergeSubTestsDict(combined, cached)
84    else:
85      # Faster to fetch by keys than by projections.
86      sub_test_paths = _FetchSubTestPaths(suite_key, False)
87      deprecated_sub_test_paths = _FetchSubTestPaths(suite_key, True)
88      sub_tests = _MergeSubTestsDict(
89          _SubTestsDict(sub_test_paths, False),
90          _SubTestsDict(deprecated_sub_test_paths, True))
91      layered_cache.Set(_ListSubTestCacheKey(suite_key), sub_tests)
92      combined = _MergeSubTestsDict(combined, sub_tests)
93  return combined
94
95
96def _FetchSubTestPaths(test_key, deprecated):
97  """Makes a list of partial test paths for descendants of a test suite.
98
99  Args:
100    test_key: A ndb.Key object for a Test entity.
101    deprecated: Whether or not to fetch deprecated tests.
102
103  Returns:
104    A list of test paths for all descendant Test entities that have associated
105    Row entities. These test paths omit the Master/bot/suite part.
106  """
107  query = graph_data.Test.query(ancestor=test_key)
108  query = query.filter(graph_data.Test.has_rows == True,
109                       graph_data.Test.deprecated == deprecated)
110  keys = query.fetch(keys_only=True)
111  return map(_SubTestPath, keys)
112
113
114def _SubTestPath(test_key):
115  """Returns the part of a test path starting from after the test suite."""
116  full_test_path = utils.TestPath(test_key)
117  parts = full_test_path.split('/')
118  assert len(parts) > 3
119  return '/'.join(parts[3:])
120
121
122def _SubTestsDict(paths, deprecated):
123  """Constructs a sub-test dict from a list of test paths.
124
125  Args:
126    paths: An iterable of test paths for which there are points. Each test
127        path is of the form "Master/bot/benchmark/chart/...". Each test path
128        corresponds to a Test entity for which has_rows is set to True.
129    deprecated: Whether test are deprecated.
130
131  Returns:
132    A recursively nested dict of sub-tests, as returned by GetSubTests.
133  """
134  sub_tests = {}
135  top_level = set(p.split('/')[0] for p in paths if p)
136  for name in top_level:
137    sub_test_paths = _SubPaths(paths, name)
138    has_rows = name in paths
139    sub_tests[name] = _SubTestsDictEntry(sub_test_paths, has_rows, deprecated)
140  return sub_tests
141
142
143def _SubPaths(paths, first_part):
144  """Returns paths of sub-tests that start with some name."""
145  assert first_part
146  return ['/'.join(p.split('/')[1:]) for p in paths
147          if '/' in p and p.split('/')[0] == first_part]
148
149
150def _SubTestsDictEntry(sub_test_paths, has_rows, deprecated):
151  """Recursively gets an entry in a sub-tests dict."""
152  entry = {
153      'has_rows': has_rows,
154      'sub_tests': _SubTestsDict(sub_test_paths, deprecated)
155  }
156  if deprecated:
157    entry['deprecated'] = True
158  return entry
159
160
161def _ListSubTestCacheKey(test_key):
162  """Returns the sub-tests list cache key for a test suite."""
163  parts = utils.TestPath(test_key).split('/')
164  master, bot, suite = parts[0:3]
165  return graph_data.LIST_TESTS_SUBTEST_CACHE_KEY % (master, bot, suite)
166
167
168def _MergeSubTestsDict(a, b):
169  """Merges two sub-tests dicts together."""
170  sub_tests = {}
171  a_names, b_names = set(a), set(b)
172  for name in a_names & b_names:
173    sub_tests[name] = _MergeSubTestsDictEntry(a[name], b[name])
174  for name in a_names - b_names:
175    sub_tests[name] = a[name]
176  for name in b_names - a_names:
177    sub_tests[name] = b[name]
178  return sub_tests
179
180
181def _MergeSubTestsDictEntry(a, b):
182  """Merges two corresponding sub-tests dict entries together."""
183  assert a and b
184  deprecated = a.get('deprecated', False) and b.get('deprecated', False)
185  entry = {
186      'has_rows': a['has_rows'] or b['has_rows'],
187      'sub_tests': _MergeSubTestsDict(a['sub_tests'], b['sub_tests'])
188  }
189  if deprecated:
190    entry['deprecated'] = True
191  return entry
192
193
194def GetTestsMatchingPattern(pattern, only_with_rows=False, list_entities=False):
195  """Gets the Test entities or keys which match |pattern|.
196
197  For this function, it's assumed that a test path should only have up to seven
198  parts. In theory, tests can be arbitrarily nested, but in practice, tests
199  are usually structured as master/bot/suite/graph/trace, and only a few have
200  seven parts.
201
202  Args:
203    pattern: /-separated string of '*' wildcard and Test string_ids.
204    only_with_rows: If True, only return Test entities which have data points.
205    list_entities: If True, return entities. If false, return keys (faster).
206
207  Returns:
208    A list of test paths, or test entities if list_entities is True.
209  """
210  property_names = [
211      'master_name', 'bot_name', 'suite_name', 'test_part1_name',
212      'test_part2_name', 'test_part3_name', 'test_part4_name']
213  pattern_parts = pattern.split('/')
214  if len(pattern_parts) > 7:
215    return []
216
217  # Below, we first build a list of (property_name, value) pairs to filter on.
218  query_filters = []
219  for index, part in enumerate(pattern_parts):
220    if '*' not in part:
221      query_filters.append((property_names[index], part))
222  for index in range(len(pattern_parts), 7):
223    # Tests longer than the desired pattern will have non-empty property names,
224    # so they can be filtered out by matching against an empty string.
225    query_filters.append((property_names[index], ''))
226
227  # Query tests based on the above filters. Pattern parts with * won't be
228  # filtered here; the set of tests queried is a superset of the matching tests.
229  query = graph_data.Test.query()
230  for f in query_filters:
231    query = query.filter(
232        graph_data.Test._properties[f[0]] == f[1])  # pylint: disable=protected-access
233  query = query.order(graph_data.Test.key)
234  if only_with_rows:
235    query = query.filter(
236        graph_data.Test.has_rows == True)
237  test_keys = query.fetch(keys_only=True)
238
239  # Filter to include only tests that match the pattern.
240  test_keys = [k for k in test_keys if utils.TestMatchesPattern(k, pattern)]
241
242  if list_entities:
243    return ndb.get_multi(test_keys)
244  return [utils.TestPath(k) for k in test_keys]
245