Developers documentation
How to implement a simulator
To implement a simulator, it is necessary to create a module that defines both a servers list and a System class.
The servers list
The servers list defines all the instances that should be running when the given simulator starts. Each element of the servers list is a tuple, composed of the following items:
the server listening address, l_address
the server sending address, s_address
the type of threading server from the SocketServer package to use to run the simulator
a dictionary of optional keyword arguments, kwargs, eventually used by System.__init__()
The l_address item is the address on which the server will listen for incoming commands to pass down to the System.parse() method. The s_address item is the address from which the server will periodically send its data to its connected clients. The type of threading server from the SocketServer argument can either be ThreadingTCPServer or ThreadingUDPServer, depending on the type of socket the server has to use. These Python object types have to be imported as follows:
from SocketServer import ThreadingTCPServer
or:
from SocketServer import ThreadingUDPServer
Some examples
Suppose the reader wants to simulate a system that has 2 listening TCP servers
and no sending servers, the first one with address ('192.168.100.10', 5000)
and the second one with address ('192.168.100.10', 5001)
. In this case we
have to define the servers list as follows:
servers = [
(('192.168.100.10', 5000), (), ThreadingTCPServer, {}),
(('192.168.100.10', 5001), (), ThreadingTCPServer, {}),
]
If the System class accepts some extra arguments, two integers, for instance, it is possible to pass them via the kwargs dictionary:
servers = [
(('192.168.100.10', 5000), (), ThreadingTCPServer, {'arg1': 10, 'arg2': 20}),
(('192.168.100.10', 5001), (), ThreadingTCPServer, {'arg1': 4, 'arg2': 5}),
]
If the System to simulate has instead a single listening UDP server, the servers list will be defined as follows:
servers = [
(('192.168.100.10', 5000), (), ThreadingUDPServer, {}),
]
A System with 3 sending TCP servers and no listening servers will have the servers list defined in the following way:
servers = [
((), ('192.168.100.10', 5002), ThreadingTCPServer, {}),
((), ('192.168.100.10', 5003), ThreadingTCPServer, {}),
((), ('192.168.100.11', 5000), ThreadingTCPServer, {}),
]
Finally, a system instance can act as both listening and sending server. In this case, each server list entry must be defined as follows:
servers = [
(('192.168.100.10', 5003), ('192.168.100.10', 5004), ThreadingTCPServer, {}),
(('192.168.100.10', 6000), ('192.168.100.10', 6001), ThreadingTCPServer, {}),
]
Be aware that multiple lines in the servers list will cause the simulator to spawn a System object per line. Every one of the spawned System objects is independent from the others and they will all act as different simulators.
Custom commands
Custom commands are useful for several use cases. For instance, suppose we want the simulator to reproduce some error conditions by changing the System state. We just need to define a method that starts with system_ inside the System class. I.e:
class System(ListeningSystem):
def system_generate_error_x(self):
# Change the state of the System
...
After implementing this method, the clients are able to call it by sending the custom command $system_generate_error_x%. It is also possible to define a method that accepts some parameters. In this case the custom command will have the form $system_commandname:par1,par2,par3%. Since every Server object is not limited to only a single connection, custom commands can be also sent by a different client that the main one. This allow the reproduction of error scenarios even when the DISCOS control software is already connected to some simulator.
In order to avoid name clashing for custom methods, it is sufficient to not use the system_ prefix for any other System method, so make sure to only use this convention for custom commands.
Useful functions
In order to make it faster to write and implement new simulator’s methods, which sometimes require converting data from a format to another, a library of useful functions called simulators.utils has been written and comes within the simulators package. Its API is described in the The simulators.utils library section.
Testing environment
In the continuous integration workflow, the tests are executed more than once. During the development process, tests will be executed locally, and after pushing the code to Github, they will be executed on GitHub Actions.
Dependencies
To Run the unit tests there is no need to install any additional depencency. That is possible thanks to the unittest framework, included in the Python standard library. But we do not want to only run the unit tests: we want to set up an environment that allows us to check for suspicious code, test the code and the documentation, evaluate the testing coverage, and replicate the GitHub Actions build locally. To accomplish this goal we need to install some additional dependencies by executing the following command:
$ pip install -r testng_requirements.txt
which is equivalent of manually installing the testing dependencies by executing the following commands:
$ pip install coverage # Coverage testing tool
$ pip install prospector # Python linter
Run the unit tests
Move to the project’s root directory and execute the following command:
$ python -m unittest discover -b tests
Run the linter
To run the linter move to the project’s root directory and execute the following command:
$ prospector
Check the testing coverage
To check the percentage of code covered by test, run the unit tests using Coverage.py:
$ coverage run -m unittest discover -b tests
Now generate an HTML report:
$ coverage combine && coverage report && coverage html
To see the HTML report open the generated htmlcov/index.html file with your browser.
Test the documentation
To make sure the documentation is written correctly, several things have to be tested:
the docstring examples
the documentation (doc directory) examples
the links inside the documentation must point correctly to the target
the HTML must be generated properly
In order to do so, some other dependencies must be installed by using the following command:
$ pip install -r doc/doc_requirements.txt
which is equivalent of manually installing the following Python packages:
$ pip install sphinx # Documentation generator
$ pip install sphinx_rtd_theme # HTML doc theme
In order to test the docstring examples, we use the Python standard library doctest module. Simply move to the root directory of the project and execute the following command:
$ python -m doctest simulators/*.py
To test the examples in the doc directory:
$ cd doc
$ make doctest
To check if there are broken URLs in the documentation:
$ make linkcheck # From the doc directory
To generate the HTML:
$ make html # From the doc directory
Run all tests at once
All tests can be run at once using the act <https://github.com/nektos/act>__ tool. This tool can be installed in several operating systems and relies on Docker to be executed. act and Docker installation procedures will not be documented here since they are already described on their respective web pages. Once everything is set up correctly, all tests can be executed by launching the following command in the repository main directory:
$ act
The act program reads the .actrc file in the main directory, this file contains the configuration in order for act to run the same GitHub Action workflow that will be run online when a commit is pushed to the remote repository.