Merge ~pieq/bugit/+git/qabro:fix-1887903-gi-snapd into bugit:master

Proposed by Pierre Equoy
Status: Merged
Approved by: Pierre Equoy
Approved revision: c33317e7d9303e13b59184e38ddd8b2a136604e7
Merged at revision: 58d863ddb47bf77078da7a30bf25bfed8fd95e2e
Proposed branch: ~pieq/bugit/+git/qabro:fix-1887903-gi-snapd
Merge into: bugit:master
Diff against target: 418 lines (+222/-82)
8 files modified
CONTRIBUTING.md (+113/-0)
qabro/__init__.py (+4/-4)
qabro/bug_assistant.py (+15/-15)
qabro/utils.py (+7/-62)
setup.cfg (+6/-0)
setup.py (+4/-0)
snap/snapcraft.yaml (+4/-1)
tests/test_bug_assistant.py (+69/-0)
Reviewer Review Type Date Requested Status
Maciej Kisielewski (community) Approve
Devices Certification Bot Pending
Review via email: mp+390065@code.launchpad.net

Description of the change

Summary
=======

This brings two different things:

1. A new way to discuss with Snapd, from using sockets to using gobject-introspection. This was suggested by jamesh 2 years ago[1] (!!!) and finally implemented, mostly to fix an annoying and hard to reproduce bug[2]

2. Some unit tests using pytest, and a contributing guidelines to help newcomers setting up test env and running unit tests.

I recommend reviewing the changes commit per commit for more clarity.

How it was tested
=================

1. Run the latest version of qabro on a device that exhibits bug lp:1887903 bug:

$ sudo snap install qabro --edge --devmode
qabro (edge) 0.14dev from Pierre Equoy (pieq) installed
$ qabro
Traceback (most recent call last):
  File "/snap/qabro/122/bin/qabro", line 33, in <module>
    sys.exit(load_entry_point('qabro==0.14.dev0', 'console_scripts', 'qabro')())
  (...)
  File "/snap/qabro/122/usr/lib/python3.6/json/decoder.py", line 357, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

2. Build a snap from this branch, and install it on the same device:

# locally
$ snapcraft
$ scp qabro_0.14dev_amd64.snap ...

# on the test device
$ sudo snap install qabro_0.14dev_amd64.snap --devmode

3. Run qabro again

$ qabro

This time, the interface shows up!

[1] https://forum.snapcraft.io/t/how-to-communicate-with-run-snapd-socket-using-python/6432/4
[2] https://bugs.launchpad.net/qabro/+bug/1887903

To post a comment you must log in.
Revision history for this message
Pierre Equoy (pieq) wrote :

Following a discussion with Stanley regarding the use of pytest, I've revamped a bit the unit tests in order to parametrize some of them.

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

I got some minor comments below. I really don't like the unit for checking the session path.

Revision history for this message
Pierre Equoy (pieq) wrote :

Thanks a lot for the feedback Maciek!

I commented below, I need your help for some clarification before I can resubmit.

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Comments below.

Revision history for this message
Pierre Equoy (pieq) wrote :

@Maciek: I've revamped the commits (and added one for the setup.py changes) with your suggestions. Let me know what you think. Thanks again!

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

Thanks! Looks great, +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
2new file mode 100644
3index 0000000..d6266ab
4--- /dev/null
5+++ b/CONTRIBUTING.md
6@@ -0,0 +1,113 @@
7+Contributing
8+============
9+
10+
11+Setting up testing environment
12+------------------------------
13+
14+If you want to run qabro without having to build a snap, you will need to setup
15+a virtual environment:
16+
17+```
18+# First, let's install packages required to build some of the dependencies
19+$ sudo apt install libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-3.0 gir1.2-snapd-1
20+$ python3 -m venv venv
21+$ source venv/bin/activate
22+# From now on, you're inside the virtual env
23+# Let's install the Python packages we need to run qabro and the unit tests
24+(venv) $ pip install -e .[test]
25+```
26+
27+
28+Running the unit tests
29+----------------------
30+
31+With a virtual environment set up:
32+
33+```
34+$ python -m pytest
35+```
36+
37+You can also run coverage to get a code coverage HTML report:
38+
39+```
40+$ coverage run -m pytest
41+```
42+
43+
44+Running QABRO from within a virtual environment
45+-----------------------------------------------
46+
47+Then, by using a python script such as this one (e.g. `test-qabro.py`):
48+
49+```
50+#!/usr/bin/env python3
51+import os
52+import qabro
53+
54+os.environ['SNAP_NAME'] = 'qabro'
55+os.environ['APPORT_LAUNCHPAD_INSTANCE'] = 'staging'
56+
57+qabro.main()
58+```
59+
60+you should be able to run QABRO like this:
61+
62+```
63+(venv) $ python test-qabro.py
64+```
65+
66+Note that QABRO depends on several other things, some of them deeply tied to
67+the snap environment, so if you want to test a whole run, it's better to build
68+a test snap.
69+
70+
71+Building a test snap
72+--------------------
73+
74+Install snapcraft and multipass:
75+
76+```
77+$ snap install snapcraft --classic
78+$ multipass
79+```
80+
81+Then, from within the qabro directory:
82+
83+```
84+$ snapcraft
85+Launching a VM.
86+(...)
87+Snapped qabro_0.14dev_amd64.snap
88+```
89+
90+You can then install this snap onto your device or a test device, and run it:
91+
92+```
93+$ snap install qabro_0.14dev_amd64.snap --devmode
94+$ qabro
95+```
96+
97+
98+Submitting a change
99+-------------------
100+
101+Set up a personal git remote for the project:
102+
103+```
104+$ git remote add <myrepo> git+ssh://<username>@git.launchpad.net/~<username>/qabro/+git/qabro
105+```
106+
107+Make your change on a dedicated branch:
108+
109+```
110+$ git checkout -b my-new-branch
111+# do your changes and commit them
112+```
113+
114+Push the branch to your repo, then propose a merge request (Launchpad should
115+indicate a URL to propose the merge request):
116+
117+```
118+$ git push <myrepo>
119+```
120\ No newline at end of file
121diff --git a/README b/README.md
122similarity index 100%
123rename from README
124rename to README.md
125diff --git a/qabro/__init__.py b/qabro/__init__.py
126index 832e3a7..5604dcb 100644
127--- a/qabro/__init__.py
128+++ b/qabro/__init__.py
129@@ -6,7 +6,7 @@ import sys
130
131 from qabro.bug_assistant import BugAssistant, BugReport
132 from qabro.ui import ReportScreen
133-from qabro.utils import snapdsocket
134+import qabro.utils
135
136
137 logger = logging.getLogger(__name__)
138@@ -21,10 +21,10 @@ def main():
139 sys.exit(("Cannot determine snap name. "
140 "Please install this tool using the snap command."))
141
142- ss = snapdsocket()
143- qabro_snap_info = [e for e in ss.get_snaps() if e['name'] == 'qabro'][0]
144+ snaps = qabro.utils.get_snaps()
145+ qabro_snap_info = [e for e in snaps if e.props.name == 'qabro'][0]
146
147- if not qabro_snap_info['devmode']:
148+ if not qabro_snap_info.props.devmode:
149 sys.exit("This tool requires --devmode option to run properly.")
150
151 logfile = os.path.join(os.getenv('SNAP_USER_COMMON', '/tmp'), 'qabro.log')
152diff --git a/qabro/bug_assistant.py b/qabro/bug_assistant.py
153index ed55c9d..6a720b0 100644
154--- a/qabro/bug_assistant.py
155+++ b/qabro/bug_assistant.py
156@@ -10,7 +10,7 @@ from collections import Counter
157 from launchpadlib.launchpad import Launchpad
158 from launchpadlib.uris import LPNET_WEB_ROOT, STAGING_WEB_ROOT, QASTAGING_WEB_ROOT
159
160-from qabro.utils import snapdsocket
161+import qabro.utils
162
163
164 class BugReport:
165@@ -252,35 +252,35 @@ class AttachmentAssistant:
166
167 def attach_snaplist(self):
168 print("Retrieving list of installed snaps...")
169- sckt = snapdsocket()
170- snaps = sckt.get_snaps()
171- fmt = "{:<32}{:<16}{:<16}{:<16}{:<16}{:<16}"
172+ snaps = qabro.utils.get_snaps()
173+ fmt = "{:<32}{:<16}{:<16}{:<16}{:<16}{:<16}\n"
174 out = fmt.format("Name", "Version", "Revision",
175- "Channel", "Devmode", "Confinement") + "\n"
176+ "Channel", "Devmode", "Confinement")
177 for snap in snaps:
178- if snap["devmode"]:
179+ if snap.props.devmode:
180 devmode = "yes"
181 else:
182 devmode = "no"
183- out += fmt.format(snap["name"], snap["version"], snap["revision"],
184- snap.get("tracking-channel", "-"), devmode,
185- snap["confinement"]) + "\n"
186+ tracking_channel = snap.props.tracking_channel or "-"
187+ out += fmt.format(snap.props.name, snap.props.version, snap.props.revision,
188+ tracking_channel, devmode,
189+ snap.props.confinement.value_nick)
190 self.attachments["snap_list.log"] = out.encode()
191
192- def _get_last_checkbox_session_path(self):
193+ def _get_last_checkbox_session_path(self, usr_dir="/home/$USER", root_dir="/root"):
194 """Return path of the most recent Checkbox session directory."""
195 rootpath = ''
196 latest_session_dir = ''
197 # Determine if we use a classic install or a snap install
198- classic_path = os.path.expandvars('/home/$USER/.cache/plainbox/sessions')
199+ classic_path = os.path.expandvars(os.path.join(usr_dir, ".cache/plainbox/sessions"))
200 if os.path.exists(classic_path):
201 rootpath = classic_path
202 else:
203- ss = snapdsocket()
204- snap_list = [e['name'] for e in ss.get_snaps() if 'checkbox-' in e['name']]
205+ snaps = qabro.utils.get_snaps()
206+ snap_list = [e.props.name for e in snaps if 'checkbox-' in e.props.name]
207 for snap_name in snap_list:
208- sp = ["/home/$USER/snap/{}/current/.cache/plainbox/sessions",
209- "/root/snap/{}/current/.cache/plainbox/sessions"]
210+ sp = [os.path.join(usr_dir, "snap/{}/current/.cache/plainbox/sessions"),
211+ os.path.join(root_dir, "snap/{}/current/.cache/plainbox/sessions")]
212 for p in sp:
213 snap_path = os.path.expandvars(p.format(snap_name))
214 if os.path.exists(snap_path):
215diff --git a/qabro/utils.py b/qabro/utils.py
216index af790eb..89845b3 100644
217--- a/qabro/utils.py
218+++ b/qabro/utils.py
219@@ -1,67 +1,12 @@
220 #!/usr/bin/env python3
221 import socket, json, pprint
222
223-class snapdsocket:
224- def __init__(self, sock=None):
225- if sock is None:
226- self.sock = socket.socket(socket.AF_UNIX)
227- else:
228- self.sock = sock
229- self.sock.settimeout(0.3)
230+import gi
231+gi.require_version('Snapd', '1')
232+from gi.repository import Snapd
233
234- def _connect(self):
235- self.sock.connect("/run/snapd.socket")
236
237- def _send(self, msg):
238- if type(msg) != "bytes":
239- msg = msg.encode()
240- self.sock.send(msg)
241-
242- def _recv(self):
243- chunks = []
244- while True:
245- try:
246- chunk = self.sock.recv(2048)
247- chunks.append(chunk.decode())
248- except socket.timeout:
249- break
250- return ''.join(chunks)
251-
252- def _close(self):
253- self.sock.close()
254-
255- def _get_json_response(self, response):
256- begin = response.find('{')
257- end = response.rfind('}') + 1
258- return json.loads(response[begin:end])
259-
260- def request(self, msg):
261- self.__init__()
262- self._connect()
263- self._send(msg)
264- raw_resp = self._recv()
265- self._close()
266- json_resp = self._get_json_response(raw_resp)
267- return json_resp
268-
269- def get_interfaces(self):
270- req = "GET /v2/interfaces HTTP/1.1\r\nHost: localhost\r\n\r\n"
271- json_resp = self.request(req)
272- return json_resp['result']
273-
274- def get_snaps(self):
275- req = "GET /v2/snaps HTTP/1.1\r\nHost: localhost\r\n\r\n"
276- json_resp = self.request(req)
277- return json_resp['result']
278-
279- def get_snap_names(self):
280- req = "GET /v2/snaps HTTP/1.1\r\nHost: localhost\r\n\r\n"
281- json_resp = self.request(req)
282- snap_names = []
283- for s in json_resp["result"]:
284- snap_names.append(s["name"])
285- return snap_names
286-
287-if __name__ == "__main__":
288- sn = snapdsocket()
289- pprint.pprint(sn.get_snaps())
290+def get_snaps():
291+ client = Snapd.Client()
292+ active_snaps_flag = Snapd.GetSnapsFlags(0)
293+ return client.get_snaps_sync(active_snaps_flag, None)
294diff --git a/setup.cfg b/setup.cfg
295new file mode 100644
296index 0000000..50c84a6
297--- /dev/null
298+++ b/setup.cfg
299@@ -0,0 +1,6 @@
300+[tool:pytest]
301+testpaths = tests
302+
303+[coverage:run]
304+branch = True
305+source = qabro
306diff --git a/setup.py b/setup.py
307index d01f071..cd3c091 100644
308--- a/setup.py
309+++ b/setup.py
310@@ -24,6 +24,10 @@ automatically attach relevent information to them (log files).
311 entry_points={
312 'console_scripts': ['qabro = qabro.__init__:main']
313 },
314+ install_requires=['launchpadlib', 'urwid'],
315+ extras_require={
316+ 'test': ['pytest', 'pytest-mock', 'coverage', 'PyGObject'],
317+ },
318 author=about['__author__'],
319 author_email=about['__author_email__'],
320 url=about['__url__'],
321diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
322index de2580d..346125e 100644
323--- a/snap/snapcraft.yaml
324+++ b/snap/snapcraft.yaml
325@@ -10,7 +10,8 @@ apps:
326 qabro:
327 command: qabro
328 environment:
329- LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/lib/fwts:$SNAP/lib/x86_64-linux-gnu
330+ GI_TYPELIB_PATH: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/girepository-1.0
331+ LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/lib/fwts:$SNAP/lib/$SNAPCRAFT_ARCH_TRIPLET
332 LANG: C.UTF-8
333 LC_ALL: C.UTF-8
334
335@@ -42,6 +43,8 @@ parts:
336 - pciutils
337 - python3-urwid
338 - ubuntu-report
339+ - python3-gi
340+ - gir1.2-snapd-1 # LP #1887903
341
342 dpkg:
343 plugin: nil
344diff --git a/tests/test_bug_assistant.py b/tests/test_bug_assistant.py
345new file mode 100644
346index 0000000..752db7d
347--- /dev/null
348+++ b/tests/test_bug_assistant.py
349@@ -0,0 +1,69 @@
350+import pytest
351+from qabro.bug_assistant import AttachmentAssistant
352+import gi
353+gi.require_version('Snapd', '1')
354+from gi.repository import Snapd
355+
356+import os
357+import pprint
358+import sys
359+pprint.pprint(sys.path)
360+
361+
362+@pytest.fixture(scope="session")
363+def get_snaps(session_mocker):
364+ test_snap = session_mocker.MagicMock()
365+ test_snap.props.name = "checkbox-snappy"
366+ test_snap.props.version = "20.15"
367+ test_snap.props.revision = "2035"
368+ test_snap.props.tracking_channel = None
369+ test_snap.props.devmode = True
370+ test_snap.props.confinement.value_nick = "strict"
371+
372+ session_mocker.patch("qabro.utils.get_snaps", return_value=[test_snap, ])
373+
374+
375+@pytest.mark.parametrize("usr_dir",
376+ [".cache/plainbox/sessions",
377+ "snap/checkbox-snappy/current/.cache/plainbox/sessions"])
378+def test_get_last_checkbox_session_path_ubuntu_classic(get_snaps, tmp_path, usr_dir):
379+ for session in ["session1", "session2", "last_session"]:
380+ s = tmp_path / usr_dir / session
381+ s.mkdir(parents=True)
382+ last_session_path = tmp_path / usr_dir / "last_session"
383+ # Add 1 second to last session directory access/modify time to make sure
384+ # it will be seen as the most recent session directory
385+ last_session_time = os.stat(last_session_path).st_mtime + 1
386+ os.utime(last_session_path, times=(last_session_time, last_session_time))
387+ aa = AttachmentAssistant()
388+ last_session = aa._get_last_checkbox_session_path(usr_dir=tmp_path, root_dir="/nothing/here")
389+ assert last_session.endswith("last_session")
390+
391+
392+def test_get_last_checkbox_session_path_ubuntu_core(get_snaps, tmp_path):
393+ root_dir = "snap/checkbox-snappy/current/.cache/plainbox/sessions"
394+ for session in ["session1", "session2", "last_session"]:
395+ s = tmp_path / root_dir / session
396+ s.mkdir(parents=True)
397+ last_session_path = tmp_path / root_dir / "last_session"
398+ # Add 1 second to last session directory access/modify time to make sure
399+ # it will be seen as the most recent session directory
400+ last_session_time = os.stat(last_session_path).st_mtime + 1
401+ os.utime(last_session_path, times=(last_session_time, last_session_time))
402+ aa = AttachmentAssistant()
403+ last_session = aa._get_last_checkbox_session_path(usr_dir="/nothing/here", root_dir=tmp_path)
404+ assert last_session.endswith("last_session")
405+
406+
407+def test_get_last_checkbox_session_path_no_session(get_snaps, tmp_path):
408+ aa = AttachmentAssistant()
409+ last_session = aa._get_last_checkbox_session_path(usr_dir="/nothing/here", root_dir="/nothing/there")
410+ assert last_session == ""
411+
412+
413+def test_attach_snaplist(get_snaps):
414+ aa = AttachmentAssistant()
415+ aa.attach_snaplist()
416+ test_output = (b"Name Version Revision Channel Devmode Confinement \n"
417+ b"checkbox-snappy 20.15 2035 - yes strict \n")
418+ assert aa.attachments["snap_list.log"] == test_output

Subscribers

People subscribed via source and target branches

to all changes: