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 functools | 5 import functools |
| 6 import collections | 6 import collections |
| 7 import contextlib | 7 import contextlib |
| 8 import json | 8 import json |
| 9 | 9 |
| 10 from recipe_engine import recipe_api | 10 from recipe_engine import recipe_api |
| 11 from recipe_engine import util as recipe_util | 11 from recipe_engine import util as recipe_util |
| 12 from recipe_engine import config_types | 12 from recipe_engine import config_types |
| 13 | 13 |
| 14 | 14 |
| 15 class JsonOutputPlaceholder(recipe_util.OutputPlaceholder): | 15 class JsonOutputPlaceholder(recipe_util.OutputPlaceholder): |
| 16 """JsonOutputPlaceholder is meant to be a placeholder object which, when added | 16 """JsonOutputPlaceholder is meant to be a placeholder object which, when added |
| 17 to a step's cmd list, will be replaced by annotated_run with the path to a | 17 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 | 18 temporary file (e.g. /tmp/tmp4lp1qM) which will exist only for the duration of |
| 19 the step. If the script requires a flag (e.g. --output-json /path/to/file), | 19 the step. Create a JsonOutputPlaceholder by calling the 'output()' method of |
| 20 you must supply that flag yourself in the cmd list. | 20 the JsonApi. |
| 21 | 21 |
| 22 This placeholder can be optionally added when you use the Steps.step() | 22 The step is expected to write JSON data to this file, and when the step is |
| 23 method in this module. | 23 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. | |
| 24 | 25 |
| 25 FIXME | 26 Example: |
| 26 After the termination of the step, this file is expected to contain a valid | 27 result = api.step('step name', |
| 27 JSON document, which will be set as the json.output for that step in the | 28 ['write_json_to_file.sh', api.json.output()]) |
| 28 step_history OrderedDict passed to your recipe generator. | 29 # `result.json.output` is the parsed JSON value. |
| 30 | |
| 31 See the example recipe (./example.py) for some more uses. | |
| 29 """ | 32 """ |
| 30 def __init__(self, api, add_json_log, name=None): | 33 def __init__(self, api, add_json_log, name=None, leak_to=None): |
| 31 self.raw = api.m.raw_io.output_text('.json') | 34 self.raw = api.m.raw_io.output_text('.json', leak_to=leak_to) |
| 32 self.add_json_log = add_json_log | 35 self.add_json_log = add_json_log |
| 33 super(JsonOutputPlaceholder, self).__init__(name=name) | 36 super(JsonOutputPlaceholder, self).__init__(name=name) |
| 34 | 37 |
| 35 @property | 38 @property |
| 36 def backing_file(self): | 39 def backing_file(self): |
| 37 return self.raw.backing_file | 40 return self.raw.backing_file |
| 38 | 41 |
| 39 def render(self, test): | 42 def render(self, test): |
| 40 return self.raw.render(test) | 43 return self.raw.render(test) |
| 41 | 44 |
| 42 def result(self, presentation, test): | 45 def result(self, presentation, test): |
| 43 raw_data = self.raw.result(presentation, test) | 46 raw_data = self.raw.result(presentation, test) |
| 44 | 47 |
| 45 valid = False | 48 valid = False |
| 46 ret = None | 49 ret = None |
| 47 try: | 50 try: |
| 48 ret = JsonApi.loads( | 51 ret = JsonApi.loads( |
| 49 raw_data, object_pairs_hook=collections.OrderedDict) | 52 raw_data, object_pairs_hook=collections.OrderedDict) |
| 50 valid = True | 53 valid = True |
| 51 # TypeError is raised when raw_data is None, which can happen if the json | 54 # TypeError is raised when raw_data is None, which can happen if the json |
| 52 # file was not created. We then correctly handle this as invalid result. | 55 # file was not created. We then correctly handle this as invalid result. |
| 53 except (ValueError, TypeError): # pragma: no cover | 56 except (ValueError, TypeError): # pragma: no cover |
| 54 pass | 57 pass |
| 55 | 58 |
| 56 if self.add_json_log: | 59 if self.add_json_log: |
| 57 key = self.label + ('' if valid else ' (invalid)') | 60 if valid: |
| 58 with contextlib.closing(recipe_util.StringListIO()) as listio: | 61 with contextlib.closing(recipe_util.StringListIO()) as listio: |
| 59 json.dump(ret, listio, indent=2, sort_keys=True) | 62 json.dump(ret, listio, indent=2, sort_keys=True) |
| 60 presentation.logs[key] = listio.lines | 63 presentation.logs[self.label] = listio.lines |
| 64 else: | |
| 65 presentation.logs[self.label + ' (invalid)'] = raw_data.splitlines() | |
|
Paweł Hajdan Jr.
2017/03/14 12:56:14
Consider also capturing what the exception was and
iannucci
2017/03/14 17:34:53
Done.
| |
| 61 | 66 |
| 62 return ret | 67 return ret |
| 63 | 68 |
| 64 | 69 |
| 65 class JsonApi(recipe_api.RecipeApi): | 70 class JsonApi(recipe_api.RecipeApi): |
| 66 def __init__(self, **kwargs): | 71 def __init__(self, **kwargs): |
| 67 super(JsonApi, self).__init__(**kwargs) | 72 super(JsonApi, self).__init__(**kwargs) |
| 68 @functools.wraps(json.dumps) | 73 @functools.wraps(json.dumps) |
| 69 def dumps(*args, **kwargs): | 74 def dumps(*args, **kwargs): |
| 70 kwargs['sort_keys'] = True | 75 kwargs['sort_keys'] = True |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 97 return True | 102 return True |
| 98 except Exception: | 103 except Exception: |
| 99 return False | 104 return False |
| 100 | 105 |
| 101 @recipe_util.returns_placeholder | 106 @recipe_util.returns_placeholder |
| 102 def input(self, data): | 107 def input(self, data): |
| 103 """A placeholder which will expand to a file path containing <data>.""" | 108 """A placeholder which will expand to a file path containing <data>.""" |
| 104 return self.m.raw_io.input_text(self.dumps(data), '.json') | 109 return self.m.raw_io.input_text(self.dumps(data), '.json') |
| 105 | 110 |
| 106 @recipe_util.returns_placeholder | 111 @recipe_util.returns_placeholder |
| 107 def output(self, add_json_log=True, name=None): | 112 def output(self, add_json_log=True, name=None, leak_to=None): |
| 108 """A placeholder which will expand to '/tmp/file'.""" | 113 """A placeholder which will expand to '/tmp/file'. |
| 109 return JsonOutputPlaceholder(self, add_json_log, name=name) | 114 |
| 115 If leak_to is provided, it must be a Path object. This path will be used in | |
| 116 place of a random temporary file, and the file will not be deleted at the | |
| 117 end of the step. | |
| 118 """ | |
| 119 return JsonOutputPlaceholder(self, add_json_log, name=name, leak_to=leak_to) | |
| 110 | 120 |
| 111 # TODO(you): This method should be in the `file` recipe_module | 121 # TODO(you): This method should be in the `file` recipe_module |
| 112 def read(self, name, path, add_json_log=True, output_name=None, **kwargs): | 122 def read(self, name, path, add_json_log=True, output_name=None, **kwargs): |
| 113 """Returns a step that reads a JSON file.""" | 123 """Returns a step that reads a JSON file.""" |
| 114 return self.m.python.inline( | 124 return self.m.python.inline( |
| 115 name, | 125 name, |
| 116 """ | 126 """ |
| 117 import shutil | 127 import shutil |
| 118 import sys | 128 import sys |
| 119 shutil.copy(sys.argv[1], sys.argv[2]) | 129 shutil.copy(sys.argv[1], sys.argv[2]) |
| 120 """, | 130 """, |
| 121 args=[path, | 131 args=[path, |
| 122 self.output(add_json_log=add_json_log, name=output_name)], | 132 self.output(add_json_log=add_json_log, name=output_name)], |
| 123 add_python_log=False, | 133 add_python_log=False, |
| 124 **kwargs | 134 **kwargs |
| 125 ) | 135 ) |
| OLD | NEW |