Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(157)

Side by Side Diff: scripts/slave/recipe_modules/json/api.py

Issue 23889036: Refactor the way that TestApi works so that it is actually useful. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: rebase Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 # Copyright 2013 The Chromium Authors. All rights reserved. 1 # Copyright 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import functools 5 import functools
6 import contextlib
6 import json 7 import json
7 import os 8 import os
8 import tempfile 9 import tempfile
9 10
10 from cStringIO import StringIO 11 from cStringIO import StringIO
11 12
12 from slave import recipe_api 13 from slave import recipe_api
14 from slave import recipe_util
15
16 from .util import TestResults
13 17
14 class StringListIO(object): 18 class StringListIO(object):
15 def __init__(self): 19 def __init__(self):
16 self.lines = [StringIO()] 20 self.lines = [StringIO()]
17 21
18 def write(self, s): 22 def write(self, s):
19 while s: 23 while s:
20 i = s.find('\n') 24 i = s.find('\n')
21 if i == -1: 25 if i == -1:
22 self.lines[-1].write(s) 26 self.lines[-1].write(s)
23 break 27 break
24 self.lines[-1].write(s[:i]) 28 self.lines[-1].write(s[:i])
25 self.lines[-1] = self.lines[-1].getvalue() 29 self.lines[-1] = self.lines[-1].getvalue()
26 self.lines.append(StringIO()) 30 self.lines.append(StringIO())
27 s = s[i+1:] 31 s = s[i+1:]
28 32
29 def close(self): 33 def close(self):
30 if not isinstance(self.lines[-1], basestring): 34 if not isinstance(self.lines[-1], basestring):
31 self.lines[-1] = self.lines[-1].getvalue() 35 self.lines[-1] = self.lines[-1].getvalue()
32 36
33 37
34 def convert_trie_to_flat_paths(trie, prefix=None): 38 class JsonOutputPlaceholder(recipe_util.Placeholder):
35 # Cloned from webkitpy.layout_tests.layout_package.json_results_generator
36 # so that this code can stand alone.
37 result = {}
38 for name, data in trie.iteritems():
39 if prefix:
40 name = prefix + "/" + name
41
42 if len(data) and not "actual" in data and not "expected" in data:
43 result.update(convert_trie_to_flat_paths(data, name))
44 else:
45 result[name] = data
46
47 return result
48
49
50 class TestResults(object):
51 def __init__(self, jsonish):
52 self.raw = jsonish
53
54 self.tests = convert_trie_to_flat_paths(jsonish.get('tests', {}))
55 self.passes = {}
56 self.unexpected_passes = {}
57 self.failures = {}
58 self.unexpected_failures = {}
59 self.flakes = {}
60 self.unexpected_flakes = {}
61
62 for (test, result) in self.tests.iteritems():
63 key = 'unexpected_' if result.get('is_unexpected') else ''
64 actual_result = result['actual']
65 data = actual_result
66 if ' PASS' in actual_result:
67 key += 'flakes'
68 elif actual_result == 'PASS':
69 key += 'passes'
70 data = result
71 else:
72 key += 'failures'
73 getattr(self, key)[test] = data
74
75 def __getattr__(self, key):
76 if key in self.raw:
77 return self.raw[key]
78 raise AttributeError("'%s' object has no attribute '%s'" %
79 (self.__class__, key)) # pragma: no cover
80
81
82 class JsonOutputPlaceholder(recipe_api.Placeholder):
83 """JsonOutputPlaceholder is meant to be a placeholder object which, when added 39 """JsonOutputPlaceholder is meant to be a placeholder object which, when added
84 to a step's cmd list, will be replaced by annotated_run with the command 40 to a step's cmd list, will be replaced by annotated_run with the command
85 parameters --output-json /path/to/file during the evaluation of your recipe 41 parameters --output-json /path/to/file during the evaluation of your recipe
86 generator. 42 generator.
87 43
88 This placeholder can be optionally added when you use the Steps.step() 44 This placeholder can be optionally added when you use the Steps.step()
89 method in this module. 45 method in this module.
90 46
91 After the termination of the step, this file is expected to contain a valid 47 After the termination of the step, this file is expected to contain a valid
92 JSON document, which will be set as the json_output for that step in the 48 JSON document, which will be set as the json_output for that step in the
93 step_history OrderedDict passed to your recipe generator. 49 step_history OrderedDict passed to your recipe generator.
94 """ 50 """
95 # TODO(iannucci): The --output-json was a shortsighted bug. It should be 51 # TODO(iannucci): The --output-json was a shortsighted bug. It should be
96 # --json-output to generalize to '--<module>-<method>' convention, which is 52 # --json-output to generalize to '--<module>-<method>' convention, which is
97 # used in multiple places in the recipe ecosystem. 53 # used in multiple places in the recipe ecosystem.
98 def __init__(self, name='output', flag='--output-json'): 54 def __init__(self, mod_name, name='output', flag='--output-json'):
99 self.name = name
100 self.flag = flag 55 self.flag = flag
101 self.output_file = None 56 self.output_file = None
102 super(JsonOutputPlaceholder, self).__init__() 57 super(JsonOutputPlaceholder, self).__init__(name, mod_name)
103 58
104 def render(self, test_data): 59 def render(self, test):
105 items = [self.flag] 60 items = [self.flag]
106 if test_data is not None: 61 if test.enabled:
107 items.append('/path/to/tmp/json') 62 items.append('/path/to/tmp/json')
108 else: # pragma: no cover 63 else: # pragma: no cover
109 json_output_fd, self.output_file = tempfile.mkstemp() 64 json_output_fd, self.output_file = tempfile.mkstemp()
110 os.close(json_output_fd) 65 os.close(json_output_fd)
111 items.append(self.output_file) 66 items.append(self.output_file)
112 return items 67 return items
113 68
114 def step_finished(self, presentation, result_data, test_data): 69 def result(self, presentation, test):
115 assert not hasattr(result_data, self.name) 70 if test.enabled:
116 if test_data is not None: 71 raw_data = json.dumps(test.data)
117 raw_data = json.dumps(test_data.pop(self.name, None))
118 else: # pragma: no cover 72 else: # pragma: no cover
119 assert self.output_file is not None 73 assert self.output_file is not None
120 with open(self.output_file, 'r') as f: 74 with open(self.output_file, 'r') as f:
121 raw_data = f.read() 75 raw_data = f.read()
122 os.unlink(self.output_file) 76 os.unlink(self.output_file)
123 77
124 valid = False 78 valid = False
79 ret = None
125 try: 80 try:
126 setattr(result_data, self.name, json.loads(raw_data)) 81 ret = json.loads(raw_data)
127 valid = True 82 valid = True
128 except ValueError: # pragma: no cover 83 except ValueError: # pragma: no cover
129 pass 84 pass
130 85
131 key = 'json.' + self.name + ('' if valid else ' (invalid)') 86 key = self.name + ('' if valid else ' (invalid)')
132 listio = StringListIO() 87 with contextlib.closing(StringListIO()) as listio:
133 json.dump(getattr(result_data, self.name), listio, indent=2, sort_keys=True) 88 json.dump(ret, listio, indent=2, sort_keys=True)
134 listio.close()
135 presentation.logs[key] = listio.lines 89 presentation.logs[key] = listio.lines
136 90
91 return ret
92
137 93
138 class TestResultsOutputPlaceholder(JsonOutputPlaceholder): 94 class TestResultsOutputPlaceholder(JsonOutputPlaceholder):
139 def __init__(self): 95 def __init__(self, mod_name):
140 super(TestResultsOutputPlaceholder, self).__init__( 96 super(TestResultsOutputPlaceholder, self).__init__(
141 name='test_results', flag='--json-test-results') 97 mod_name, name='test_results', flag='--json-test-results')
142 98
143 def step_finished(self, presentation, result_data, test_data): 99 def result(self, presentation, test):
144 super(TestResultsOutputPlaceholder, self).step_finished( 100 ret = super(TestResultsOutputPlaceholder, self).result(presentation, test)
145 presentation, result_data, test_data) 101 return TestResults(ret)
146 result_data.test_results = TestResults(result_data.test_results)
147 102
148 103
149 class JsonApi(recipe_api.RecipeApi): 104 class JsonApi(recipe_api.RecipeApi):
150 def __init__(self, **kwargs): 105 def __init__(self, **kwargs):
151 super(JsonApi, self).__init__(**kwargs) 106 super(JsonApi, self).__init__(**kwargs)
152 self.loads = json.loads 107 self.loads = json.loads
153 @functools.wraps(json.dumps) 108 @functools.wraps(json.dumps)
154 def dumps(*args, **kwargs): 109 def dumps(*args, **kwargs):
155 kwargs['sort_keys'] = True 110 kwargs['sort_keys'] = True
156 return json.dumps(*args, **kwargs) 111 return json.dumps(*args, **kwargs)
157 self.dumps = dumps 112 self.dumps = dumps
158 113
159 def input(self, data): 114 def input(self, data):
160 """A placeholder which will expand to a file path containing <data>.""" 115 """A placeholder which will expand to a file path containing <data>."""
161 return recipe_api.InputDataPlaceholder(self.dumps(data), '.json') 116 return self.m.raw.input(self.dumps(data), '.json', self.name)
162 117
163 @staticmethod 118 def output(self):
164 def output():
165 """A placeholder which will expand to '--output-json /tmp/file'.""" 119 """A placeholder which will expand to '--output-json /tmp/file'."""
166 return JsonOutputPlaceholder() 120 return JsonOutputPlaceholder(self.name)
167 121
168 @staticmethod 122 def test_results(self):
169 def test_results():
170 """A placeholder which will expand to '--json-test-results /tmp/file'. 123 """A placeholder which will expand to '--json-test-results /tmp/file'.
171 124
172 The test_results will be an instance of the TestResults class. 125 The test_results will be an instance of the TestResults class.
173 """ 126 """
174 return TestResultsOutputPlaceholder() 127 return TestResultsOutputPlaceholder(self.name)
175 128
176 def property_args(self): 129 def property_args(self):
177 """Return --build-properties and --factory-properties arguments. LEGACY! 130 """Return --build-properties and --factory-properties arguments. LEGACY!
178 131
179 Since properties is the merge of build_properties and factory_properties, 132 Since properties is the merge of build_properties and factory_properties,
180 pass the merged dict as both arguments. 133 pass the merged dict as both arguments.
181 134
182 It's vastly preferable to have your recipe only pass the bare minimum 135 It's vastly preferable to have your recipe only pass the bare minimum
183 of arguments to steps. Passing property objects obscures the data that 136 of arguments to steps. Passing property objects obscures the data that
184 the script actually consumes from the property object. 137 the script actually consumes from the property object.
185 """ 138 """
186 prop_str = self.dumps(dict(self.m.properties)) 139 prop_str = self.dumps(dict(self.m.properties))
187 return [ 140 return [
188 '--factory-properties', prop_str, 141 '--factory-properties', prop_str,
189 '--build-properties', prop_str 142 '--build-properties', prop_str
190 ] 143 ]
191
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698