Getting started : writing scripts in Crappy

In the following tutorials, you’re going to learn the very basics of writing scripts for running test protocols with Crappy. Only a beginner’s level in Python is required, don’t worry !

0. General concepts

This first section of the tutorials introduces the very basic concepts of Crappy. No code is involved for now, it only describes the general way data can flow in Crappy.

0.a. Blocks

In Crappy, even the most complex setups can be described with only two elements : the Blocks and the Links. The Blocks are responsible for managing data. There are many different types of Blocks, that all have a unique function. Some will acquire data, others transform it, or use it to drive hardware, etc. As the Blocks perform very specific tasks, a test script usually contains several Blocks (there is no upper limit). Blocks either take data as an input, or they output data, or both.

0.c. Labels

Data flowing between the Blocks through the Links is always labeled. Labels are simply names associated with a given stream of data. Let’s say that Block 1 outputs three data streams labeled 'time', 'Force', 'Position', and is linked with Block 2 that only takes two inputs. As we said, Block 2 is aware of all of Block 1’s outputs and thus needs a way to differentiate them. Thanks to labels, the user can simply specify in the arguments of Block 2 which labels to consider (for example only :py`’time’, ‘Position’). The data stream labeled :py:’Force’` will be lost to Block 2, but maybe Block 1 is also linked with a Block 3 that’s using it !

1. Understanding Crappy’s syntax

In this second part of the tutorials, we’re going to write step-by-step an actual script for Crappy that you can run locally on your machine ! All the following tutorials will also follow the same principle. If a script would not work as expected, please signal it to the developers (see the Troubleshooting page). Note that this first example script requires the matplotlib Python module to run.

The first thing to do when writing a script for Crappy is to open a new .py file. In this new file, you should start by importing the module Crappy :

# coding: utf-8

import crappy

Then, depending on the requirements of your experimental setup, you can add various Blocks and link them together. For this first example, let’s say that we want to acquire both the position and the force signal from a tensile test machine, plot the data against time and save it. So that everyone can run this first example without requiring any hardware, let’s use the Fake Machine Block instead of a real machine. To add this Block to the script, follow this syntax :

<chosen_name> = crappy.blocks.<Block_name>(<arguments>)

<chosen_name> is the name of the instance of the Block. There can be several instances of a same Block running simultaneously, for example several Grapher Blocks plotting different labels. <Block_name> is the exact name of the Block, as given in Crappy’s API. The possible <arguments> differ for every Block, the only way to know for sure is to refer to the documentation !

Note

To easily access the online documentation on a computer that has an internet access, simply type in a Python terminal :

>>> import crappy
>>> crappy.docs()

In the case of the Fake Machine Block, its description is given in the API at crappy.blocks.FakeMachine. As you can see, all its arguments are optional, and it outputs data over specific labels. Let’s still specify the cmd_label argument. The code now looks as follows :

# coding: utf-8

import crappy

if __name__ == '__main__':

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

Warning

If you’re not familiar with the if __name__ == '__main__': statement, you can find technical documentation here. Crappy might not run if you don’t wrap your code in this statement !

In addition to the Fake Machine, we also need a Recorder Block for saving the data, and two Grapher Blocks for plotting it. There will also be a Generator Block for driving the Fake Machine. The usage of the most used Blocks is detailed in the next section. In our specific example, the script could be as follows :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph_force = crappy.blocks.Grapher(('t(s)', 'F(N)'))

  graph_pos = crappy.blocks.Grapher(('t(s)', 'x(mm)'))

Now that all the Blocks are instantiated, they need to be linked together so that they can share data between each other. To link two Blocks together, simply add the following line to the script :

crappy.link(<block1>, <block2>)

Where <block1> and <block2> are the names you assigned to the instances of the Blocks. In the example, we need the Generator to drive the Fake Machine, and the Fake Machine has to transfer the data it acquired to both Graphers and to the Recorder Block. Here’s what the script becomes after adding the Links :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph_force = crappy.blocks.Grapher(('t(s)', 'F(N)'))

  graph_pos = crappy.blocks.Grapher(('t(s)', 'x(mm)'))

  crappy.link(gen, machine)

  crappy.link(machine, record)
  crappy.link(machine, graph_pos)
  crappy.link(machine, graph_force)

Let’s have a more detailed look at what the script is doing ! First, the Generator is generating a constant signal, that it sends to the Fake Machine over the label 'input_speed'. Obviously, this is the target speed at which the Fake Machine should operate for the fake tensile test, and its value is 5 mm/min. Notice the 'delay=40' condition, that indicates the Generator Block to stop the test after 40s. As stated in the documentation, the Fake Machine Block outputs the following labels : 't(s)', 'F(N)', 'x(mm)', 'Exx(%)', 'Eyy(%)'. They are all transmitted to the Grapher and Recorder Blocks, that respectively plot and record only part of these labels. The Recorder will save the received data to a 'data.csv' file, at the same level as the script.

Notice how in the Blocks you can often specify the names of the labels to use as inputs and/or the ones to output. This way, it is straightforward to keep track of the data flow throughout the code.

There’s only one final line to add before you can run this first example :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph_force = crappy.blocks.Grapher(('t(s)', 'F(N)'))

  graph_pos = crappy.blocks.Grapher(('t(s)', 'x(mm)'))

  crappy.link(gen, machine)

  crappy.link(machine, record)
  crappy.link(machine, graph_pos)
  crappy.link(machine, graph_force)

  crappy.start()

You can now execute the file like any regular Python file :

python crappy_syntax.py

As the script starts, two windows should appear and plot the data coming from the Fake Machine Block. In the mean time, a data.csv file should appear at the same level as the script that was just started. It contains the data being acquired by the Recorder Block. As mentioned earlier, the execution of the script will stop after 40s as specified to the Generator Block. The script can also stop earlier if an error occurs (e.g. missing dependency), or if the user hits Control-c. Note that this last way of ending a script should only be used in case something goes wrong, e.g. if the script crashes. You can find more about the different ways to stop a script in Crappy in a later section.

You now know learned the very basics of writing scripts for Crappy ! You can download this first example to run it locally on your computer. This second section of the tutorials was only a brief introduction, there’s still much more to learn in the following sections !

2. The most used Blocks

In this third section, you will learn how to handle the most used Blocks of Crappy. These Blocks are all essential, and you’ll come across at least one of them in most scripts. For an extensive list of all the implemented Blocks, refer to the Current functionalities section of the documentation.

2.a. The Generator Block and its Paths

Let’s start this tour of the most used Blocks with the Generator. It allows to generate a signal according to a pre-defined pattern, and to send it to downstream Blocks. It is mostly used for generating commands when driving actuators or motors, but has actually many more possible applications (trigger generation, target value for a PID, etc.). In the previous section, the presented example already contained an instance of the Generator Block. So let’s take a closer look at it :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

Note

To run this example, you’ll need to have the matplotlib Python module installed.

As you can see, the first argument of the Generator is its path. It describes the shape of the generated signal, and is the main parameter to set when instantiating a Generator Block. It has to be an Iterable (like a list or a tuple), that contains one or several dict with the correct keys. Each dictionary represents one type of signal to generate, and these signals are generated in the same order as the dictionaries are given. The moment when the Generator switches to the next dictionary is usually determined by the 'condition' argument of the current dictionary, if applicable. After finishing the last dictionary, the default behavior for the Generator is to stop the current script.

To know the available types of signals and their mandatory and optional arguments, you’ll need to refer to the Generator Paths section of the API page. There are quite many options available, and if you have a very specific need you can always create your own Generator Path. The name of the Path to use (the type of signal to generate) is given by the 'type' key of each dictionary. The other keys represent the possible arguments for the given Path, and thus depend on the type of Path.

In the example above, the first and only chosen Path is the Constant one. As you can read in the API, it requires the 'condition' and 'value' arguments, which are indeed present in the dictionary. The Constant Path generates a constant signal of value 'value', and stops when 'condition' is met. The syntax for the conditions is described in detail in the parse_condition() method of the base Path, and a tutorial section is dedicated to the advanced uses of this argument. For a number of applications, setting it to 'delay=xx' (next Path after xx seconds) or to None (never switches to next Path) is fine.

Let’s now try to modify the previous example, so that the Fake Machine is driven with a more complex pattern than just a constant speed. Let’s say that we now want to perform cyclic stretching and relaxation on the fake sample, and then stretch it until failure. Compared to the previous example, we can keep the Constant Path, but we must add a Cyclic Path before to perform the cyclic stretching. Here’s how it looks :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Cyclic',
                                       'value1': 5/60,
                                       'value2': -5/60,
                                       'condition1': 'delay=10',
                                       'condition2': 'delay=10',
                                       'cycles': 2},
                                      {'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

As you may have guessed (or read in the API), the Cyclic Path alternates between two Constant Paths, and must thus be given the arguments for these two Paths. The 'condition{1,2}' keys indicate when to switch to the other Constant, and the 'cycles' key indicates after how many cycles to end the Cyclic Path and switch to the next one. Here, the Generator will switch to the Constant Path once the Cyclic one ends, and then end the test once the Constant Path finishes. To make the script runnable, let’s complete it with the same code as in the previous example :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Cyclic',
                                       'value1': 5/60,
                                       'value2': -5/60,
                                       'condition1': 'delay=10',
                                       'condition2': 'delay=10',
                                       'cycles': 2},
                                      {'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph_force = crappy.blocks.Grapher(('t(s)', 'F(N)'))

  graph_pos = crappy.blocks.Grapher(('t(s)', 'x(mm)'))

  crappy.link(gen, machine)

  crappy.link(machine, record)
  crappy.link(machine, graph_pos)
  crappy.link(machine, graph_force)

  crappy.start()

The script should run in the exact same way as the one of the previous section, except this time there should be two cycles of stretching and relaxation before the final step of stretching until failure. That reflects the changes we made to the path of the Generator Block. Just like previously, the script will stop by itself. You can stop it earlier with Control-c, but this is not considered as a clean way to stop Crappy. Download this Generator example to run it locally on your machine !

You should now be able to build an run a variety of patterns for your Generator Blocks ! It is after all just a matter of reading the API, selecting the Paths that you want to use, and include them in the path argument of the Generator Block with the correct parameters. As mentioned earlier in this section, more information about the Generator Paths can be found in another tutorial section. More examples of the Generator Block can be found in the examples folder on GitHub.

2.b. The Camera Block

For this second highlighted Block, let’s introduce the Camera Block. It allows to acquire images from real or virtual Cameras objects, and to record, display, and process them. No processing is included in the base Camera Block, but all of its children are meant to perform some sort of processing (see the Video Extenso, DIS Correl or DIC VE Blocks). The instantiation of a Camera Block is quite simple, here’s how it looks like :

# coding: utf-8

import crappy

if __name__ == '__main__':

  cam = crappy.blocks.Camera('FakeCamera',
                             config=True,
                             display_images=True,
                             displayer_framerate=30,
                             save_images=False,
                             freq=40)

Note

To run this example, you’ll need to have the opencv-python, matplotlib and Pillow Python modules installed.

The first given argument is the name of the Camera to use for acquiring the images. In this demo, the Fake Camera is used so that the code can run without any hardware. Then, the user specifies whether they want the captured images to be displayed and/or recorded. There are more options available for tuning the acquisition, the recording and the display, they are all listed in the documentation of the Camera Block in the API.

Another important argument is the config one. When enabled, a CameraConfig window is displayed before the main part of the script runs. In this window, the user can interactively tune the available settings for the selected Camera object. The possible settings can be viewed by looking at the documentation in the API, for example in the open method of FakeCamera for the Fake Camera. If the config windows is disabled, the settings can still be adjusted by providing them as kwargs to the Camera Block. Note that disabling the config window makes some arguments mandatory, as detailed in the documentation of the Camera Block.

To have a functional and clean example script, we still need to add a few lines. In particular, unlike the Generator Block, the Camera does not automatically stop after a condition is met. To allow the script to stop in a proper way, a Stop Button Block should be added. It will display a button, that will stop the execution of the script when clicked upon. It is always possible to stop Crappy using Control-c, but this is not considered a proper way of ending the script. After inserting the stop button, here’s the final runnable script :

# coding: utf-8

import crappy

if __name__ == '__main__':

  cam = crappy.blocks.Camera('FakeCamera',
                             config=True,
                             display_images=True,
                             displayer_framerate=30,
                             save_images=False,
                             freq=40)

  stop = crappy.blocks.StopButton()

  crappy.start()

Download this Camera example to run it locally on your machine ! You can adjust the parameters to see what the effect is. You’ll find more information on the possible arguments and their effects in the documentation. The children of the Camera Block that perform image processing work on the exact sample principle, except they accept extra arguments and can output data to downstream Blocks. More examples of the Camera Block and its children can be found in the examples folder on GitHub.

2.c. The Grapher Block

For displaying the data acquired or generated by a Block, the Grapher Block is by far the most popular solution. It allows to plot the received data, always one label against another one. In the first example, you can see that the syntax for providing the labels is ('label_x', 'label_y'). What is not shown in the first example, though, is that you can plot multiple curves on a same graph. You also don’t have to plot data against time, you can plot any label against any other one as long as they are synchronized.

Just like any other Block, the Grapher also has a number of parameters that can be adjusted. You can find the exact list in the API, at the Grapher entry. Here is a modified version of the first example, where the force is plotted against the position and where some extra arguments of the Grapher Block are set :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(path=[{'type': 'Constant',
                                       'value': 5 / 60,
                                       'condition': 'delay=40'}],
                                cmd_label='input_speed')

  machine = crappy.blocks.FakeMachine(cmd_label='input_speed')

  record = crappy.blocks.Recorder(file_name='data.csv',
                                  labels=['t(s)', 'F(N)', 'x(mm)'])

  graph = crappy.blocks.Grapher(('x(mm)', 'F(N)'),
                                interp=False,
                                window_size=(6, 6))

  crappy.link(gen, machine)

  crappy.link(machine, record)
  crappy.link(machine, graph)

  crappy.start()

Note that there are other ways of displaying data in Crappy, check the Dashboard and the Link Reader Blocks for example. The Grapher Block takes up quite much CPU and memory, so it is better not to have too many of its instances in a script. You can download this Grapher example to run it locally on your machine. Another example of the Grapher Block can be found in the examples folder on GitHub.

2.d. The Recorder Block

For saving the data acquired or generated by a Block, the preferred solution in Crappy is to use the Recorder Block. It must be linked to one and only one upstream Block, and will save all the data it receives from it in a .csv (or equivalent text format) file. This Block is quite basic, so the first example given above should be enough for you to understand its syntax. Another example of the Recorder Block can be found in the examples folder on GitHub. Note that for recording streams, the HDF Recorder Block should be used instead (see this later section).

2.e. The IOBlock Block

Along with the Camera and Actuator Blocks, the IOBlock is one of the few Blocks in Crappy that can interact with hardware. It serves two purposes : first, it can acquire data from a device and send it to downstream Blocks. And second, it can also receive commands from upstream Blocks and set them on active hardware. These two functions can be used simultaneously, for hardware supporting it. To communicate with hardware, the IOBlock relies on the In / Out objects, that each implement the communication with a different device. Here’s an example of code featuring an IOBlock for data acquisition :

# coding: utf-8

import crappy

if __name__ == '__main__':

  io = crappy.blocks.IOBlock('FakeInOut',
                             labels=('t(s)', 'ram(%)'),
                             freq=30)

  graph = crappy.blocks.Grapher(('t(s)', 'ram(%)'))

  stop = crappy.blocks.StopButton()

  crappy.link(io, graph)

  crappy.start()

Note

To run this example, you’ll need to have the psutil and matplotlib Python modules installed.

As you can see, the base syntax is quite simple for acquiring data with an IOBlock. You first have to specify the InOut that you want to use for data acquisition. The FakeInOut was chosen here as it does not require any hardware to run. Then , you need to indicate which labels will carry the acquired values. Refer to the API to know what kind of data the chosen InOut outputs. The output data is here visualized using a Grapher Block, and that’s pretty much it ! The data that you can visualize on the graph corresponds to the current RAM usage of your computer. You can open or close a web browser to see it change consistently. Let’s now write another example where a command is set by an IOBlock :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(({'type': 'Sine',
                                  'amplitude': 20,
                                  'offset': 50,
                                  'freq': 0.02,
                                  'condition': 'delay=100'},),
                                cmd_label='target_ram(%)',
                                freq=30)

  io = crappy.blocks.IOBlock('FakeInOut',
                             cmd_labels='target_ram(%)',
                             freq=30)

  crappy.link(gen, io)

  crappy.start()

This time, the 'cmd_labels' argument must be set on the IOBlock to indicate which label carries the command to set. The label carrying the command can be generated by any type of Block, but for simplicity it is here output by a Generator Block. When receiving a command, the FakeInOut tries to use the correct amount of RAM to match the target value. Here, the command is a sine wave oscillating between 30 and 70% of RAM usage. You can visualize the effect of the script by opening a RAM monitor, such as the Task Manager in Windows or htop in Linux. Finally, it is possible to use both behaviors of the IOBlock simultaneously :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(({'type': 'Sine',
                                  'amplitude': 20,
                                  'offset': 50,
                                  'freq': 0.02,
                                  'condition': 'delay=100'},),
                                cmd_label='target_ram(%)',
                                freq=30)

  io = crappy.blocks.IOBlock('FakeInOut',
                             cmd_labels='target_ram(%)',
                             labels=('t(s)', 'ram(%)'),
                             freq=30)

  graph = crappy.blocks.Grapher(('t(s)', 'ram(%)'))

  stop = crappy.blocks.StopButton()

  crappy.link(gen, io)
  crappy.link(io, graph)

  crappy.start()

Notice how the two functionalities of the IOBlock integrate seamlessly into a single common script. You can download this IOBlock example to run it locally on your machine. More examples of the IOBlock can be found in the examples folder on GitHub. Note that the streamer mode of the IOBlock is presented in a dedicated section, and same goes for the make_zero functionality. Directly check the documentation of the IOBlock to learn more about it.

2.f. The Machine Block

Similar to the IOBlock, the Machine Block can send commands to hardware and acquire data from it. The difference is that the IOBlock can send any type of command and acquire any type of data, whereas the Machine Block can only send speed or position commands and acquire speed and position data. The Machine Block is therefore specifically designed to drive motors, or comparable actuators. The Machine Block relies on the Actuators objects for communicating with the hardware. The syntax of the arguments to provide to the Machine Block is quite similar to that of the Generator Block, as demonstrated here :

# coding: utf-8

import crappy

if __name__ == '__main__':

  gen = crappy.blocks.Generator(({'type': 'Cyclic',
                                  'value1': -10,
                                  'condition1': 'delay=3',
                                  'value2': 10,
                                  'condition2': 'delay=3',
                                  'cycles': 3},),
                                freq=50,
                                cmd_label='tension(V)')

  mot = crappy.blocks.Machine(({'type': 'FakeDCMotor',
                                'cmd_label': 'tension(V)',
                                'mode': 'speed',
                                'speed_label': 'speed(RPM)',
                                'kv': 500},),
                              freq=50)

  graph = crappy.blocks.Grapher(('t(s)', 'speed(RPM)'))

  crappy.link(gen, mot)
  crappy.link(mot, graph)

  crappy.start()

As you can see, the Machine Block accepts an iterable of dict as its first argument. Each dictionary contains the information corresponding to one Actuator to drive. This means that it is possible to drive several Actuators from only one Machine Block ! In each dictionary, the 'type' key indicates the name of the Actuator to use. Then, other keys like 'mode' or 'cmd_label' provide information on how to drive the Actuator. The 'speed_label' key indicates which information to acquire from the Actuator and under which label to return it. To have an overview of the keys that are not Actuator-dependent and their effect, refer to the documentation of the Machine Block in the API. Finally, the arguments to pass to the Actuator should also be given in the dictionary, here through the 'kv' key for example. The Actuator used here is the FakeDCMotor, that does not require any hardware to run. Check its documentation to get all the possible arguments it accepts.

Note

Driving several Actuators with one Machine Block is only recommended when these Actuators need to be synchronized, e.g. on a bi-motor machine. Otherwise, you should rather drive each Actuator with a different Machine Block.

You can download this Machine Block example to run it locally on your machine. It should last only 20s before stopping by itself. The Grapher window that appears displays the current speed of the Actuator driven by the Machine Block, and responding to the voltage command (treated as a speed by the Machine) received from the Generator. More examples of the Machine Block can be found in the examples folder on GitHub.

3. Properly stopping a script

In the previous sections, several different ways to stop a script in Crappy have been presented. In this section, you will learn about the best practices for stopping Crappy and the right objects to use.

First, why is it important to stop a script in a clean way ? Depending on what you do, you might want, or even need, to properly de-initialize stuff. For example, you certainly don’t want a motor to keep running forever when a test stops ! It should instead halt, regardless of the reason why the script ends. Another reason why scripts should be properly stopped is because otherwise it could lead to “zombie” processes running on your computer, and taking up memory and processor resources. This situation should of course be avoided.

So, what should you not do to stop a script ? Obviously, you should avoid any “aggressive” termination method, like abruptly closing the terminal where Crappy runs or stopping Crappy from a Task Manager. Keep these extreme solutions for the (very unlikely) situations when Crappy would become totally unresponsive…

Starting from version 2.0.0, hitting Control-c to stop Crappy (i.e. raising KeyboardInterrupt) is also considered as an invalid behavior. However, unlike more aggressive methods, Control-c is still handled internally and should lead to a proper termination of the Blocks. As it might lead to unexpected behavior, and to deter users from using it, we chose to have Control-c raise an Exception once all the Blocks are correctly stopped. This behavior can be tuned, see the 7. Advanced control over the runtime section of the tutorials. The take-home message about Control-c is : using :kbd:`Control-c` to stop a script is fine in most cases but it is preferable to do otherwise, so it will by default raise an error even if everything went fine !

Now that we know what are the forbidden and not recommended ways of stopping Crappy, let’s review the 100% approved ones ! As you should have noticed in the tutorials above, some objects in Crappy have the ability to stop a script once they are done. The most common one is the Generator, that can stop a script once its Generator Paths are exhausted. Same goes for the File Reader Camera object, that can stop a script once its images are exhausted. In addition to these two Blocks, two other ones are specifically designed to stop a test in a clean way. They are the Stop Block and Stop Button Blocks, designed to respectively stop a test automatically when a condition is met, and stop a test manually when the user clicks on a button. For users integrating Crappy in a GUI, the crappy.stop() method is also an option.

Note

A test in Crappy will also end if an unexpected Exception is raised anywhere in the module. In that case, all Blocks will instantly stop and, just like with Control-c, an error will be raised once all the Blocks are stopped. The unexpected Exception will still be handled and all Blocks should terminate properly if the problem is not too serious.

When writing a script, first determine which termination way seems more appropriate. If you want to be able to stop the test anytime, opt for the Stop Button Block (for example if you use samples with variable properties that would make the duration of a test unpredictable). And if there’s an objective condition that signals the end of your test, rather choose the Stop Block or rely on the Generator Block if applicable. Note that you can use both a Stop Button and a Stop Block together. And in the specific case when you want to trigger Crappy’s termination from a GUI, use crappy.stop() instead.