b14bdc067ad6277af1fadfa8c31e41dbca76bd99
[AGL/documentation.git] / docs / 5_Component_Documentation / 7_pyagl.md
1 ---
2 title: PyAGL
3 ---
4
5 # 0. Intro
6 ## Main purpose 
7 PyAGL was written to be used as a testing framework replacing the Lua afb-test one,
8 however the modules are written in a way that could be used as standalone utilities to query
9 and evaluate apis and verbs from the App Framework Binder services in AGL.
10
11 ## High level overview
12 Python compatibility:
13 Initial development PyAGL was done with Python **3.6** in mind, heavily using f-strings and a few typings. As of writing this 
14 documentation(June 3rd 2021), current stable AGL version is Koi 11.0.2 which has Python 3.8, and further development is 
15 done using 3.8 and 3.9 runtimes although **no** version-specific features are used from later versions; 
16 features **used** are kept within features **offered** by Python version first used for PyAGL in AGL Jellyfish.
17
18 The test suite is written in a relatively standard way of extending **pytest** with a couple tweaks 
19 tailored to Jenkins CI and LAVA for AGL with regards to output and timings/timeouts, and these tweaks are enabled by running `pytest -L`
20 in order to enable LAVA logging behavior.
21
22 The way PyAGL works could be summarized in several bullets below:
23   * `websockets` package is used to communicate to the services, `x-afb-ws-json1` is used as a subprotocol, 
24   * base.py provides AGLBaseService to be extended for each service
25   * AGLBaseService has a portfinder() routine which will use `asyncssh` if used remotely, 
26 to figure out the port of the service's websocket that is listening on. When this was implemented services had a hardcoded listening port,
27 and was often changed when a new service was introduced. If you specify port, pyagl will connect to it directly. If no port is specified and
28 portfinder() cannot find the process or listening port should throw an exception and exit.
29   * main() implementations in most PyAGL services' bindings are intended to be used as a convenient standalone utility to query verbs, although
30 not necessarily available.
31   * PyAGL bindings are organized in classes, method names and respective parameters mostly adhere to service verbs/apis described 
32 per service in https://git.automotivelinux.org/apps/agl-service-*/about
33 For example, in https://git.automotivelinux.org/apps/agl-service-audiomixer/about/ the docs for the service describe 5 verbs -
34 subscribe, unsubscribe, list_controls, volume, mute - and their respective methods in audiomixer.py.
35   * as mentioned above `pytest` package is required for unit tests. 
36   * `pytest-async` is needed by pytest to cooperate with asyncio
37   * `pytest-dependency` is used in cases where specific testing order is needed and used via decorators
38
39 # 1. Using PyAGL
40 There are few prerequisites to start using it. First, your AGL image **must** be bitbaked with **agl-devel** feature when sourcing aglsetup.sh;
41 if not - the running AGL instance won't have websocket services exposed to listening TCP ports and PyAGL will fail to connect.
42
43 ```bash
44 git clone "https://gerrit.automotivelinux.org/gerrit/src/pyagl"
45 ```
46 Preferably create a virtualenv and install the packages in the env
47 ```bash
48 pip install -r requirements.txt
49 ```
50 Hard requirements are asyncssh, websockets, pytest, pytest-dependency, pytest-async; the others in the file are dependencies of the mentioned packages.
51 ```
52 cd pyagl/pyagl/services
53 python3 audiomixer.py 192.168.234.34 --list_controls
54 ```
55 or if you have installed PyAGL as python package 
56 ```
57 python3 -m pyagl.services.audiomixer --list_controls
58 ```
59 should produce the following or similar result depending on how many controls are exposed and which AGL version you are running:
60 ```
61 matching services: ['afm-service-agl-service-audiomixer--0.1--main@1001.service']
62 Requesting list_controls with id 359450446
63 [RESPONSE][Status: success][359450446][Info: None][Data: [{'control': 'Master Playback', 'volume': 1.0, 'mute': 0}, 
64 {'control': 'Playback: Speech-Low', 'volume': 1.0, 'mute': 0}, {'control': 'Playback: Emergency', 'volume': 1.0, 'mute': 0}, 
65 {'control': 'Playback: Speech-High', 'volume': 1.0, 'mute': 0}, {'control': 'Playback: Navigation', 'volume': 1.0, 'mute': 0}, 
66 {'control': 'Playback: Multimedia', 'volume': 1.0, 'mute': 0}, {'control': 'Playback: Custom-Low', 'volume': 1.0, 'mute': 0}, 
67 {'control': 'Playback: Communication', 'volume': 1.0, 'mute': 0}, {'control': 'Playback: Custom-High', 'volume': 1.0, 'mute': 0}]]
68 ```
69
70 # 2. Running the tests
71
72 ## Locally - On the board itself
73 There is the /usr/bin/pyagl script which invokes the tests residing in 
74 `/usr/lib/python3.8/site-packages/pyagl/tests`
75
76 ```
77 qemux86-64:~# pyagl
78 =================== test session starts =============================
79 platform linux -- Python 3.8.2, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
80 rootdir: /usr/lib/python3.8/site-packages/pyagl, inifile: pytest.ini
81 plugins: dependency-0.5.1, asyncio-0.10.0, reverse-1.0.1
82 collected 213 items
83
84 test_audiomixer.py .......                                                                                                                                                                                                                                                                                            [  3%]
85 test_bluetooth.py ............xxxsxx                                                                                                                                                                                                                                                                                  [ 11%]
86 test_bluetooth_map.py .x.xs. 
87 ...
88 ```
89
90 ## Remotely
91 You must export `AGL_TGT_IP` environment variable first, containing a string with a reachable IP address 
92 configured(either DHCP or static) on one of the interfeces on the AGL instance(board or vm) on your network.
93 `AGL_TGT_PORT` is not required, however can be exported to skip over connecting to the board via ssh first 
94 in order to figure out the listening port of service. 
95
96 ```
97 user@debian:~$ source ~/.virtualenvs/pyagl/bin/activate
98 (pyagl) user@debian:~$ export AGL_TGT_IP=192.168.234.34
99 (pyagl) user@debian:~$ cd pydev/pyagl/pyagl/tests
100 (pyagl) user@debian:~/pydev/pyagl/pyagl/tests$ pytest test_geoclue.py
101 ========================= test session starts =========================
102 platform linux -- Python 3.9.2, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
103 rootdir: /home/user/pydev/pyagl/pyagl, inifile: pytest.ini
104 plugins: dependency-0.5.1, asyncio-0.11.0
105 collected 3 items
106
107 test_geoclue.py ...                                              [100%]
108 ```
109 # 3. Writing bindings and/or tests for new services
110 Templates directory contains barebone _cookiecutter_ templates to create your project.
111 If you do not intend to use cookiecutter, you need a simple service file in which you inherit AGLBaseService
112 from base.py. 
113 You can take a look at pyagl/services/geoclue.py and pyagl/tests/test_geoclue.py which is probably the
114 simplest binding in PyAGL for a reference and example. All basic methods like 
115 send|receive|un/subscribe|portfinder are implemented in the base class. 
116 You would need to do minimal work to create new service binding from scratch and by example of the geoclue you need to do the following:
117 - do the basic imports 
118 ```
119 from pyagl.services.base import AGLBaseService, AFBResponse
120 import asyncio
121 import os
122 ```
123 - inherit AGLBaseService and type in the service class member the service name presuming you are following the AGL naming convention:
124 (if your new service does not follow the convention, the portfider routine wont work and you'll have to specify service port manually)
125 ```
126 class GeoClueService(AGLBaseService):
127     service = 'agl-service-geoclue'
128 ```
129 - if you intend to run the new service binding as a standalone utility, you might want to add your new options to the argparser
130 ```
131     parser = AGLBaseService.getparser()
132     parser.add_argument('--location', help='Get current location', action='store_true')
133 ```
134 - override the __init__ method with the respective parameters as api(used in the binding) and systemd service slug
135 ```
136     def __init__(self, ip, port=None, api='geoclue'):
137         super().__init__(ip=ip, port=port, api=api, service='agl-service-geoclue')
138 ```
139 - define your methods and send requests with .request() which prepares the data in a JSON format, request returns message id
140 ```
141     async def location(self):
142         return await self.request('location')
143 ```
144 - get the raw response with data = await .response() or use .afbresponse() to get structured data
145
146 README.md in the root directory of the project also contains useful information.
147