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
diff --git a/checkbox_support/scripts/tests/test_zapper_proxy.py b/checkbox_support/scripts/tests/test_zapper_proxy.py
0new file mode 1006440new file mode 100644
index 0000000..db3e754
--- /dev/null
+++ b/checkbox_support/scripts/tests/test_zapper_proxy.py
@@ -0,0 +1,129 @@
1# Copyright 2022 Canonical Ltd.
2# Written by:
3# Maciej Kisielewski <maciej.kisielewski@canonical.com>
4#
5# This is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 3,
7# as published by the Free Software Foundation.
8#
9# This file is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this file. If not, see <http://www.gnu.org/licenses/>.
16"""Tests for zapper_proxy module."""
17
18from unittest import TestCase
19from unittest.mock import Mock, patch
20
21from checkbox_support.scripts.zapper_proxy import ZapperControlV1
22
23
24class ZapperProxyV1Tests(TestCase):
25 """Unit tests for ZapperProxyV1 class."""
26
27 def setUp(self):
28 self._mocked_conn = Mock()
29 self._mocked_conn.root = Mock()
30
31 def test_usb_get_state_smoke(self):
32 """
33 Check if usb_get_state calls appropriate function on the rpyc client.
34
35 Current implementation on the service side uses mutable arguments for
36 returning values (C-style stuff that should be removed) this is why we
37 need the stateful side_effect below
38 """
39 def side_effect_fn(_, ret):
40 ret.append('ON')
41 return True
42 self._mocked_conn.root.zombiemux_get_state = Mock(
43 side_effect=side_effect_fn)
44 zapctl = ZapperControlV1(self._mocked_conn)
45
46 with patch('builtins.print') as mocked_print:
47 zapctl.usb_get_state(0)
48 mocked_print.assert_called_once_with(
49 'State for address 0 is ON')
50
51 def test_usb_get_state_fails(self):
52 """Check if usb_get_state quits with a proper message on failure."""
53 self._mocked_conn.root.zombiemux_get_state = Mock(return_value=False)
54 zapctl = ZapperControlV1(self._mocked_conn)
55 with self.assertRaises(SystemExit) as context:
56 zapctl.usb_get_state(0)
57 self.assertEqual(
58 context.exception.code, 'Failed to get state for address 0.')
59
60 def test_usb_set_state_smoke(self):
61 """
62 Check if usb_set_state calls appropriate functions on the rpyc client.
63 """
64 self._mocked_conn.root.zombiemux_set_state = Mock(return_value=True)
65 zapctl = ZapperControlV1(self._mocked_conn)
66 with patch('builtins.print') as mocked_print:
67 zapctl.usb_set_state(0, 'ON')
68 mocked_print.assert_called_once_with(
69 "State 'ON' set for the address 0.")
70
71 def test_usb_set_state_fails(self):
72 """Check if usb_set_state quits with a proper message on failure."""
73 self._mocked_conn.root.zombiemux_set_state = Mock(return_value=False)
74 zapctl = ZapperControlV1(self._mocked_conn)
75 with self.assertRaises(SystemExit) as context:
76 zapctl.usb_set_state(0, 'ON')
77 self.assertEqual(
78 context.exception.code, "Failed to set 'ON' state for address 0.")
79
80 def test_get_capabilities_one_cap(self):
81 """
82 Check if get_capabilities properly prints one record.
83
84 The record should be in Checkbox resource syntax and should not be
85 surrounded by any newlines.
86 """
87 ret_val = [{'foo': 'bar'}]
88 self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
89 zapctl = ZapperControlV1(self._mocked_conn)
90 with patch('builtins.print') as mocked_print:
91 zapctl.get_capabilities()
92 mocked_print.assert_called_once_with('foo: bar')
93
94 def test_get_capabilities_empty(self):
95 """Check if get_capabilities prints nothing on no capabilities."""
96 ret_val = []
97 self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
98 zapctl = ZapperControlV1(self._mocked_conn)
99 with patch('builtins.print') as mocked_print:
100 zapctl.get_capabilities()
101 mocked_print.assert_called_once_with('')
102
103 def test_get_capabilities_multiple_caps(self):
104 """
105 Check if get_capabilities properly prints multiple records.
106
107 The records should be in Checkbox resource syntax. Records should be
108 separated by an empty line.
109 """
110 ret_val = [{'foo': 'bar'}, {'baz': 'biz'}]
111 self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
112 zapctl = ZapperControlV1(self._mocked_conn)
113 with patch('builtins.print') as mocked_print:
114 zapctl.get_capabilities()
115 mocked_print.assert_called_once_with('foo: bar\n\nbaz: biz')
116
117 def test_get_capabilities_one_cap_multi_rows(self):
118 """
119 Check if get_capabilities properly prints a record with multiple caps.
120
121 Each capability should be printed in a separate line.
122 No additional newlines should be printed.
123 """
124 ret_val = [{'foo': 'bar', 'foo2': 'bar2'}]
125 self._mocked_conn.root.get_capabilities = Mock(return_value=ret_val)
126 zapctl = ZapperControlV1(self._mocked_conn)
127 with patch('builtins.print') as mocked_print:
128 zapctl.get_capabilities()
129 mocked_print.assert_called_once_with('foo: bar\nfoo2: bar2')
diff --git a/checkbox_support/scripts/zapper_proxy.py b/checkbox_support/scripts/zapper_proxy.py
index afd998f..e591f71 100644
--- a/checkbox_support/scripts/zapper_proxy.py
+++ b/checkbox_support/scripts/zapper_proxy.py
@@ -57,10 +57,17 @@ class IZapperControl:
57 :param str state: state to set on the USBMUX addon57 :param str state: state to set on the USBMUX addon
58 """58 """
5959
60 @abstractmethod
61 def get_capabilities(self):
62 """Get Zapper's setup capabilities in checkbox resource form."""
63
6064
61class ZapperControlV1(IZapperControl):65class ZapperControlV1(IZapperControl):
62 """Control Zapper via RPyC using v1 of the API."""66 """
67 Control Zapper via RPyC using v1 of the API.
6368
69 :return str: list of capabilities in Checkbox resource form.
70 """
64 def __init__(self, connection):71 def __init__(self, connection):
65 self._conn = connection72 self._conn = connection
6673
@@ -73,28 +80,84 @@ class ZapperControlV1(IZapperControl):
73 print("State for address {} is {}".format(address, ret[0]))80 print("State for address {} is {}".format(address, ret[0]))
7481
75 def usb_set_state(self, address, state):82 def usb_set_state(self, address, state):
76 success = self._conn.root.zombiemux_get_state(address, state)83 success = self._conn.root.zombiemux_set_state(address, state)
77 if not success:84 if not success:
78 raise SystemExit(85 raise SystemExit(
79 "Failed to set '{}' state for address {}.".format(86 "Failed to set '{}' state for address {}.".format(
80 state, address))87 state, address))
81 print("State '{}' set for the address {}.".format(state, address))88 print("State '{}' set for the address {}.".format(state, address))
8289
90 def get_capabilities(self):
91 capabilities = self._conn.root.get_capabilities()
92
93 def stringify_cap(cap):
94 return '\n'.join(
95 '{}: {}'.format(key, val) for key, val in sorted(cap.items()))
96 print('\n\n'.join(stringify_cap(cap) for cap in capabilities))
97
98
99class ControlVersionDecider:
100 """
101 This class helps establish which API version of Zapper Control to use.
102
103 Normally it would be just in the main function, but some of the clients of
104 this functionality may be internal code from checkbox-support, this class
105 makes it possible to use the code without spawning new python process
106 (processing args etc.)
107
108 It is a class and not a function, because we should inform the user about
109 missing RPyC module as soon as possible, in this case in the __init__. The
110 RPyC is kept in the instance state, so later on it may be used to run the
111 actual functionality.
112 """
113 def __init__(self):
114 # to not make checkbox-support dependant on RPyC let's use one
115 # available in the system, and if it's not available let's try loading
116 # one provided by Checkbox. Real world usecase would be to run this
117 # program from within Checkbox, so chances for not finding it are
118 # pretty slim.
119 try:
120 self._rpyc = import_module('rpyc')
121 except ImportError:
122 try:
123 self._rpyc = import_module('plainbox.vendor.rpyc')
124 except ImportError as exc:
125 msg = "RPyC not found. Neither from sys nor from Checkbox"
126 raise SystemExit(msg) from exc
127
128 def decide(self, host):
129 """
130 Determine which version of Zapper Control API to use.
131
132 :param str host: Address of the Zapper host to connect to.
133 :returns IZapperControl: An appropriate ZapperControl instance.
134 """
135 conn = self._rpyc.connect(
136 host, 60000, config={"allow_all_attrs": True})
137 try:
138 version = conn.root.get_api_version()
139 except AttributeError:
140 # there was no "get_api_version" method on Zapper
141 # so this means the oldest version possible - 1
142 version = 1
143 # the following mapping could be replaced by something that generates
144 # a class name and tries looking it up in this module, but using dict
145 # feels simpler due to explicitness and can include classes defined in
146 # some other modules
147 control_cls = {
148 1: ZapperControlV1,
149 }.get(version, None)
150 if control_cls is None:
151 raise SystemExit((
152 "Zapper host returned unknown Zapper Control Version: {ver}\n"
153 "Implement ZapperControlV{ver} in checkbox_support!"
154 ).format(ver=version))
155 return control_cls(conn)
156
83157
84def main():158def main():
85 """Entry point."""159 """Entry point."""
86 # to not make checkbox-support dependant on RPyC let's use one available in160 decider = ControlVersionDecider()
87 # the system, and if it's not available let's try loading one provided by
88 # Checkbox. Real world usecase would be to run this program from within
89 # Checkbox, so chances for not finding it are pretty slim.
90 try:
91 rpyc = import_module('rpyc')
92 except ImportError:
93 try:
94 rpyc = import_module('plainbox.vendor.rpyc')
95 except ImportError as exc:
96 raise SystemExit(
97 "RPyC not found. Neither from sys nor from Checkbox") from exc
98161
99 # generate argparse from the interface of Zapper Control162 # generate argparse from the interface of Zapper Control
100 parser = AutoArgParser(cls=IZapperControl)163 parser = AutoArgParser(cls=IZapperControl)
@@ -112,28 +175,7 @@ def main():
112 raise SystemExit(175 raise SystemExit(
113 "You have to provide Zapper host, either via '--host' or via "176 "You have to provide Zapper host, either via '--host' or via "
114 "ZAPPER_ADDRESS environment variable")177 "ZAPPER_ADDRESS environment variable")
115 # connect and see which protol version should be used178 zapper_control = decider.decide(host)
116 conn = rpyc.connect(
117 host, 60000, config={"allow_all_attrs": True})
118 try:
119 version = conn.root.get_api_version()
120 except AttributeError:
121 # there was no "get_api_version" method on Zapper
122 # so this means the oldest version possible - 1
123 version = 1
124 # the following mapping could be replaced by something that generates
125 # a class name and tries looking it up in this module, but using dict
126 # feels simpler due to explicitness and can include classes defined in
127 # some other modules
128 control_cls = {
129 1: ZapperControlV1,
130 }.get(version, None)
131 if control_cls is None:
132 raise SystemExit((
133 "Zapper host returned unknown Zapper Control Version: {ver}\n"
134 "Implement ZapperControlV{ver} in checkbox_support!"
135 ).format(ver=version))
136 zapper_control = control_cls(conn)
137 parser.run(zapper_control)179 parser.run(zapper_control)
138180
139181

Subscribers

People subscribed via source and target branches