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

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

Issue 2768883004: json: Improve error handling and adding ability to limit log output.
Patch Set: Created 3 years, 9 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
OLDNEW
1 # Copyright 2013 The LUCI Authors. All rights reserved. 1 # Copyright 2013 The LUCI Authors. All rights reserved.
2 # Use of this source code is governed under the Apache License, Version 2.0 2 # Use of this source code is governed under the Apache License, Version 2.0
3 # that can be found in the LICENSE file. 3 # that can be found in the LICENSE file.
4 4
5 import sys
Paweł Hajdan Jr. 2017/03/23 12:22:12 nit: sort
5 import functools 6 import functools
6 import collections 7 import collections
7 import contextlib 8 import contextlib
8 import json 9 import json
9 10
10 from recipe_engine import recipe_api 11 from recipe_engine import recipe_api
11 from recipe_engine import util as recipe_util 12 from recipe_engine import util as recipe_util
12 from recipe_engine import config_types 13 from recipe_engine import config_types
13 14
14 15
15 class JsonOutputPlaceholder(recipe_util.OutputPlaceholder): 16 class JsonOutputPlaceholder(recipe_util.OutputPlaceholder):
16 """JsonOutputPlaceholder is meant to be a placeholder object which, when added 17 """JsonOutputPlaceholder is meant to be a placeholder object which, when added
17 to a step's cmd list, will be replaced by the recipe engine with the path to a 18 to a step's cmd list, will be replaced by the recipe engine with the path to a
18 temporary file (e.g. /tmp/tmp4lp1qM) which will exist only for the duration of 19 temporary file (e.g. /tmp/tmp4lp1qM) which will exist only for the duration of
19 the step. Create a JsonOutputPlaceholder by calling the 'output()' method of 20 the step. Create a JsonOutputPlaceholder by calling the 'output()' method of
20 the JsonApi. 21 the JsonApi.
21 22
22 The step is expected to write JSON data to this file, and when the step is 23 The step is expected to write JSON data to this file, and when the step is
23 finished, the file will be read and the JSON parsed back into the recipe, and 24 finished, the file will be read and the JSON parsed back into the recipe, and
24 will be available as part of the step result. 25 will be available as part of the step result.
25 26
26 Example: 27 Example:
27 result = api.step('step name', 28 result = api.step('step name',
28 ['write_json_to_file.sh', api.json.output()]) 29 ['write_json_to_file.sh', api.json.output()])
29 # `result.json.output` is the parsed JSON value. 30 # `result.json.output` is the parsed JSON value.
30 31
31 See the example recipe (./example.py) for some more uses. 32 See the example recipe (./example.py) for some more uses.
32 """ 33 """
33 def __init__(self, api, add_json_log, name=None, leak_to=None): 34 MAX_OUTPUT_BYTES = 5000
35
36 def __init__(self, api, add_json_log, force_full_log=False, name=None, leak_to =None):
34 self.raw = api.m.raw_io.output_text('.json', leak_to=leak_to) 37 self.raw = api.m.raw_io.output_text('.json', leak_to=leak_to)
38 if force_full_log:
39 self.max_output_bytes = None
40 else:
41 self.max_output_bytes = self.MAX_OUTPUT_BYTES
35 self.add_json_log = add_json_log 42 self.add_json_log = add_json_log
36 super(JsonOutputPlaceholder, self).__init__(name=name) 43 super(JsonOutputPlaceholder, self).__init__(name=name)
37 44
38 @property 45 @property
39 def backing_file(self): 46 def backing_file(self):
40 return self.raw.backing_file 47 return self.raw.backing_file
41 48
42 def render(self, test): 49 def render(self, test):
43 return self.raw.render(test) 50 return self.raw.render(test)
44 51
45 def result(self, presentation, test): 52 def result(self, presentation, test):
46 raw_data = self.raw.result(presentation, test) 53 raw_data = self.raw.result(presentation, test)
47 54
48 valid = False 55 valid = False
49 invalid_error = '' 56 invalid_error = ''
50 ret = None 57 ret = None
51 try: 58 try:
52 ret = JsonApi.loads( 59 ret = JsonApi.loads(
53 raw_data, object_pairs_hook=collections.OrderedDict) 60 raw_data, object_pairs_hook=collections.OrderedDict)
54 valid = True 61 valid = True
55 # TypeError is raised when raw_data is None, which can happen if the json 62 # TypeError is raised when raw_data is None, which can happen if the json
56 # file was not created. We then correctly handle this as invalid result. 63 # file was not created. We then correctly handle this as invalid result.
57 except (ValueError, TypeError) as ex: # pragma: no cover 64 except (ValueError, TypeError) as ex: # pragma: no cover
58 invalid_error = str(ex) 65 invalid_error = str(ex)
59 66
67 if not valid:
68 if raw_data is None:
69 explain_invalid = 'input was None'
iannucci 2017/03/23 19:38:00 This isn't a good check; 'null' is a perfectly val
70 elif not raw_data:
71 explain_invalid = 'input was empty'
iannucci 2017/03/23 19:38:00 This is also a bad check. The following json docum
72 else:
73 explain_invalid = 'input was invalid'
Paweł Hajdan Jr. 2017/03/23 12:22:12 Why not propagate the original exception then?
iannucci 2017/03/23 19:44:13 The exception is reported below. But yes; it migh
74
75 presentation.logs[self.label + ' (exception)'] = [
76 explain_invalid] + invalid_error.splitlines()
77
60 if self.add_json_log: 78 if self.add_json_log:
79 # If valid, output a pretty-printed version of the JSON file, otherwise
80 # just use the raw data.
61 if valid: 81 if valid:
62 with contextlib.closing(recipe_util.StringListIO()) as listio: 82 with contextlib.closing(recipe_util.StringListIO()) as listio:
63 json.dump(ret, listio, indent=2, sort_keys=True) 83 json.dump(ret, listio, indent=2, sort_keys=True)
64 presentation.logs[self.label] = listio.lines 84 log_data = listio.lines
65 else: 85 else:
66 presentation.logs[self.label + ' (invalid)'] = raw_data.splitlines() 86 log_data = raw_data.splitlines()
67 presentation.logs[self.label + ' (exception)'] = ( 87
68 invalid_error.splitlines()) 88 if self.max_output_bytes:
89 log_data = recipe_util.snip_output_lines(log_data, self.max_output_bytes )
Paweł Hajdan Jr. 2017/03/23 12:22:12 nit: 80 cols
90 presentation.logs[self.label] = log_data
69 91
70 return ret 92 return ret
71 93
72 94
73 class JsonApi(recipe_api.RecipeApi): 95 class JsonApi(recipe_api.RecipeApi):
74 def __init__(self, **kwargs): 96 def __init__(self, **kwargs):
75 super(JsonApi, self).__init__(**kwargs) 97 super(JsonApi, self).__init__(**kwargs)
76 @functools.wraps(json.dumps) 98 @functools.wraps(json.dumps)
77 def dumps(*args, **kwargs): 99 def dumps(*args, **kwargs):
78 kwargs['sort_keys'] = True 100 kwargs['sort_keys'] = True
(...skipping 26 matching lines...) Expand all
105 return True 127 return True
106 except Exception: 128 except Exception:
107 return False 129 return False
108 130
109 @recipe_util.returns_placeholder 131 @recipe_util.returns_placeholder
110 def input(self, data): 132 def input(self, data):
111 """A placeholder which will expand to a file path containing <data>.""" 133 """A placeholder which will expand to a file path containing <data>."""
112 return self.m.raw_io.input_text(self.dumps(data), '.json') 134 return self.m.raw_io.input_text(self.dumps(data), '.json')
113 135
114 @recipe_util.returns_placeholder 136 @recipe_util.returns_placeholder
115 def output(self, add_json_log=True, name=None, leak_to=None): 137 def output(self, add_json_log=True, force_full_log=False, name=None, leak_to=N one):
Paweł Hajdan Jr. 2017/03/23 12:22:12 Remember to propagate force_full_log.
iannucci 2017/03/23 19:44:13 Maybe `limit_log=<default amount with None meaning
116 """A placeholder which will expand to '/tmp/file'. 138 """A placeholder which will expand to '/tmp/file'.
117 139
118 If leak_to is provided, it must be a Path object. This path will be used in 140 If leak_to is provided, it must be a Path object. This path will be used in
119 place of a random temporary file, and the file will not be deleted at the 141 place of a random temporary file, and the file will not be deleted at the
120 end of the step. 142 end of the step.
143
144 Args:
145 add_json_log - Output the JSON in the step output.
146 force_full_log - Output the full JSON even if data is huge
147 name - Override the name of placeholder, default name is 'json.output'
148 leak_to - WTF does this do?
Paweł Hajdan Jr. 2017/03/23 12:22:12 See comment above ("If leak_to is provided").
121 """ 149 """
122 return JsonOutputPlaceholder(self, add_json_log, name=name, leak_to=leak_to) 150 return JsonOutputPlaceholder(self, add_json_log, name=name, leak_to=leak_to)
123 151
124 # TODO(you): This method should be in the `file` recipe_module 152 # TODO(you): This method should be in the `file` recipe_module
125 def read(self, name, path, add_json_log=True, output_name=None, **kwargs): 153 def read(self, name, path, add_json_log=True, output_name=None, **kwargs):
126 """Returns a step that reads a JSON file.""" 154 """Returns a step that reads a JSON file."""
127 return self.m.python.inline( 155 return self.m.python.inline(
128 name, 156 name,
129 """ 157 """
130 import shutil 158 import shutil
131 import sys 159 import sys
132 shutil.copy(sys.argv[1], sys.argv[2]) 160 shutil.copy(sys.argv[1], sys.argv[2])
133 """, 161 """,
134 args=[path, 162 args=[path,
135 self.output(add_json_log=add_json_log, name=output_name)], 163 self.output(add_json_log=add_json_log, name=output_name)],
136 add_python_log=False, 164 add_python_log=False,
137 **kwargs 165 **kwargs
138 ) 166 )
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698