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

Side by Side Diff: scripts/slave/README.recipes.md

Issue 1151423002: Move recipe engine to third_party/recipe_engine. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Moved field_composer_test with its buddies Created 5 years, 6 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
« no previous file with comments | « scripts/common/unittests/annotator_test.py ('k') | scripts/slave/annotated_run.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 Recipes
2 =======
3 Recipes are a flexible way to specify How to Do Things, without knowing too much
4 about those Things.
5
6
7 Background
8 ----------
9
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 reduces the number of master restarts needed when changing build
15 configuration.
16
17 Intro
18 -----
19 This README will seek to teach the ways of Recipes, so that you may do one or
20 more of the following:
21
22 * Read them
23 * Make new recipes
24 * Fix bugs in recipes
25 * Create libraries (api modules) for others to use in their recipes.
26
27 The document will build knowledge up in small steps using examples, and so it's
28 probably best to read the whole doc through from top to bottom once before using
29 it as a reference.
30
31
32 Small Beginnings
33 ----------------
34 **Recipes are a means to cause a series of actions to occur on a machine.**
35
36 All recipes take the form of a python file whose body looks like this:
37
38 ```python
39 def GenSteps(api):
40 yield {
41 'name': 'Hello World',
42 'cmd': ['/b/build/echo.sh', 'hello', 'world']
43 }
44 ```
45
46 The GenSteps function is expected to take a single argument `api` (we'll get to
47 that in more detail later), and yield a series of 'stepish' items. A stepish
48 item can be:
49
50 * A single step (a dictionary as in the example above)
51 * A series (list or tuple) of stepish items
52 * A python-generator of stepish items
53
54
55 We should probably test as we go...
56 -----------------------------------
57 **All recipes MUST have corresponding tests, which achieve 100% code coverage.**
58
59 So, we have our recipe. Let's add a test to it.
60
61 ```python
62 def GenSteps(api):
63 yield {
64 'name': 'Hello World',
65 'cmd': ['/b/build/echo.sh', 'hello', 'world']
66 }
67
68 def GenTests(api):
69 yield 'basic', {}
70 ```
71
72 This causes a single test case to be generated, called 'basic', which has no
73 input parameters. As your recipe becomes more complex, you'll need to add more
74 tests to make sure that you maintain 100% code coverage.
75
76
77 Let's do something useful
78 -------------------------
79 **Properties is the primary input for your recipes.**
80
81 In order to do something useful, we need to pull in parameters from the outside
82 world. There's one primary source of input for recipes, which is `properties`.
83
84 `properties` are a relic from the days of BuildBot, though they have been
85 dressed up a bit to be more like we'll want them in the future. If you're
86 familiar with BuildBot, you'll probably know them as `factory_properties` and
87 `build_properties`. The new `properties` object is a merging of these two, and
88 is provided by the `properties` api module.
89
90 ```python
91 DEPS = ['properties']
92
93 def GenSteps(api):
94 verb = 'Hello, %s'
95 target = api.properties['target_of_admiration']
96 if target == 'DarthVader':
97 verb = 'Die in a fire, %s!'
98 yield {'name': 'Hello World', 'cmd': ['/b/build/echo.sh', verb % target]}
99
100 def GenTests(api):
101 yield 'basic', {
102 'properties': {'target_of_admiration': 'Bob'}
103 }
104
105 yield 'vader', {
106 'properties': {'target_of_admiration': 'DarthVader'}
107 }
108 ```
109
110 Ok, I lied. It wasn't very useful.
111
112
113 Let's make it a bit prettier
114 ----------------------------
115 **You can add modules to your recipe by simply adding them to DEPS.**
116
117 So there are all sorts of helper modules. I'll add in the 'step' and 'path'
118 modules here as an example.
119
120 ```python
121 DEPS = ['properties', 'step', 'path']
122
123 def GenSteps(api):
124 verb = 'Hello, %s'
125 target = api.properties['target_of_admiration']
126 if target == 'DarthVader':
127 verb = 'Die in a fire, %s!'
128 yield api.step('Hello World', [api.path['build'].join('echo.sh'),
129 verb % target])
130
131 def GenTests(api):
132 yield 'basic', {
133 'properties': {'target_of_admiration': 'Bob'}
134 }
135
136 yield 'vader', {
137 'properties': {'target_of_admiration': 'DarthVader'}
138 }
139 ```
140
141 Notice the `DEPS` line in the recipe. Any modules named by string in DEPS are
142 'injected' into the `api` parameter that your recipe gets. If you leave them out
143 of DEPS, you'll get an AttributeError when you try to access them. The modules
144 are located primarily in `recipe_modules/`, and their name is their folder name.
145
146 > The full list of module locations which get added are in `recipe_util.py` in
147 > the `MODULE_DIRS` variable.
148
149 There are a whole bunch of modules which provide really helpful tools. You
150 should go take a look at them. `show_me_the_modules.py` is a pretty helpful
151 tool. If you want to know more about properties, step and path, I would suggest
152 starting with `show_me_the_modules.py`, and then delving into the docstrings in
153 those modules.
154
155
156 Making Modules
157 --------------
158 **Modules are for grouping functionality together and exposing it across
159 recipes.**
160
161 So now you feel like you're pretty good at recipes, but you want to share your
162 echo functionality across a couple recipes which all start the same way. To do
163 this, you need to add a module directory.
164
165 ```
166 recipe_modules/
167 step/
168 properties/
169 path/
170 hello/
171 __init__.py # (Required) Contains optional `DEPS = list([other modules])`
172 api.py # (Required) Contains single required RecipeApi-derived class
173 config.py # (Optional) Contains configuration for your api
174 *_config.py # (Optional) These contain extensions to the configurations of
175 # your dependency APIs
176 ```
177
178 First add an `__init__.py` with DEPS:
179
180 ```python
181 # recipe_modules/hello/__init__.py
182 DEPS = ['properties', 'path', 'step']
183 ```
184
185 And your api.py should look something like:
186
187 ```python
188 from slave import recipe_api
189
190 class HelloApi(recipe_api.RecipeApi):
191 def greet(self, default_verb=None, target=None):
192 verb = default_verb or 'Hello %s'
193 target = target or self.m.properties['target_of_admiration']
194 if target == 'DarthVader':
195 verb = 'Die in a fire %s!'
196 yield self.m.step('Hello World',
197 [self.m.path.build('echo.sh'), verb % target])
198 ```
199
200 See that all the DEPS get injected into `self.m`. This logic is handled outside
201 of the object (i.e. not in `__init__`) in the loading function creatively named
202 `load_recipe_modules()`, which resides in `recipe_api.py`.
203
204 > Because dependencies are injected after module initialization, *you do not hav e
205 > access to injected modules in your APIs `__init__` method*!
206
207 And now, our refactored recipe:
208
209 ```python
210 DEPS = ['hello']
211
212 def GenSteps(api):
213 yield api.hello.greet()
214
215 def GenTests(api):
216 yield 'basic', {
217 'properties': {'target_of_admiration': 'Bob'}
218 }
219
220 yield 'vader', {
221 'properties': {'target_of_admiration': 'DarthVader'}
222 }
223 ```
224
225 > NOTE: all of the modules are also under code coverage, but you only need some
226 > test SOMEWHERE to cover each line.
227
228
229 So how do I really write those tests?
230 -------------------------------------
231 **Tests are yielded as a (name, test data) tuple.**
232
233 The basic form of tests is:
234 ```python
235 def GenTests(api):
236 yield 'testname', {
237 # Test data
238 }
239 ```
240
241 Test data can contain any of the following keys:
242 * *properties*: This represents the merged factory properties and build
243 properties which will show up as api.properties for the duration of the
244 test. This dictionary is in simple `{<prop name>: <prop value>}` form.
245 * *mock*: Some modules need to have their behavior altered before the recipe
246 starts. For example, you could mock which platform is being tested, or mock
247 which paths exist. This dictionary is in the form of `{<mod name>: <mod
248 data>}`. See module docstrings to see what they accept for mocks.
249 * *step_mocks*: This is a dictionary which defines the mock data for
250 various `recipe_api.Placeholder` objects. These are explained more in
251 a later section. This dictionary is in the form of `{<step name>: {<mod
252 name>: <mod data>}}`
253 * There is one 'special' mod name, which is '$R'. This module refers to the
254 return code of the step, and takes an integer. If it is missing, it is
255 assumed that the step succeeded with a retcode of 0.
256
257 The `api` passed to GenTests is confusingly **NOT** the same as the recipe api.
258 It's actually an instance of `recipe_test_api.py:RecipeTestApi()`. This is
259 admittedly pretty weak, and it would be great to have the test api
260 automatically created via modules. On the flip side, the test api is much less
261 necessary than the recipe api, so this transformation has not been designed yet.
262
263
264 What is that config business?
265 -----------------------------
266 **Configs are a way for a module to expose it's "global" state in a reusable
267 way.**
268
269 A common problem in Building Things is that you end up with an inordinantly
270 large matrix of configurations. Let's take chromium, for example. Here is a
271 sample list of axes of configuration which chromium needs to build and test:
272
273 * BUILD_CONFIG
274 * HOST_PLATFORM
275 * HOST_ARCH
276 * HOST_BITS
277 * TARGET_PLATFORM
278 * TARGET_ARCH
279 * TARGET_BITS
280 * builder type (ninja? msvs? xcodebuild?)
281 * compiler
282 * ...
283
284 Obviously there are a lot of combinations of those things, but only a relatively
285 small number of *valid* combinations of those things. How can we represent all
286 the valid states while still retaining our sanity?
287
288 ```python
289 # recipe_modules/hello/config.py
290 from slave.recipe_config import config_item_context, ConfigGroup
291 from slave.recipe_config import SimpleConfig, StaticConfig, BadConf
292
293 def BaseConfig(TARGET='Bob'):
294 # This is a schema for the 'config blobs' that the hello module deals with.
295 return ConfigGroup(
296 verb = SimpleConfig(str),
297 # A config blob is not complete() until all required entries have a value.
298 tool = SimpleConfig(str, required=True),
299 # Generally, your schema should take a series of CAPITAL args which will be
300 # set as StaticConfig data in the config blob.
301 TARGET = StaticConfig(str(TARGET)),
302 )
303
304 VAR_TEST_MAP = {
305 'TARGET': ('Bob', 'DarthVader', 'Charlie'),
306 }
307 config_ctx = config_item_context(BaseConfig, VAR_TEST_MAP, '%(TARGET)s')
308
309 # Each of these functions is a 'config item' in the context of config_ctx.
310
311 # is_root means that every config item will apply this item first.
312 @config_ctx(is_root=True)
313 def BASE(c):
314 if c.TARGET == 'DarthVader':
315 c.verb = 'Die in a fire, %s!'
316 else:
317 c.verb = 'Hello, %s'
318
319 @config_ctx(group='tool'): # items with the same group are mutually exclusive.
320 def super_tool(c):
321 if c.TARGET != 'Charlie':
322 raise BadConf('Can only use super tool for Charlie!')
323 c.tool = 'unicorn.py'
324
325 @config_ctx(group='tool'):
326 def default_tool(c):
327 c.tool = 'echo.sh'
328 ```
329
330 Ok, this config file looks a bit intimidating. Let's decompose it. The first
331 portion is the schema `BaseConfig`. This is expected to return a ConfigGroup
332 instance of some sort. All the configs that you get out of this file will be
333 a modified version something returned by the schema method. The arguments should
334 have sane defaults, and should be named in `ALL_CAPS` (this is to avoid argument
335 name conflicts as we'll see later).
336
337 The `VAR_TEST_MAP` is a mapping of argument name for the schema (in this case,
338 just 'TARGET'), to a sequence of values to test. The test harness will
339 automatically generate the product of all arguments in this map, and will run
340 through each config function in the file to generate the expected config blob
341 for each. In this config, it would essentially be:
342
343 ```python
344 for TARGET in ('Bob', 'DarthVader', 'Charlie'):
345 for test_function in (super_tool, default_tool):
346 yield TestCase(test_function(BasicSchema(TARGET)))
347 ```
348
349 Next, we get to the `config_ctx`. This is the 'context' for all the config
350 items in this file, and will become the `CONFIG_CTX` for the entire module.
351 Other modules may add into this config context (for example, they could have
352 a `hello_config.py` file, which imports this config context like
353
354 ```python
355 import DEPS
356 CONFIG_CTX = DEPS['hello'].CONFIG_CTX
357 ```
358
359
360 This will be useful for separation of concerns with the `set_config()`
361 method.). The string format argument that `config_item_context` takes will be
362 used to format the test case names and test expectation file names. Not
363 terribly useful here, but it can be useful for making the test names more
364 obvious in more complex cases.
365
366 Finally we get to the config items themselves. A config item is a function
367 decorated with the `config_ctx`, and takes a config blob as 'c'. The config item
368 updates the config blob, perhaps conditionally. There are many features to
369 `slave/recipe_config.py`. I would recommend reading the docstrings there
370 for all the details.
371
372 Now that we have our config, let's use it.
373
374 ```python
375 # recipe_modules/hello/api.py
376 from slave import recipe_api
377
378 class HelloApi(recipe_api.RecipeApi):
379 def get_config_defaults(self, _config_name):
380 return {'TARGET': self.m.properties['target_of_admiration']}
381
382 def greet(self):
383 yield self.m.step('Hello World', [
384 self.m.path.build(self.c.tool), self.c.verb % self.c.TARGET])
385 ```
386
387 Note that `recipe_api.RecipeApi` contains all the plumbing for dealing with
388 configs. If your module has a config, you can access its current value via
389 `self.c`. The users of your module (read: recipes) will need to set this value
390 in one way or another. Also note that c is a 'public' variable, which means that
391 recipes have direct access to the configuration state by `api.<modname>.c`.
392
393 ```python
394 # recipes/hello.py
395 DEPS = ['hello']
396 def GenSteps(api):
397 api.hello.set_config('default_tool')
398 yield api.hello.greet() # Greets 'target_of_admiration' or 'Bob' with echo.sh .
399
400 def GenTests(api):
401 yield 'bob', {}
402 yield 'anya', {'properties': {'target_of_admiration': 'anya'}}
403 ```
404
405 Note the call to `set_config`. This method takes the configuration name
406 specifed, finds it in the given module (`'hello'` in this case), and sets
407 `api.hello.c` equal to the result of invoking the named config item
408 (`'default_tool'`) with the default configuration (the result of calling
409 `get_config_defaults`), merged over the static defaults specified by the schema.
410 In this case, the schema will be initialized by essentially the following calls:
411
412 ```python
413 raw = BaseConfig(**api.hello.get_config_defaults())
414 api.hello.c = default_tool(BASE(raw))
415 ```
416
417 We can also call `set_config` differently to get different results:
418
419 ```python
420 # recipes/rainbow_hello.py
421 DEPS = ['hello']
422 def GenSteps(api):
423 api.hello.set_config('super_tool', TARGET='Charlie')
424 yield api.hello.greet() # Greets 'Charlie' with unicorn.py.
425
426 def GenTests(api):
427 yield 'charlie', {}
428 ```
429
430 ```python
431 # recipes/evil_hello.py
432 DEPS = ['hello']
433 def GenSteps(api):
434 api.hello.set_config('default_tool', TARGET='DarthVader')
435 yield api.hello.greet() # Causes 'DarthVader' to despair with echo.sh
436
437 def GenTests(api):
438 yield 'darth', {}
439 ```
440
441 `set_config()` also has one additional bit of magic. If a module (say,
442 chromium), depends on some other modules (say, gclient), if you do
443 `api.chromium.set_config('blink')`, it will apply the 'blink' config item from
444 the chromium module, but it will also attempt to apply the 'blink' config for
445 all the dependencies, too. This way, you can have the chromium module extend the
446 gclient config context with a 'blink' config item, and then set_configs will
447 stack across all the relevent contexts.
448
449 `recipe_api.RecipeApi` also provides `make_config` and `apply_config`, which
450 allow recipes more-direct access to the config items. However, `set_config()` is
451 the most-preferred way to apply configurations.
452
453
454 What about getting data back from a step?
455 -----------------------------------------
456 **If you need you recipe to be conditional on something that a step does, you'll
457 need to make use of the `step_history` api.**
458
459 Consider this recipe:
460 ```python
461 DEPS = ['step', 'path', 'step_history']
462
463 def GenSteps(api):
464 yield api.step('Determine blue moon', [api.path['build'].join('is_blue_moon.sh ')])
465 if api.step_history.last_step().retcode == 0:
466 yield api.step('HARLEM SHAKE!', [api.path['build'].join('do_the_harlem_shake .sh')])
467 else:
468 yield api.step('Boring', [api.path['build'].join('its_a_small_world.sh')])
469
470 def GenTests(api):
471 yield 'harlem', {
472 'step_mocks': {'Determine blue moon': {'$R': 0}}
473 }
474 yield 'boring', {
475 'step_mocks': {'Determine blue moon': {'$R': 1}}
476 }
477 ```
478
479 See how we use `step_history` to get the result of the last step? The item we
480 get back is an `annotated_run.RecipeData` instance (really, just a basic object
481 with member data). The members of this object which are guaranteed to exist are:
482 * retcode: Pretty much what you think
483 * step: The actual step json which was sent to `annotator.py`. Not usually
484 useful for recipes, but it is used internally for the recipe tests
485 framework.
486
487 This is pretty neat... However, it turns out that returncodes suck bigtime for
488 communicating actual information. `api.json.output()` to the rescue!
489
490 ```python
491 DEPS = ['step', 'path', 'step_history', 'json']
492
493 def GenSteps(api):
494 yield api.step('run tests', [
495 api.path['build'].join('do_test_things.sh'), api.json.output()])
496 num_passed = api.step_history.last_step().json.output['num_passed']
497 if num_passed > 500:
498 yield api.step('victory', [api.path['build'].join('do_a_dance.sh')])
499 elif num_passed > 200:
500 yield api.step('not defeated', [api.path['build'].join('woohoo.sh')])
501 else:
502 yield api.step('deads!', [api.path['build'].join('you_r_deads.sh')])
503
504 def GenTests(api):
505 yield 'winning', {
506 'step_mocks': {'run tests': {'json': {'output': {'num_passed': 791}}}}
507 }
508 yield 'not_dead_yet', {
509 'step_mocks': {'run tests': {'json': {'output': {'num_passed': 302}}}}
510 }
511 yield 'nooooo', {
512 'step_mocks': {'run tests': {'json': {'output': {'num_passed': 10}}}}
513 }
514 ```
515
516 How does THAT work!?
517
518 `api.json.output()` returns a `recipe_api.Placeholder` which is meant to be
519 added into a step command list. When the step runs, the placeholder gets
520 rendered into some strings (in this case, like `['--output-json',
521 '/tmp/some392ra8'`]). When the step finishes, the Placeholder is allowed to add
522 data to the step history for the step which just ran, namespaced by the module
523 name (in this case, the 'json' module decided to add an 'output' attribute to
524 the `step_history` item). I'd encourage you to take a peek at the implementation
525 of the json module to see how this is implemented.
526
527
528 How do I know what modules to use?
529 ----------------------------------
530 Use `tools/show_me_the_modules.py`. It's super effective!
531
532
533 How do I run those tests you were talking about?
534 ------------------------------------------------
535 To test all the recipes/apis, use `slave/unittests/recipe_simulation_test.py`.
536 To set new expectations `slave/unittests/recipe_simulation_test.py train`.
537
538
539
540 Where's the docs on `*.py`?
541 --------------------------------------------
542 Check the docstrings in `*.py`. `<trollface text="Problem?"/>`
543
544 In addition, most recipe modules have an `example.py` file which exercises most
545 of the code in the module for both test coverage and example purposes.
546
547 If you want to know what keys a step dictionary can take, take a look at
548 `common/annotator.py`.
549
OLDNEW
« no previous file with comments | « scripts/common/unittests/annotator_test.py ('k') | scripts/slave/annotated_run.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698