blob: 0c963c7cd480d0e7b5de1f11fd65f2b3a8312b1e [file] [log] [blame] [view]
[email protected]4bd9dd02015-07-13 23:25:281# Recipes
2
[email protected]891a1082015-07-13 23:08:353Recipes are a domain-specific language (embedded in python) for specifying
4sequences of subprocess calls in a cross-platform and testable way.
5
[email protected]891a1082015-07-13 23:08:356[TOC]
7
[email protected]4bd9dd02015-07-13 23:25:288## Background
9
[email protected]891a1082015-07-13 23:08:3510Chromium uses BuildBot for its builds. It requires master restarts to change
11bot configs, which slows bot changes down.
12
13With Recipes, most build-related things happen in scripts that run on the
14slave, which means that the master does not need to be restarted in order
15to change something about a build configuration.
16
17Recipes also provide a way to unit test build scripts, by mocking commands and
18recording "expectations" of what will happen when the script runs under various
19conditions. This makes it easy to verify that the scope of a change is limited.
20
[email protected]4bd9dd02015-07-13 23:25:2821## Intro
[email protected]891a1082015-07-13 23:08:3522
[email protected]891a1082015-07-13 23:08:3523This README will seek to teach the ways of Recipes, so that you may do one or
24more 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
31The document will build knowledge up in small steps using examples, and so it's
32probably best to read the whole doc through from top to bottom once before using
33it as a reference.
34
[email protected]4bd9dd02015-07-13 23:25:2835## Small Beginnings
[email protected]891a1082015-07-13 23:08:3536
[email protected]891a1082015-07-13 23:08:3537**Recipes are a means to cause a series of commands to run on a machine.**
38
39All recipes take the form of a python file whose body looks like this:
40
41```python
whessee6cee892016-01-04 08:37:4142DEPS = ['recipe_engine/step']
[email protected]891a1082015-07-13 23:08:3543
44def RunSteps(api):
45 api.step('Print Hello World', ['echo', 'hello', 'world'])
46```
47
[email protected]7f414512015-08-14 23:02:0448The `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
50api 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
52machine. Using python libraries with OS side-effects is prohibited to enable
53testing.
[email protected]891a1082015-07-13 23:08:3554
55For these examples we will work out of the
56[tools/build](https://chromium.googlesource.com/chromium/tools/build/)
57repository.
58
59Put this in a file under `scripts/slave/recipes/hello.py`. You can then
60run this recipe by calling
61
whessee6cee892016-01-04 08:37:4162 $ scripts/slave/recipes.py run hello
[email protected]891a1082015-07-13 23:08:3563
64*** promo
65Note: every recipe execution (e.g. build on buildbot) emits
66a step log called `run_recipe` on the `setup_build` step which provides
67a precise invocation for `run_recipe.py` correlating exactly with the current
68recipe invocation. This is useful to locally repro a failing build without
69having to guess at the parameters to `run_recipe.py`.
70***
71
[email protected]4bd9dd02015-07-13 23:25:2872## We should probably test as we go...
73
[email protected]891a1082015-07-13 23:08:3574**All recipes MUST have corresponding tests, which achieve 100% code coverage.**
75
76So, we have our recipe. Let's add a test to it.
77
78```python
Eric Seidelb4c7b0c2016-01-15 18:59:3679DEPS = ['recipe_engine/step']
[email protected]891a1082015-07-13 23:08:3580
81def RunSteps(api):
82 api.step('Print Hello World', ['echo', 'hello', 'world'])
83
84def GenTests(api):
85 yield api.test('basic')
86```
87
88This causes a single test case to be generated, called 'basic', which has no
89input parameters. As your recipe becomes more complex, you'll need to add more
90tests to make sure that you maintain 100% code coverage.
91
92In order to run the tests, run
93
phajdan.jrfb848fd2017-04-18 19:10:0394 $ scripts/slave/recipes.py test train --filter hello
[email protected]891a1082015-07-13 23:08:3595
96This will write the file `build/scripts/slave/recipes/hello.expected/basic.json`
97summarizing the actions of the recipe under the boring conditions
98specified 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]4bd9dd02015-07-13 23:25:28112## Let's do something useful
[email protected]891a1082015-07-13 23:08:35113
114### Properties are the primary input for your recipes
115
116In order to do something useful, we need to pull in parameters from the outside
117world. There's one primary source of input for recipes, which is `properties`.
118
119Properties are a relic from the days of BuildBot, though they have been
120dressed up a bit to be more like we'll want them in the future. If you're
121familiar 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
123is provided by the `properties` api module.
124
martiniss0d478802016-01-27 20:30:53125This is now abstracted into the PROPERTIES top level declaration in your recipe.
126You declare a dictionary of properties that your recipe accepts. The recipe
127engine will extract the properties your recipe cares about from all the
128properties it knows about, and pass them as arguments to your RunSteps function.
129
130Let's see an example!
131
[email protected]891a1082015-07-13 23:08:35132```python
[email protected]7f414512015-08-14 23:02:04133from recipe_engine.recipe_api import Property
134
[email protected]891a1082015-07-13 23:08:35135DEPS = [
[email protected]891a1082015-07-13 23:08:35136 'step',
Stephen Martinisb4b5fc32015-09-18 22:57:54137 'properties',
[email protected]891a1082015-07-13 23:08:35138]
139
[email protected]7f414512015-08-14 23:02:04140PROPERTIES = {
141 'target_of_admiration': Property(
[email protected]cdc2e792015-08-27 23:49:52142 kind=str, help="Who you love and adore.", default="Chrome Infra"),
[email protected]7f414512015-08-14 23:02:04143}
144
145def RunSteps(api, target_of_admiration):
[email protected]891a1082015-07-13 23:08:35146 verb = 'Hello, %s'
[email protected]7f414512015-08-14 23:02:04147 if target_of_admiration == 'DarthVader':
[email protected]891a1082015-07-13 23:08:35148 verb = 'Die in a fire, %s!'
[email protected]7f414512015-08-14 23:02:04149 api.step('Greet Admired Individual', ['echo', verb % target_of_admiration])
[email protected]891a1082015-07-13 23:08:35150
151def 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]7f414512015-08-14 23:02:04154 yield api.test('infra rocks')
[email protected]891a1082015-07-13 23:08:35155```
156
martiniss0d478802016-01-27 20:30:53157The property list is a whitelist, so if the properties provided as inputs to the
158current recipe run were
159
160```python
161{
162 'target_of_admiration': 'Darth Vader',
163 'some_other_chill_thing': 'so_chill',
164}
165```
166
167then the recipe wouldn't know about the other `some_other_chill_thing` property
168at all.
169
170Note that properties without a default are required. If you don't want a
iannucci09efd6f2017-06-01 22:33:55171property to be required, just add `default=None` to the definition.
martiniss0d478802016-01-27 20:30:53172
[email protected]891a1082015-07-13 23:08:35173Yes, elements of a test specification are combined with `+` and it's weird.
174
175To specify property values in a local run:
176
177 build/scripts/tools/run_recipe.py <recipe-name> opt=bob other=sally
178
179Or, more explicitly::
180
181 build/scripts/tools/run_recipe.py --properties-file <path/to/json>
182
183Where `<path/to/json>` is a file containing a valid json `object` (i.e.
184key:value pairs).
185
Stephen Martinisb4b5fc32015-09-18 22:57:54186Note that we need to put a dependency on the 'properties' module in the DEPS
187because we use it to generate our tests, even though we don't actually call
188the module in our code.
189See this [crbug.com/532275](bug) for more info.
190
[email protected]891a1082015-07-13 23:08:35191### Modules
192
193There are all sorts of helper modules. They are found in the `recipe_modules`
194directory alongside the `recipes` directory where the recipes go.
195
196Notice 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
198of DEPS, you'll get an AttributeError when you try to access them. The modules
199are located primarily in `recipe_modules/`, and their name is their folder name.
200
201There are a whole bunch of modules which provide really helpful tools. You
whessee6cee892016-01-04 08:37:41202should go take a look at them. `scripts/slave/recipes.py` is a
[email protected]891a1082015-07-13 23:08:35203pretty helpful tool. If you want to know more about properties, step and path, I
whessee6cee892016-01-04 08:37:41204would suggest starting with `scripts/slave/recipes.py doc`, and then delving
205into the helpful docstrings in those helpful modules.
[email protected]891a1082015-07-13 23:08:35206
[email protected]4bd9dd02015-07-13 23:25:28207## Making Modules
[email protected]891a1082015-07-13 23:08:35208
[email protected]891a1082015-07-13 23:08:35209**Modules are for grouping functionality together and exposing it across
210recipes.**
211
212So now you feel like you're pretty good at recipes, but you want to share your
213echo functionality across a couple recipes which all start the same way. To do
214this, you need to add a module directory.
215
216```
217recipe_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
229First add an `__init__.py` with DEPS:
230
231```python
232# recipe_modules/hello/__init__.py
[email protected]7f414512015-08-14 23:02:04233from recipe_api import Property
234
[email protected]891a1082015-07-13 23:08:35235DEPS = ['properties', 'step']
[email protected]7f414512015-08-14 23:02:04236PROPERTIES = {
237 'target_of_admiration': Property(default=None),
238}
[email protected]891a1082015-07-13 23:08:35239```
240
241And your api.py should look something like:
242
243```python
244from slave import recipe_api
245
246class HelloApi(recipe_api.RecipeApi):
[email protected]7f414512015-08-14 23:02:04247 def __init__(self, target_of_admiration):
248 self._target = target_of_admiration
249
250 def greet(self, default_verb=None):
[email protected]891a1082015-07-13 23:08:35251 verb = default_verb or 'Hello %s'
[email protected]7f414512015-08-14 23:02:04252 if self._target == 'DarthVader':
[email protected]891a1082015-07-13 23:08:35253 verb = 'Die in a fire %s!'
254 self.m.step('Hello World',
[email protected]7f414512015-08-14 23:02:04255 ['echo', verb % self._target])
[email protected]891a1082015-07-13 23:08:35256```
257
258Note that all the DEPS get injected into `self.m`. This logic is handled outside
259of 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
264And now, our refactored recipe:
265
266```python
267DEPS = ['hello']
268
269def RunSteps(api):
270 api.hello.greet()
271
272def 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]4bd9dd02015-07-13 23:25:28280## So how do I really write those tests?
[email protected]891a1082015-07-13 23:08:35281
[email protected]891a1082015-07-13 23:08:35282The basic form of tests is:
283
284```python
285def GenTests(api):
286 yield api.test('testname') + # other stuff
287```
288
289Some modules define interfaces for specifying necessary step data; these are
290injected into `api` from `DEPS` similarly to how it works for `RunSteps`. There
291are 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
298By default all simulated steps succeed, the platform is 64-bit linux, and
299there are no properties. The `api.properties.generic()` method populates some
300common properties for Chromium recipes.
301
302The `api` passed to GenTests is confusingly **NOT** the same as the recipe api.
303It's actually an instance of `recipe_test_api.py:RecipeTestApi()`. This is
304admittedly pretty weak, and it would be great to have the test api
305automatically created via modules. On the flip side, the test api is much less
306necessary than the recipe api, so this transformation has not been designed yet.
307
[email protected]4bd9dd02015-07-13 23:25:28308## What is that config business?
[email protected]891a1082015-07-13 23:08:35309
[email protected]891a1082015-07-13 23:08:35310**Configs are a way for a module to expose it's "global" state in a reusable
311way.**
312
313A common problem in Building Things is that you end up with an inordinantly
314large matrix of configurations. Let's take chromium, for example. Here is a
315sample 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
328Obviously there are a lot of combinations of those things, but only a relatively
329small number of *valid* combinations of those things. How can we represent all
330the valid states while still retaining our sanity?
331
332We begin by specifying a schema that configurations of the `hello` module
333will follow, and the config context based on it that we will add configuration
334items to.
335
336```python
337# recipe_modules/hello/config.py
338from slave.recipe_config import config_item_context, ConfigGroup
339from slave.recipe_config import SimpleConfig, StaticConfig, BadConf
340
341def 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
352config_ctx = config_item_context(BaseConfig)
353```
354
355The `BaseConfig` schema is expected to return a `ConfigGroup` instance of some
356sort. All the configs that you get out of this file will be a modified version
357of something returned by the schema method. The arguments should have sane
358defaults, and should be named in `ALL_CAPS` (this is to avoid argument name
359conflicts as we'll see later).
360
361`config_ctx` is the 'context' for all the config items in this file, and will
362magically become the `CONFIG_CTX` for the entire module. Other modules may
363extend this context, which we will get to later.
364
365Finally let's define some config items themselves. A config item is a function
366decorated with the `config_ctx`, and takes a config blob as 'c'. The config item
367updates the config blob, perhaps conditionally. There are many features to
368`slave/recipe_config.py`. I would recommend reading the docstrings there
369for 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)
376def 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.
383def 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'):
389def default_tool(c):
390 c.tool = 'echo'
391```
392
393Now that we have our config, let's use it.
394
395```python
396# recipe_modules/hello/api.py
397from slave import recipe_api
398
399class HelloApi(recipe_api.RecipeApi):
[email protected]7f414512015-08-14 23:02:04400 def __init__(self, target_of_admiration):
401 self._target = target_of_admiration
402
[email protected]891a1082015-07-13 23:08:35403 def get_config_defaults(self, _config_name):
[email protected]7f414512015-08-14 23:02:04404 return {'TARGET': self._target}
[email protected]891a1082015-07-13 23:08:35405
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
411Note that `recipe_api.RecipeApi` contains all the plumbing for dealing with
412configs. 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
414in one way or another. Also note that c is a 'public' variable, which means that
415recipes have direct access to the configuration state by `api.<modname>.c`.
416
417```python
418# recipes/hello.py
419DEPS = ['hello']
420def RunSteps(api):
421 api.hello.set_config('default_tool')
422 api.hello.greet() # Greets 'target_of_admiration' or 'Bob' with echo.
423
424def GenTests(api):
425 yield api.test('bob')
426 yield api.test('anya') + api.properties(target_of_admiration='anya')
427```
428
429Note the call to `set_config`. This method takes the configuration name
430specifed, 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
435We can also call `set_config` differently to get different results:
436
437```python
438# recipes/rainbow_hello.py
439DEPS = ['hello']
440def RunSteps(api):
441 api.hello.set_config('super_tool', TARGET='Charlie')
442 api.hello.greet() # Greets 'Charlie' with unicorn.py.
443
444def GenTests(api):
445 yield api.test('charlie')
446```
447
448```python
449# recipes/evil_hello.py
450DEPS = ['hello']
451def RunSteps(api):
452 api.hello.set_config('default_tool', TARGET='DarthVader')
453 api.hello.greet() # Causes 'DarthVader' to despair with echo
454
455def 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
462the chromium module, but it will also attempt to apply the `'blink'` config for
463all the dependencies, too. This way, you can have the chromium module extend the
464gclient config context with a 'blink' config item, and then `set_configs` will
465stack across all the relevent contexts. (This has since been recognized as a
466design mistake)
467
468`recipe_api.RecipeApi` also provides `make_config` and `apply_config`, which
469allow recipes more-direct access to the config items. However, `set_config()` is
470the most-preferred way to apply configurations.
471
[email protected]4bd9dd02015-07-13 23:25:28472## What about getting data back from a step?
[email protected]891a1082015-07-13 23:08:35473
[email protected]891a1082015-07-13 23:08:35474Consider this recipe:
475
476```python
477DEPS = ['step', 'path']
478
479def 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
488def 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
493See how we use `step_result` to get the result of the last step? The item we get
494back is a `recipe_engine.main.StepData` instance (really, just a basic object
495with 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
506This is pretty neat... However, it turns out that returncodes suck bigtime for
507communicating actual information. `api.json.output()` to the rescue!
508
509```python
510DEPS = ['step', 'path', 'step_history', 'json']
511
512def 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
524def 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
536added into a step command list. When the step runs, the placeholder gets
537rendered into some strings (in this case, like '/tmp/some392ra8'). When the step
538finishes, the Placeholder adds data to the `StepData` object for the step which
539just ran, namespaced by the module name (in this case, the 'json' module decided
540to add an 'output' attribute to the `step_history` item). I'd encourage you to
541take a peek at the implementation of the json module to see how this is
542implemented.
543
544### Example: write to standard input of a step
545
546```python
547api.step(..., stdin=api.raw_io.input('test input'))
548```
549
550Also see [raw_io's
iannucci09efd6f2017-06-01 22:33:55551example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/raw_io/examples/full.py).
[email protected]891a1082015-07-13 23:08:35552
553### Example: read standard output of a step as json
554
555```python
556step_result = api.step(..., stdout=api.json.output())
557data = step_result.stdout
558# data is a parsed JSON value, such as dict
559```
560
561Also see [json's
iannucci09efd6f2017-06-01 22:33:55562example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/json/examples/full.py).
[email protected]891a1082015-07-13 23:08:35563
564### Example: write to standard input of a step as json
565
566```python
567data = {'value': 1}
568api.step(..., stdin=api.json.input(data))
569```
570
571Also see [json's
iannucci09efd6f2017-06-01 22:33:55572example](https://chromium.googlesource.com/chromium/tools/build.git/+/master/scripts/slave/recipe_modules/json/examples/full.py).
[email protected]891a1082015-07-13 23:08:35573
574### Example: simulated step output
575
576This example specifies the standard output that should be returned when
577a step is executed in simulation mode. This is typically used for
578specifying default test data in the recipe or recipe module and removes
579the need to specify too much test data for each test in GenTests:
580
581```python
582api.step(..., step_test_data=api.raw_io.output('test data'))
583```
584
585### Example: simulated step output for a test case
586
587```python
588yield (
589 api.test('my_test') +
590 api.step_data(
591 'step_name',
592 output=api.raw_io.output('test data')))
593```
594
[email protected]4bd9dd02015-07-13 23:25:28595## How to change step presentation?
[email protected]891a1082015-07-13 23:08:35596
597`step_result.presentation` allows modifying the appearance of a step:
598
599### Logging
600
601```python
602step_result.presentation.logs['mylog'] = ['line1', 'line2']
603```
604
605Creates an extra log "mylog" under the step.
606
607### Setting properties
608
609`api.properties` are immutable, but you can change and add new
610properties at the buildbot level.
611
612```python
613step_result.presentation.properties['newprop'] = 1
614```
615
616### Example: step text
617
618This modifies the text displayed next to a step name:
619
620```python
621step_result = api.step(...)
622step_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]4bd9dd02015-07-13 23:25:28635## How do I know what modules to use?
[email protected]891a1082015-07-13 23:08:35636
whessee6cee892016-01-04 08:37:41637Use `scripts/slave/recipes.py doc`. It's super effective!
[email protected]891a1082015-07-13 23:08:35638
[email protected]4bd9dd02015-07-13 23:25:28639## How do I run those tests you were talking about?
[email protected]891a1082015-07-13 23:08:35640
phajdan.jr033f8112017-03-23 18:16:45641Each repo has a recipes.py entry point under `recipes_path` from `recipes.cfg` .
642
643Execute the following commands:
phajdan.jrfb848fd2017-04-18 19:10:03644`./recipes.py test run`
645`./recipes.py test train`
phajdan.jr033f8112017-03-23 18:16:45646
647Specifically, for `tools/build` repo, the commands to execute are:
phajdan.jrfb848fd2017-04-18 19:10:03648`scripts/slave/recipes.py test run`
649`scripts/slave/recipes.py test train`
[email protected]891a1082015-07-13 23:08:35650
[email protected]4bd9dd02015-07-13 23:25:28651## Where's the docs on `*.py`?
[email protected]891a1082015-07-13 23:08:35652
[email protected]891a1082015-07-13 23:08:35653Check the docstrings in `*.py`. `<trollface text="Problem?"/>`
654
iannucci09efd6f2017-06-01 22:33:55655In addition, most recipe modules have example recipes in the `examples`
656subfolder which exercises most of the code in the module for example purposes.