Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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 ) |
| OLD | NEW |