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