[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 1 | # Recipes |
| 2 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 3 | Recipes are a domain-specific language (embedded in python) for specifying |
| 4 | sequences of subprocess calls in a cross-platform and testable way. |
| 5 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 6 | [TOC] |
| 7 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 8 | ## Background |
| 9 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 10 | Chromium uses BuildBot for its builds. It requires master restarts to change |
| 11 | bot configs, which slows bot changes down. |
| 12 | |
| 13 | With Recipes, most build-related things happen in scripts that run on the |
| 14 | slave, which means that the master does not need to be restarted in order |
| 15 | to change something about a build configuration. |
| 16 | |
| 17 | Recipes also provide a way to unit test build scripts, by mocking commands and |
| 18 | recording "expectations" of what will happen when the script runs under various |
| 19 | conditions. This makes it easy to verify that the scope of a change is limited. |
| 20 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 21 | ## Intro |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 22 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 23 | This README will seek to teach the ways of Recipes, so that you may do one or |
| 24 | more of the following: |
| 25 | |
| 26 | * Read them |
| 27 | * Make new recipes |
| 28 | * Fix bugs in recipes |
| 29 | * Create libraries (api modules) for others to use in their recipes. |
| 30 | |
| 31 | The document will build knowledge up in small steps using examples, and so it's |
| 32 | probably best to read the whole doc through from top to bottom once before using |
| 33 | it as a reference. |
| 34 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 35 | ## Small Beginnings |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 36 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 37 | **Recipes are a means to cause a series of commands to run on a machine.** |
| 38 | |
| 39 | All recipes take the form of a python file whose body looks like this: |
| 40 | |
| 41 | ```python |
whesse | e6cee89 | 2016-01-04 08:37:41 | [diff] [blame] | 42 | DEPS = ['recipe_engine/step'] |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 43 | |
| 44 | def RunSteps(api): |
| 45 | api.step('Print Hello World', ['echo', 'hello', 'world']) |
| 46 | ``` |
| 47 | |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 48 | The `RunSteps` function is expected to take at least a single argument `api` |
| 49 | (we'll get to that in more detail later), and run a series of steps by calling |
| 50 | api functions. All of these functions will eventually make calls to |
| 51 | `api.step()`, which is the only way to actually get anything done on the |
| 52 | machine. Using python libraries with OS side-effects is prohibited to enable |
| 53 | testing. |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 54 | |
| 55 | For these examples we will work out of the |
| 56 | [tools/build](https://chromium.googlesource.com/chromium/tools/build/) |
| 57 | repository. |
| 58 | |
| 59 | Put this in a file under `scripts/slave/recipes/hello.py`. You can then |
| 60 | run this recipe by calling |
| 61 | |
whesse | e6cee89 | 2016-01-04 08:37:41 | [diff] [blame] | 62 | $ scripts/slave/recipes.py run hello |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 63 | |
| 64 | *** promo |
| 65 | Note: every recipe execution (e.g. build on buildbot) emits |
| 66 | a step log called `run_recipe` on the `setup_build` step which provides |
| 67 | a precise invocation for `run_recipe.py` correlating exactly with the current |
| 68 | recipe invocation. This is useful to locally repro a failing build without |
| 69 | having to guess at the parameters to `run_recipe.py`. |
| 70 | *** |
| 71 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 72 | ## We should probably test as we go... |
| 73 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 74 | **All recipes MUST have corresponding tests, which achieve 100% code coverage.** |
| 75 | |
| 76 | So, we have our recipe. Let's add a test to it. |
| 77 | |
| 78 | ```python |
Eric Seidel | b4c7b0c | 2016-01-15 18:59:36 | [diff] [blame] | 79 | DEPS = ['recipe_engine/step'] |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 80 | |
| 81 | def RunSteps(api): |
| 82 | api.step('Print Hello World', ['echo', 'hello', 'world']) |
| 83 | |
| 84 | def GenTests(api): |
| 85 | yield api.test('basic') |
| 86 | ``` |
| 87 | |
| 88 | This causes a single test case to be generated, called 'basic', which has no |
| 89 | input parameters. As your recipe becomes more complex, you'll need to add more |
| 90 | tests to make sure that you maintain 100% code coverage. |
| 91 | |
| 92 | In order to run the tests, run |
| 93 | |
phajdan.jr | fb848fd | 2017-04-18 19:10:03 | [diff] [blame] | 94 | $ scripts/slave/recipes.py test train --filter hello |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 95 | |
| 96 | This will write the file `build/scripts/slave/recipes/hello.expected/basic.json` |
| 97 | summarizing the actions of the recipe under the boring conditions |
| 98 | specified by `api.test('basic')`. |
| 99 | |
| 100 | [ |
| 101 | { |
| 102 | "cmd": [ |
| 103 | "echo", |
| 104 | "hello", |
| 105 | "world" |
| 106 | ], |
| 107 | "cwd": "[SLAVE_BUILD]", |
| 108 | "name": "Print Hello World" |
| 109 | } |
| 110 | ] |
| 111 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 112 | ## Let's do something useful |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 113 | |
| 114 | ### Properties are the primary input for your recipes |
| 115 | |
| 116 | In order to do something useful, we need to pull in parameters from the outside |
| 117 | world. There's one primary source of input for recipes, which is `properties`. |
| 118 | |
| 119 | Properties are a relic from the days of BuildBot, though they have been |
| 120 | dressed up a bit to be more like we'll want them in the future. If you're |
| 121 | familiar with BuildBot, you'll probably know them as `factory_properties` and |
| 122 | `build_properties`. The new `properties` object is a merging of these two, and |
| 123 | is provided by the `properties` api module. |
| 124 | |
martiniss | 0d47880 | 2016-01-27 20:30:53 | [diff] [blame] | 125 | This is now abstracted into the PROPERTIES top level declaration in your recipe. |
| 126 | You declare a dictionary of properties that your recipe accepts. The recipe |
| 127 | engine will extract the properties your recipe cares about from all the |
| 128 | properties it knows about, and pass them as arguments to your RunSteps function. |
| 129 | |
| 130 | Let's see an example! |
| 131 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 132 | ```python |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 133 | from recipe_engine.recipe_api import Property |
| 134 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 135 | DEPS = [ |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 136 | 'step', |
Stephen Martinis | b4b5fc3 | 2015-09-18 22:57:54 | [diff] [blame] | 137 | 'properties', |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 138 | ] |
| 139 | |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 140 | PROPERTIES = { |
| 141 | 'target_of_admiration': Property( |
[email protected] | cdc2e79 | 2015-08-27 23:49:52 | [diff] [blame] | 142 | kind=str, help="Who you love and adore.", default="Chrome Infra"), |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 143 | } |
| 144 | |
| 145 | def RunSteps(api, target_of_admiration): |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 146 | verb = 'Hello, %s' |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 147 | if target_of_admiration == 'DarthVader': |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 148 | verb = 'Die in a fire, %s!' |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 149 | api.step('Greet Admired Individual', ['echo', verb % target_of_admiration]) |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 150 | |
| 151 | def GenTests(api): |
| 152 | yield api.test('basic') + api.properties(target_of_admiration='Bob') |
| 153 | yield api.test('vader') + api.properties(target_of_admiration='DarthVader') |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 154 | yield api.test('infra rocks') |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 155 | ``` |
| 156 | |
martiniss | 0d47880 | 2016-01-27 20:30:53 | [diff] [blame] | 157 | The property list is a whitelist, so if the properties provided as inputs to the |
| 158 | current recipe run were |
| 159 | |
| 160 | ```python |
| 161 | { |
| 162 | 'target_of_admiration': 'Darth Vader', |
| 163 | 'some_other_chill_thing': 'so_chill', |
| 164 | } |
| 165 | ``` |
| 166 | |
| 167 | then the recipe wouldn't know about the other `some_other_chill_thing` property |
| 168 | at all. |
| 169 | |
| 170 | Note that properties without a default are required. If you don't want a |
iannucci | 09efd6f | 2017-06-01 22:33:55 | [diff] [blame] | 171 | property to be required, just add `default=None` to the definition. |
martiniss | 0d47880 | 2016-01-27 20:30:53 | [diff] [blame] | 172 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 173 | Yes, elements of a test specification are combined with `+` and it's weird. |
| 174 | |
| 175 | To specify property values in a local run: |
| 176 | |
| 177 | build/scripts/tools/run_recipe.py <recipe-name> opt=bob other=sally |
| 178 | |
| 179 | Or, more explicitly:: |
| 180 | |
| 181 | build/scripts/tools/run_recipe.py --properties-file <path/to/json> |
| 182 | |
| 183 | Where `<path/to/json>` is a file containing a valid json `object` (i.e. |
| 184 | key:value pairs). |
| 185 | |
Stephen Martinis | b4b5fc3 | 2015-09-18 22:57:54 | [diff] [blame] | 186 | Note that we need to put a dependency on the 'properties' module in the DEPS |
| 187 | because we use it to generate our tests, even though we don't actually call |
| 188 | the module in our code. |
| 189 | See this [crbug.com/532275](bug) for more info. |
| 190 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 191 | ### Modules |
| 192 | |
| 193 | There are all sorts of helper modules. They are found in the `recipe_modules` |
| 194 | directory alongside the `recipes` directory where the recipes go. |
| 195 | |
| 196 | Notice the `DEPS` line in the recipe. Any modules named by string in DEPS are |
| 197 | 'injected' into the `api` parameter that your recipe gets. If you leave them out |
| 198 | of DEPS, you'll get an AttributeError when you try to access them. The modules |
| 199 | are located primarily in `recipe_modules/`, and their name is their folder name. |
| 200 | |
| 201 | There are a whole bunch of modules which provide really helpful tools. You |
whesse | e6cee89 | 2016-01-04 08:37:41 | [diff] [blame] | 202 | should go take a look at them. `scripts/slave/recipes.py` is a |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 203 | pretty helpful tool. If you want to know more about properties, step and path, I |
whesse | e6cee89 | 2016-01-04 08:37:41 | [diff] [blame] | 204 | would suggest starting with `scripts/slave/recipes.py doc`, and then delving |
| 205 | into the helpful docstrings in those helpful modules. |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 206 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 207 | ## Making Modules |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 208 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 209 | **Modules are for grouping functionality together and exposing it across |
| 210 | recipes.** |
| 211 | |
| 212 | So now you feel like you're pretty good at recipes, but you want to share your |
| 213 | echo functionality across a couple recipes which all start the same way. To do |
| 214 | this, you need to add a module directory. |
| 215 | |
| 216 | ``` |
| 217 | recipe_modules/ |
| 218 | step/ |
| 219 | properties/ |
| 220 | path/ |
| 221 | hello/ |
| 222 | __init__.py # (Required) Contains optional `DEPS = list([other modules])` |
| 223 | api.py # (Required) Contains single required RecipeApi-derived class |
| 224 | config.py # (Optional) Contains configuration for your api |
| 225 | *_config.py # (Optional) These contain extensions to the configurations of |
| 226 | # your dependency APIs |
| 227 | ``` |
| 228 | |
| 229 | First add an `__init__.py` with DEPS: |
| 230 | |
| 231 | ```python |
| 232 | # recipe_modules/hello/__init__.py |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 233 | from recipe_api import Property |
| 234 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 235 | DEPS = ['properties', 'step'] |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 236 | PROPERTIES = { |
| 237 | 'target_of_admiration': Property(default=None), |
| 238 | } |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 239 | ``` |
| 240 | |
| 241 | And your api.py should look something like: |
| 242 | |
| 243 | ```python |
| 244 | from slave import recipe_api |
| 245 | |
| 246 | class HelloApi(recipe_api.RecipeApi): |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 247 | def __init__(self, target_of_admiration): |
| 248 | self._target = target_of_admiration |
| 249 | |
| 250 | def greet(self, default_verb=None): |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 251 | verb = default_verb or 'Hello %s' |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 252 | if self._target == 'DarthVader': |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 253 | verb = 'Die in a fire %s!' |
| 254 | self.m.step('Hello World', |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 255 | ['echo', verb % self._target]) |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 256 | ``` |
| 257 | |
| 258 | Note that all the DEPS get injected into `self.m`. This logic is handled outside |
| 259 | of the object (i.e. not in `__init__`). |
| 260 | |
| 261 | > Because dependencies are injected after module initialization, *you do not |
| 262 | > have access to injected modules in your APIs `__init__` method*! |
| 263 | |
| 264 | And now, our refactored recipe: |
| 265 | |
| 266 | ```python |
| 267 | DEPS = ['hello'] |
| 268 | |
| 269 | def RunSteps(api): |
| 270 | api.hello.greet() |
| 271 | |
| 272 | def GenTests(api): |
| 273 | yield api.test('basic') + api.properties(target_of_admiration='Bob') |
| 274 | yield api.test('vader') + api.properties(target_of_admiration='DarthVader') |
| 275 | ``` |
| 276 | |
| 277 | > NOTE: all of the modules are also require 100% code coverage, but you only |
| 278 | > need coverage from SOME recipe. |
| 279 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 280 | ## So how do I really write those tests? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 281 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 282 | The basic form of tests is: |
| 283 | |
| 284 | ```python |
| 285 | def GenTests(api): |
| 286 | yield api.test('testname') + # other stuff |
| 287 | ``` |
| 288 | |
| 289 | Some modules define interfaces for specifying necessary step data; these are |
| 290 | injected into `api` from `DEPS` similarly to how it works for `RunSteps`. There |
| 291 | are a few other methods available to `GenTests`'s `api`. Common ones include: |
| 292 | |
| 293 | * `api.properties(buildername='foo_builder')` sets properties as we have seen. |
| 294 | * `api.platform('linux', 32)` sets the mock platform to 32-bit linux. |
| 295 | * `api.step_data('Hello World', retcode=1)` mocks the `'Hello World'` step |
| 296 | to have failed with exit code 1. |
| 297 | |
| 298 | By default all simulated steps succeed, the platform is 64-bit linux, and |
| 299 | there are no properties. The `api.properties.generic()` method populates some |
| 300 | common properties for Chromium recipes. |
| 301 | |
| 302 | The `api` passed to GenTests is confusingly **NOT** the same as the recipe api. |
| 303 | It's actually an instance of `recipe_test_api.py:RecipeTestApi()`. This is |
| 304 | admittedly pretty weak, and it would be great to have the test api |
| 305 | automatically created via modules. On the flip side, the test api is much less |
| 306 | necessary than the recipe api, so this transformation has not been designed yet. |
| 307 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 308 | ## What is that config business? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 309 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 310 | **Configs are a way for a module to expose it's "global" state in a reusable |
| 311 | way.** |
| 312 | |
| 313 | A common problem in Building Things is that you end up with an inordinantly |
| 314 | large matrix of configurations. Let's take chromium, for example. Here is a |
| 315 | sample list of axes of configuration which chromium needs to build and test: |
| 316 | |
| 317 | * BUILD_CONFIG |
| 318 | * HOST_PLATFORM |
| 319 | * HOST_ARCH |
| 320 | * HOST_BITS |
| 321 | * TARGET_PLATFORM |
| 322 | * TARGET_ARCH |
| 323 | * TARGET_BITS |
| 324 | * builder type (ninja? msvs? xcodebuild?) |
| 325 | * compiler |
| 326 | * ... |
| 327 | |
| 328 | Obviously there are a lot of combinations of those things, but only a relatively |
| 329 | small number of *valid* combinations of those things. How can we represent all |
| 330 | the valid states while still retaining our sanity? |
| 331 | |
| 332 | We begin by specifying a schema that configurations of the `hello` module |
| 333 | will follow, and the config context based on it that we will add configuration |
| 334 | items to. |
| 335 | |
| 336 | ```python |
| 337 | # recipe_modules/hello/config.py |
| 338 | from slave.recipe_config import config_item_context, ConfigGroup |
| 339 | from slave.recipe_config import SimpleConfig, StaticConfig, BadConf |
| 340 | |
| 341 | def BaseConfig(TARGET='Bob'): |
| 342 | # This is a schema for the 'config blobs' that the hello module deals with. |
| 343 | return ConfigGroup( |
| 344 | verb = SimpleConfig(str), |
| 345 | # A config blob is not complete() until all required entries have a value. |
| 346 | tool = SimpleConfig(str, required=True), |
| 347 | # Generally, your schema should take a series of CAPITAL args which will be |
| 348 | # set as StaticConfig data in the config blob. |
| 349 | TARGET = StaticConfig(str(TARGET)), |
| 350 | ) |
| 351 | |
| 352 | config_ctx = config_item_context(BaseConfig) |
| 353 | ``` |
| 354 | |
| 355 | The `BaseConfig` schema is expected to return a `ConfigGroup` instance of some |
| 356 | sort. All the configs that you get out of this file will be a modified version |
| 357 | of something returned by the schema method. The arguments should have sane |
| 358 | defaults, and should be named in `ALL_CAPS` (this is to avoid argument name |
| 359 | conflicts as we'll see later). |
| 360 | |
| 361 | `config_ctx` is the 'context' for all the config items in this file, and will |
| 362 | magically become the `CONFIG_CTX` for the entire module. Other modules may |
| 363 | extend this context, which we will get to later. |
| 364 | |
| 365 | Finally let's define some config items themselves. A config item is a function |
| 366 | decorated with the `config_ctx`, and takes a config blob as 'c'. The config item |
| 367 | updates the config blob, perhaps conditionally. There are many features to |
| 368 | `slave/recipe_config.py`. I would recommend reading the docstrings there |
| 369 | for all the details. |
| 370 | |
| 371 | ```python |
| 372 | # Each of these functions is a 'config item' in the context of config_ctx. |
| 373 | |
| 374 | # is_root means that every config item will apply this item first. |
| 375 | @config_ctx(is_root=True) |
| 376 | def BASE(c): |
| 377 | if c.TARGET == 'DarthVader': |
| 378 | c.verb = 'Die in a fire, %s!' |
| 379 | else: |
| 380 | c.verb = 'Hello, %s' |
| 381 | |
| 382 | @config_ctx(group='tool'): # items with the same group are mutually exclusive. |
| 383 | def super_tool(c): |
| 384 | if c.TARGET != 'Charlie': |
| 385 | raise BadConf('Can only use super tool for Charlie!') |
| 386 | c.tool = 'unicorn.py' |
| 387 | |
| 388 | @config_ctx(group='tool'): |
| 389 | def default_tool(c): |
| 390 | c.tool = 'echo' |
| 391 | ``` |
| 392 | |
| 393 | Now that we have our config, let's use it. |
| 394 | |
| 395 | ```python |
| 396 | # recipe_modules/hello/api.py |
| 397 | from slave import recipe_api |
| 398 | |
| 399 | class HelloApi(recipe_api.RecipeApi): |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 400 | def __init__(self, target_of_admiration): |
| 401 | self._target = target_of_admiration |
| 402 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 403 | def get_config_defaults(self, _config_name): |
[email protected] | 7f41451 | 2015-08-14 23:02:04 | [diff] [blame] | 404 | return {'TARGET': self._target} |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 405 | |
| 406 | def greet(self): |
| 407 | self.m.step('Hello World', [ |
| 408 | self.m.path.build(self.c.tool), self.c.verb % self.c.TARGET]) |
| 409 | ``` |
| 410 | |
| 411 | Note that `recipe_api.RecipeApi` contains all the plumbing for dealing with |
| 412 | configs. If your module has a config, you can access its current value via |
| 413 | `self.c`. The users of your module (read: recipes) will need to set this value |
| 414 | in one way or another. Also note that c is a 'public' variable, which means that |
| 415 | recipes have direct access to the configuration state by `api.<modname>.c`. |
| 416 | |
| 417 | ```python |
| 418 | # recipes/hello.py |
| 419 | DEPS = ['hello'] |
| 420 | def RunSteps(api): |
| 421 | api.hello.set_config('default_tool') |
| 422 | api.hello.greet() # Greets 'target_of_admiration' or 'Bob' with echo. |
| 423 | |
| 424 | def GenTests(api): |
| 425 | yield api.test('bob') |
| 426 | yield api.test('anya') + api.properties(target_of_admiration='anya') |
| 427 | ``` |
| 428 | |
| 429 | Note the call to `set_config`. This method takes the configuration name |
| 430 | specifed, finds it in the given module (`'hello'` in this case), and sets |
| 431 | `api.hello.c` equal to the result of invoking the named config item |
| 432 | (`'default_tool'`) with the default configuration (the result of calling |
| 433 | `get_config_defaults`), merged over the static defaults specified by the schema. |
| 434 | |
| 435 | We can also call `set_config` differently to get different results: |
| 436 | |
| 437 | ```python |
| 438 | # recipes/rainbow_hello.py |
| 439 | DEPS = ['hello'] |
| 440 | def RunSteps(api): |
| 441 | api.hello.set_config('super_tool', TARGET='Charlie') |
| 442 | api.hello.greet() # Greets 'Charlie' with unicorn.py. |
| 443 | |
| 444 | def GenTests(api): |
| 445 | yield api.test('charlie') |
| 446 | ``` |
| 447 | |
| 448 | ```python |
| 449 | # recipes/evil_hello.py |
| 450 | DEPS = ['hello'] |
| 451 | def RunSteps(api): |
| 452 | api.hello.set_config('default_tool', TARGET='DarthVader') |
| 453 | api.hello.greet() # Causes 'DarthVader' to despair with echo |
| 454 | |
| 455 | def GenTests(api): |
| 456 | yield api.test('darth') |
| 457 | ``` |
| 458 | |
| 459 | `set_config()` also has one additional bit of magic. If a module (say, |
| 460 | `chromium`), depends on some other modules (say, `gclient`), if you do |
| 461 | `api.chromium.set_config('blink')`, it will apply the `'blink'` config item from |
| 462 | the chromium module, but it will also attempt to apply the `'blink'` config for |
| 463 | all the dependencies, too. This way, you can have the chromium module extend the |
| 464 | gclient config context with a 'blink' config item, and then `set_configs` will |
| 465 | stack across all the relevent contexts. (This has since been recognized as a |
| 466 | design mistake) |
| 467 | |
| 468 | `recipe_api.RecipeApi` also provides `make_config` and `apply_config`, which |
| 469 | allow recipes more-direct access to the config items. However, `set_config()` is |
| 470 | the most-preferred way to apply configurations. |
| 471 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 472 | ## What about getting data back from a step? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 473 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 474 | Consider this recipe: |
| 475 | |
| 476 | ```python |
| 477 | DEPS = ['step', 'path'] |
| 478 | |
| 479 | def RunSteps(api): |
| 480 | step_result = api.step('Determine blue moon', |
| 481 | [api.path['build'].join('is_blue_moon.sh')]) |
| 482 | |
| 483 | if step_result.retcode == 0: |
| 484 | api.step('HARLEM SHAKE!', [api.path['build'].join('do_the_harlem_shake.sh')]) |
| 485 | else: |
| 486 | api.step('Boring', [api.path['build'].join('its_a_small_world.sh')]) |
| 487 | |
| 488 | def GenTests(api): |
| 489 | yield api.test('harlem') + api.step_data('Determine blue moon', retcode=0) |
| 490 | yield api.test('boring') + api.step_data('Determine blue moon', retcode=1) |
| 491 | ``` |
| 492 | |
| 493 | See how we use `step_result` to get the result of the last step? The item we get |
| 494 | back is a `recipe_engine.main.StepData` instance (really, just a basic object |
| 495 | with member data). The members of this object which are guaranteed to exist are: |
| 496 | * `retcode`: Pretty much what you think |
| 497 | * `step`: The actual step json which was sent to `annotator.py`. Not usually |
| 498 | useful for recipes, but it is used internally for the recipe tests |
| 499 | framework. |
| 500 | * `presentation`: An object representing how the step will show up on the |
| 501 | build page, including its exit status, links, and extra log text. This is a |
| 502 | `recipe_engine.main.StepPresentation` object. |
| 503 | See also |
| 504 | [How to change step presentation](#how-to-change-step-presentation). |
| 505 | |
| 506 | This is pretty neat... However, it turns out that returncodes suck bigtime for |
| 507 | communicating actual information. `api.json.output()` to the rescue! |
| 508 | |
| 509 | ```python |
| 510 | DEPS = ['step', 'path', 'step_history', 'json'] |
| 511 | |
| 512 | def RunSteps(api): |
| 513 | step_result = api.step( |
| 514 | 'run tests', |
| 515 | [api.path['build'].join('do_test_things.sh'), api.json.output()]) |
| 516 | num_passed = step_result.json.output['num_passed'] |
| 517 | if num_passed > 500: |
| 518 | api.step('victory', [api.path['build'].join('do_a_dance.sh')]) |
| 519 | elif num_passed > 200: |
| 520 | api.step('not defeated', [api.path['build'].join('woohoo.sh')]) |
| 521 | else: |
| 522 | api.step('deads!', [api.path['build'].join('you_r_deads.sh')]) |
| 523 | |
| 524 | def GenTests(api): |
| 525 | yield (api.test('winning') + |
| 526 | api.step_data('run tests', api.json.output({'num_passed': 791})) |
| 527 | yield (api.test('not_dead_yet') + |
| 528 | api.step_data('run tests', api.json.output({'num_passed': 302})) |
| 529 | yield (api.test('noooooo') + |
| 530 | api.step_data('run tests', api.json.output({'num_passed': 10}))) |
| 531 | ``` |
| 532 | |
| 533 | ### How does THAT work!? |
| 534 | |
| 535 | `api.json.output()` returns a `recipe_api.Placeholder` which is meant to be |
| 536 | added into a step command list. When the step runs, the placeholder gets |
| 537 | rendered into some strings (in this case, like '/tmp/some392ra8'). When the step |
| 538 | finishes, the Placeholder adds data to the `StepData` object for the step which |
| 539 | just ran, namespaced by the module name (in this case, the 'json' module decided |
| 540 | to add an 'output' attribute to the `step_history` item). I'd encourage you to |
| 541 | take a peek at the implementation of the json module to see how this is |
| 542 | implemented. |
| 543 | |
| 544 | ### Example: write to standard input of a step |
| 545 | |
| 546 | ```python |
| 547 | api.step(..., stdin=api.raw_io.input('test input')) |
| 548 | ``` |
| 549 | |
| 550 | Also see [raw_io's |
iannucci | 09efd6f | 2017-06-01 22:33:55 | [diff] [blame] | 551 | example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/raw_io/examples/full.py). |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 552 | |
| 553 | ### Example: read standard output of a step as json |
| 554 | |
| 555 | ```python |
| 556 | step_result = api.step(..., stdout=api.json.output()) |
| 557 | data = step_result.stdout |
| 558 | # data is a parsed JSON value, such as dict |
| 559 | ``` |
| 560 | |
| 561 | Also see [json's |
iannucci | 09efd6f | 2017-06-01 22:33:55 | [diff] [blame] | 562 | example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/json/examples/full.py). |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 563 | |
| 564 | ### Example: write to standard input of a step as json |
| 565 | |
| 566 | ```python |
| 567 | data = {'value': 1} |
| 568 | api.step(..., stdin=api.json.input(data)) |
| 569 | ``` |
| 570 | |
| 571 | Also see [json's |
iannucci | 09efd6f | 2017-06-01 22:33:55 | [diff] [blame] | 572 | example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/json/examples/full.py). |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 573 | |
| 574 | ### Example: simulated step output |
| 575 | |
| 576 | This example specifies the standard output that should be returned when |
| 577 | a step is executed in simulation mode. This is typically used for |
| 578 | specifying default test data in the recipe or recipe module and removes |
| 579 | the need to specify too much test data for each test in GenTests: |
| 580 | |
| 581 | ```python |
| 582 | api.step(..., step_test_data=api.raw_io.output('test data')) |
| 583 | ``` |
| 584 | |
| 585 | ### Example: simulated step output for a test case |
| 586 | |
| 587 | ```python |
| 588 | yield ( |
| 589 | api.test('my_test') + |
| 590 | api.step_data( |
| 591 | 'step_name', |
| 592 | output=api.raw_io.output('test data'))) |
| 593 | ``` |
| 594 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 595 | ## How to change step presentation? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 596 | |
| 597 | `step_result.presentation` allows modifying the appearance of a step: |
| 598 | |
| 599 | ### Logging |
| 600 | |
| 601 | ```python |
| 602 | step_result.presentation.logs['mylog'] = ['line1', 'line2'] |
| 603 | ``` |
| 604 | |
| 605 | Creates an extra log "mylog" under the step. |
| 606 | |
| 607 | ### Setting properties |
| 608 | |
| 609 | `api.properties` are immutable, but you can change and add new |
| 610 | properties at the buildbot level. |
| 611 | |
| 612 | ```python |
| 613 | step_result.presentation.properties['newprop'] = 1 |
| 614 | ``` |
| 615 | |
| 616 | ### Example: step text |
| 617 | |
| 618 | This modifies the text displayed next to a step name: |
| 619 | |
| 620 | ```python |
| 621 | step_result = api.step(...) |
| 622 | step_result.presentation.step_text = 'Dynamic step result text' |
| 623 | ``` |
| 624 | |
| 625 | * `presentaton.logs` allows creating extra logs of a step run. Example: |
| 626 | ```python |
| 627 | step_result.presentation.logs['mylog'] = ['line1', 'line2'] |
| 628 | ``` |
| 629 | * presentation.properties allows changing and adding new properties at the |
| 630 | buildbot level. Example: |
| 631 | ```python |
| 632 | step_result.presentation.properties['newprop'] = 1 |
| 633 | ``` |
| 634 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 635 | ## How do I know what modules to use? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 636 | |
whesse | e6cee89 | 2016-01-04 08:37:41 | [diff] [blame] | 637 | Use `scripts/slave/recipes.py doc`. It's super effective! |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 638 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 639 | ## How do I run those tests you were talking about? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 640 | |
phajdan.jr | 033f811 | 2017-03-23 18:16:45 | [diff] [blame] | 641 | Each repo has a recipes.py entry point under `recipes_path` from `recipes.cfg` . |
| 642 | |
| 643 | Execute the following commands: |
phajdan.jr | fb848fd | 2017-04-18 19:10:03 | [diff] [blame] | 644 | `./recipes.py test run` |
| 645 | `./recipes.py test train` |
phajdan.jr | 033f811 | 2017-03-23 18:16:45 | [diff] [blame] | 646 | |
| 647 | Specifically, for `tools/build` repo, the commands to execute are: |
phajdan.jr | fb848fd | 2017-04-18 19:10:03 | [diff] [blame] | 648 | `scripts/slave/recipes.py test run` |
| 649 | `scripts/slave/recipes.py test train` |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 650 | |
[email protected] | 4bd9dd0 | 2015-07-13 23:25:28 | [diff] [blame] | 651 | ## Where's the docs on `*.py`? |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 652 | |
[email protected] | 891a108 | 2015-07-13 23:08:35 | [diff] [blame] | 653 | Check the docstrings in `*.py`. `<trollface text="Problem?"/>` |
| 654 | |
iannucci | 09efd6f | 2017-06-01 22:33:55 | [diff] [blame] | 655 | In addition, most recipe modules have example recipes in the `examples` |
| 656 | subfolder which exercises most of the code in the module for example purposes. |