Merge ~kissiel/checkbox-support:zapper-capabilities-in-proxy into checkbox-support:master

Proposed by Maciej Kisielewski
Status: Merged
Approved by: Maciej Kisielewski
Approved revision: 05328605fd6ca595b2ddd65b2aa4564ae9050170
Merged at revision: 98d3933ad70ba0ef8a8189975f376b46fe42d5e5
Proposed branch: ~kissiel/checkbox-support:zapper-capabilities-in-proxy
Merge into: checkbox-support:master
Diff against target: 284 lines (+207/-36)
2 files modified
checkbox_support/scripts/tests/test_zapper_proxy.py (+129/-0)
checkbox_support/scripts/zapper_proxy.py (+78/-36)
Reviewer Review Type Date Requested Status
Devices Certification Bot Needs Fixing
Paul Larson Approve
Review via email: mp+421204@code.launchpad.net

Description of the change

This MR brings stuff to the Zapper Proxy.

First of all it adds missing unit tests.
It fixes the call in usb_set_state.
It adds the call for getting Zappers functionality.
And it changes the main "driver" of the script from ordinary main to a class, so other code of checkbox-support may use it directly, instead of spawning a new process.

To post a comment you must log in.
Revision history for this message
Paul Larson (pwlars) wrote :

2 minor spelling issues, but I'll hit approve here with the assumption that you'll likely fix those :)

review: Approve
Revision history for this message
Devices Certification Bot (ce-certification-qa) wrote :

The merge was fine but running tests failed.

"10.38.105.54"
"10.38.105.108"
"10.38.105.197"
[xenial] [21:00:41] starting container
Device project added to xenial-testing
"10.38.105.54"
[xenial] [21:01:10] provisioning container
[bionic] [21:01:30] starting container
[focal] [21:01:40] starting container
Device project added to bionic-testing
Device project added to focal-testing
"10.38.105.145"
[bionic] [21:01:59] provisioning container
"10.38.105.235"
[focal] [21:02:11] provisioning container
[bionic] [21:02:38] Starting tests...
[bionic] Found a test script: ./requirements/container-tests-checkbox-support
[focal] [21:02:43] Starting tests...
[focal] Found a test script: ./requirements/container-tests-checkbox-support
[xenial] [21:02:56] Starting tests...
[xenial] Found a test script: ./requirements/container-tests-checkbox-support
[focal] [21:03:34] container-tests-checkbox-support: PASS
[focal] [21:03:34] Fixing file permissions in source directory
[bionic] [21:03:35] container-tests-checkbox-support: PASS
[bionic] [21:03:35] Fixing file permissions in source directory
[focal] [21:03:35] Destroying container
[bionic] [21:03:35] Destroying container
[xenial] [21:03:45] container-tests-checkbox-support: FAIL
[xenial] output: https://paste.ubuntu.com/p/VWVgK4f3Zc/
[xenial] [21:03:48] Fixing file permissions in source directory
[xenial] [21:03:48] Destroying container

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/checkbox_support/scripts/tests/test_zapper_proxy.py b/checkbox_support/scripts/tests/test_zapper_proxy.py
2new file mode 100644
3index 0000000..db3e754
4--- /dev/null
5+++ b/checkbox_support/scripts/tests/test_zapper_proxy.py
6@@ -0,0 +1,129 @@
7+# Copyright 2022 Canonical Ltd.
8+# Written by:
9+# Maciej Kisielewski <maciej.kisielewski@canonical.com>
10+#
11+# This is free software: you can redistribute it and/or modify
12+# it under the terms of the GNU General Public License version 3,
13+# as published by the Free Software Foundation.
14+#
15+# This file is distributed in the hope that it will be useful,
16+# but WITHOUT ANY WARRANTY; without even the implied warranty of
17+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+# GNU General Public License for more details.
19+#
20+# You should have received a copy of the GNU General Public License
21+# along with this file. If not, see <http://www.gnu.org/licenses/>.
22+"""Tests for zapper_proxy module."""
23+
24+from unittest import TestCase
25+from unittest.mock import Mock, patch
26+
27+from checkbox_support.scripts.zapper_proxy import ZapperControlV1
28+
29+
30+class ZapperProxyV1Tests(TestCase):
31+ """Unit tests for ZapperProxyV1 class."""
32+
33+ def setUp(self):
34+ self._mocked_conn = Mock()
35+ self._mocked_conn.root = Mock()
36+
37+ def test_usb_get_state_smoke(self):
38+ """
39+ Check if usb_get_state calls appropriate function on the rpyc client.
40+
41+ Current implementation on the service side uses mutable arguments for
42+ returning values (C-style stuff that should be removed) this is why we
43+ need the stateful side_effect below
44+ """
45+ def side_effect_fn(_, ret):
46+ ret.append('ON')
47+ return True
48+ self._mocked_conn.root.zombiemux_get_state = Mock(
49+ side_effect=side_effect_fn)
50+ zapctl = ZapperControlV1(self._mocked_conn)
51+
52+ with patch('builtins.print') as mocked_print:
53+ zapctl.usb_get_state(0)
54+ mocked_print.assert_called_once_with(
55+ 'State for address 0 is ON')
56+
57+ def test_usb_get_state_fails(self):
58+ """Check if usb_get_state quits with a proper message on failure."""
59+ self._mocked_conn.root.zombiemux_get_state = Mock(return_value=False)
60+ zapctl = ZapperControlV1(self._mocked_conn)
61+ with self.assertRaises(SystemExit) as context:
62+ zapctl.usb_get_state(0)
63+ self.assertEqual(
64+ context.exception.code, 'Failed to get state for address 0.')
65+
66+ def test_usb_set_state_smoke(self):
67+ """
68+ Check if usb_set_state calls appropriate functions on the rpyc client.
69+ """
70+ self._mocked_conn.root.zombiemux_set_state = Mock(return_value=True)
71+ zapctl = ZapperControlV1(self._mocked_conn)
72+ with patch('builtins.print') as mocked_print:
73+ zapctl.usb_set_state(0, 'ON')
74+ mocked_print.assert_called_once_with(
75+ "State 'ON' set for the address 0.")
76+
77+ def test_usb_set_state_fails(self):
78+ """Check if usb_set_state quits with a proper message on failure."""
79+ self._mocked_conn.root.zombiemux_set_state = Mock(return_value=False)
80+ zapctl = ZapperControlV1(self._mocked_conn)
81+ with self.assertRaises(SystemExit) as context:
82+ zapctl.usb_set_state(0, 'ON')
83+ self.assertEqual(
84+ context.exception.code, "Failed to set 'ON' state for address 0.")
85+
86+ def test_get_capabilities_one_cap(self):
87+ """
88+ Check if get_capabilities properly prints one record.
89+
90+ The record should be in Checkbox resource syntax and should not be
91+ surrounded by any newlines.
92+ """
93+ ret_val = [{'foo': 'bar'}]
94+ self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
95+ zapctl = ZapperControlV1(self._mocked_conn)
96+ with patch('builtins.print') as mocked_print:
97+ zapctl.get_capabilities()
98+ mocked_print.assert_called_once_with('foo: bar')
99+
100+ def test_get_capabilities_empty(self):
101+ """Check if get_capabilities prints nothing on no capabilities."""
102+ ret_val = []
103+ self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
104+ zapctl = ZapperControlV1(self._mocked_conn)
105+ with patch('builtins.print') as mocked_print:
106+ zapctl.get_capabilities()
107+ mocked_print.assert_called_once_with('')
108+
109+ def test_get_capabilities_multiple_caps(self):
110+ """
111+ Check if get_capabilities properly prints multiple records.
112+
113+ The records should be in Checkbox resource syntax. Records should be
114+ separated by an empty line.
115+ """
116+ ret_val = [{'foo': 'bar'}, {'baz': 'biz'}]
117+ self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
118+ zapctl = ZapperControlV1(self._mocked_conn)
119+ with patch('builtins.print') as mocked_print:
120+ zapctl.get_capabilities()
121+ mocked_print.assert_called_once_with('foo: bar\n\nbaz: biz')
122+
123+ def test_get_capabilities_one_cap_multi_rows(self):
124+ """
125+ Check if get_capabilities properly prints a record with multiple caps.
126+
127+ Each capability should be printed in a separate line.
128+ No additional newlines should be printed.
129+ """
130+ ret_val = [{'foo': 'bar', 'foo2': 'bar2'}]
131+ self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
132+ zapctl = ZapperControlV1(self._mocked_conn)
133+ with patch('builtins.print') as mocked_print:
134+ zapctl.get_capabilities()
135+ mocked_print.assert_called_once_with('foo: bar\nfoo2: bar2')
136diff --git a/checkbox_support/scripts/zapper_proxy.py b/checkbox_support/scripts/zapper_proxy.py
137index afd998f..e591f71 100644
138--- a/checkbox_support/scripts/zapper_proxy.py
139+++ b/checkbox_support/scripts/zapper_proxy.py
140@@ -57,10 +57,17 @@ class IZapperControl:
141 :param str state: state to set on the USBMUX addon
142 """
143
144+ @abstractmethod
145+ def get_capabilities(self):
146+ """Get Zapper's setup capabilities in checkbox resource form."""
147+
148
149 class ZapperControlV1(IZapperControl):
150- """Control Zapper via RPyC using v1 of the API."""
151+ """
152+ Control Zapper via RPyC using v1 of the API.
153
154+ :return str: list of capabilities in Checkbox resource form.
155+ """
156 def __init__(self, connection):
157 self._conn = connection
158
159@@ -73,28 +80,84 @@ class ZapperControlV1(IZapperControl):
160 print("State for address {} is {}".format(address, ret[0]))
161
162 def usb_set_state(self, address, state):
163- success = self._conn.root.zombiemux_get_state(address, state)
164+ success = self._conn.root.zombiemux_set_state(address, state)
165 if not success:
166 raise SystemExit(
167 "Failed to set '{}' state for address {}.".format(
168 state, address))
169 print("State '{}' set for the address {}.".format(state, address))
170
171+ def get_capabilities(self):
172+ capabilities = self._conn.root.get_capabilities()
173+
174+ def stringify_cap(cap):
175+ return '\n'.join(
176+ '{}: {}'.format(key, val) for key, val in sorted(cap.items()))
177+ print('\n\n'.join(stringify_cap(cap) for cap in capabilities))
178+
179+
180+class ControlVersionDecider:
181+ """
182+ This class helps establish which API version of Zapper Control to use.
183+
184+ Normally it would be just in the main function, but some of the clients of
185+ this functionality may be internal code from checkbox-support, this class
186+ makes it possible to use the code without spawning new python process
187+ (processing args etc.)
188+
189+ It is a class and not a function, because we should inform the user about
190+ missing RPyC module as soon as possible, in this case in the __init__. The
191+ RPyC is kept in the instance state, so later on it may be used to run the
192+ actual functionality.
193+ """
194+ def __init__(self):
195+ # to not make checkbox-support dependant on RPyC let's use one
196+ # available in the system, and if it's not available let's try loading
197+ # one provided by Checkbox. Real world usecase would be to run this
198+ # program from within Checkbox, so chances for not finding it are
199+ # pretty slim.
200+ try:
201+ self._rpyc = import_module('rpyc')
202+ except ImportError:
203+ try:
204+ self._rpyc = import_module('plainbox.vendor.rpyc')
205+ except ImportError as exc:
206+ msg = "RPyC not found. Neither from sys nor from Checkbox"
207+ raise SystemExit(msg) from exc
208+
209+ def decide(self, host):
210+ """
211+ Determine which version of Zapper Control API to use.
212+
213+ :param str host: Address of the Zapper host to connect to.
214+ :returns IZapperControl: An appropriate ZapperControl instance.
215+ """
216+ conn = self._rpyc.connect(
217+ host, 60000, config={"allow_all_attrs": True})
218+ try:
219+ version = conn.root.get_api_version()
220+ except AttributeError:
221+ # there was no "get_api_version" method on Zapper
222+ # so this means the oldest version possible - 1
223+ version = 1
224+ # the following mapping could be replaced by something that generates
225+ # a class name and tries looking it up in this module, but using dict
226+ # feels simpler due to explicitness and can include classes defined in
227+ # some other modules
228+ control_cls = {
229+ 1: ZapperControlV1,
230+ }.get(version, None)
231+ if control_cls is None:
232+ raise SystemExit((
233+ "Zapper host returned unknown Zapper Control Version: {ver}\n"
234+ "Implement ZapperControlV{ver} in checkbox_support!"
235+ ).format(ver=version))
236+ return control_cls(conn)
237+
238
239 def main():
240 """Entry point."""
241- # to not make checkbox-support dependant on RPyC let's use one available in
242- # the system, and if it's not available let's try loading one provided by
243- # Checkbox. Real world usecase would be to run this program from within
244- # Checkbox, so chances for not finding it are pretty slim.
245- try:
246- rpyc = import_module('rpyc')
247- except ImportError:
248- try:
249- rpyc = import_module('plainbox.vendor.rpyc')
250- except ImportError as exc:
251- raise SystemExit(
252- "RPyC not found. Neither from sys nor from Checkbox") from exc
253+ decider = ControlVersionDecider()
254
255 # generate argparse from the interface of Zapper Control
256 parser = AutoArgParser(cls=IZapperControl)
257@@ -112,28 +175,7 @@ def main():
258 raise SystemExit(
259 "You have to provide Zapper host, either via '--host' or via "
260 "ZAPPER_ADDRESS environment variable")
261- # connect and see which protol version should be used
262- conn = rpyc.connect(
263- host, 60000, config={"allow_all_attrs": True})
264- try:
265- version = conn.root.get_api_version()
266- except AttributeError:
267- # there was no "get_api_version" method on Zapper
268- # so this means the oldest version possible - 1
269- version = 1
270- # the following mapping could be replaced by something that generates
271- # a class name and tries looking it up in this module, but using dict
272- # feels simpler due to explicitness and can include classes defined in
273- # some other modules
274- control_cls = {
275- 1: ZapperControlV1,
276- }.get(version, None)
277- if control_cls is None:
278- raise SystemExit((
279- "Zapper host returned unknown Zapper Control Version: {ver}\n"
280- "Implement ZapperControlV{ver} in checkbox_support!"
281- ).format(ver=version))
282- zapper_control = control_cls(conn)
283+ zapper_control = decider.decide(host)
284 parser.run(zapper_control)
285
286

Subscribers

People subscribed via source and target branches