Merge lp:~brandontschaefer/libertine/ll-refactor-part-1 into lp:libertine
- ll-refactor-part-1
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Christopher Townsend |
Approved revision: | 269 |
Merged at revision: | 268 |
Proposed branch: | lp:~brandontschaefer/libertine/ll-refactor-part-1 |
Merge into: | lp:libertine |
Diff against target: |
285 lines (+80/-67) 6 files modified
python/libertine/Libertine.py (+48/-0) python/libertine/__init__.py (+2/-1) tests/unit/CMakeLists.txt (+7/-11) tests/unit/libertine_launch_tests.py (+12/-34) tools/libertine-launch (+5/-20) tools/libertine-session-bridge (+6/-1) |
To merge this branch: | bzr merge lp:~brandontschaefer/libertine/ll-refactor-part-1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Townsend | Approve | ||
Libertine CI Bot | continuous-integration | Approve | |
Review via email: mp+298470@code.launchpad.net |
Commit message
This creates a LibertineApplic
Description of the change
This creates a LibertineApplic
Note this needs testing on desktop and phone!
Libertine CI Bot (libertine-ci-bot) wrote : | # |
- 260. By Brandon Schaefer
-
* Add some missing env setup for CI
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:260
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 261. By Brandon Schaefer
-
* More config...
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:261
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 262. By Brandon Schaefer
-
* Merge trunk fix conflict
Libertine CI Bot (libertine-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:262
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 263. By Brandon Schaefer
-
* We are just testing ...
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:263
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 264. By Brandon Schaefer
-
* Yell if the container doesnt exist
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:264
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 265. By Brandon Schaefer
-
* Remove redundant test
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:265
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Christopher Townsend (townsend) wrote : | # |
Made some inline comments about the code itself. I will test it on some devices to make sure it works as expected:)
- 266. By Brandon Schaefer
-
* Remove extra line
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:266
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Christopher Townsend (townsend) wrote : | # |
Just a quick nitpick below. Still need to test...
Christopher Townsend (townsend) wrote : | # |
Hmm, seems the session bridge socket gets left behind with this.
Brandon Schaefer (brandontschaefer) wrote : | # |
Hmm always, when the maliit server is around or not? O Dang i need to commit diff comments urg...
- 267. By Brandon Schaefer
-
* Merge trunk
- 268. By Brandon Schaefer
-
* Remove white space
Brandon Schaefer (brandontschaefer) wrote : | # |
Hmm i fully built and installed python3-libertine and libertine-tools which should cover the parts i changed. Socket is being cleaned up when no maliit server.... Not sure what youre seeing :(
Brandon Schaefer (brandontschaefer) wrote : | # |
Tested by, disabling maliit server on start up for me. Ran gedit, checked the dbus socket was there. Closed gedit, and checked the socket was not there
Christopher Townsend (townsend) wrote : | # |
Ok, I think I figured out how to reproduce the issue I see.
Instead of using the 'X' button to close the window, use the actual program's way of closing.
For example, in gedit, go to File->Quit and then the socket is left hanging around. I often use this method as it will cleanly shut down an app unlike using the 'X' button in the window which just kills it.
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:268
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 269. By Brandon Schaefer
-
* If we read and our socket is closed lets clean it up
Libertine CI Bot (libertine-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:269
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
Christopher Townsend (townsend) wrote : | # |
Ok, this seems to work and the code is good. Thanks!
Preview Diff
1 | === modified file 'python/libertine/Libertine.py' |
2 | --- python/libertine/Libertine.py 2016-07-07 15:59:49 +0000 |
3 | +++ python/libertine/Libertine.py 2016-07-08 00:12:35 +0000 |
4 | @@ -18,7 +18,9 @@ |
5 | import contextlib |
6 | import libertine.utils |
7 | import os |
8 | +import psutil |
9 | import shutil |
10 | +import shlex |
11 | |
12 | from libertine.ContainersConfig import ContainersConfig |
13 | from libertine.HostInfo import HostInfo |
14 | @@ -396,3 +398,49 @@ |
15 | return self.container.configure_command(command, *args) |
16 | except RuntimeError as e: |
17 | return handle_runtime_error(e) |
18 | + |
19 | + |
20 | +class LibertineApplication(object): |
21 | + """ |
22 | + Launches a libertine container with a session bridge for sockets such as dbus |
23 | + |
24 | + :param container_id: The container id. |
25 | + "param app_exec_line: The exec line used to start the app in the container. |
26 | + """ |
27 | + def __init__(self, container_id, app_exec_line): |
28 | + self.container_id = container_id |
29 | + self.app_exec_line = app_exec_line |
30 | + self.session_bridge = None |
31 | + |
32 | + """ |
33 | + Launches the libertine session bridge. This creates a proxy socket to read to and from |
34 | + for abstract sockets such as dbus. |
35 | + |
36 | + :param session_socket_paths: A list of socket paths the session will create. |
37 | + """ |
38 | + def launch_session_bridge(self, session_socket_paths): |
39 | + session_bridge_arguments = '' |
40 | + for paths in session_socket_paths: |
41 | + session_bridge_arguments += paths + ' ' |
42 | + |
43 | + libertine_session_bridge_cmd = "libertine-session-bridge " + session_bridge_arguments |
44 | + |
45 | + args = shlex.split(libertine_session_bridge_cmd) |
46 | + self.session_bridge = psutil.Popen(args) |
47 | + |
48 | + """ |
49 | + Launches the container from the id and attempts to run the application exec. |
50 | + """ |
51 | + def launch_application(self): |
52 | + if not ContainersConfig().container_exists(self.container_id): |
53 | + raise RuntimeError("Container ID %s does not exist." % self.container_id) |
54 | + |
55 | + container = LibertineContainer(self.container_id) |
56 | + |
57 | + try: |
58 | + container.launch_application(self.app_exec_line) |
59 | + except: |
60 | + raise |
61 | + finally: |
62 | + if self.session_bridge is not None: |
63 | + self.session_bridge.terminate() |
64 | |
65 | === modified file 'python/libertine/__init__.py' |
66 | --- python/libertine/__init__.py 2015-11-23 20:04:30 +0000 |
67 | +++ python/libertine/__init__.py 2016-07-08 00:12:35 +0000 |
68 | @@ -22,9 +22,10 @@ |
69 | |
70 | __all__ = [ |
71 | # from Libertine |
72 | - 'LibertineContainer', 'utils' |
73 | + 'LibertineContainer', 'utils', 'LibertineApplication' |
74 | ] |
75 | |
76 | __docformat__ = "restructuredtext en" |
77 | |
78 | from libertine.Libertine import LibertineContainer |
79 | +from libertine.Libertine import LibertineApplication |
80 | |
81 | === modified file 'tests/unit/CMakeLists.txt' |
82 | --- tests/unit/CMakeLists.txt 2016-06-27 16:41:52 +0000 |
83 | +++ tests/unit/CMakeLists.txt 2016-07-08 00:12:35 +0000 |
84 | @@ -44,14 +44,10 @@ |
85 | ENVIRONMENT |
86 | "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
87 | |
88 | -set(DISABLED 1) |
89 | - |
90 | -if (NOT DISABLED) |
91 | - add_test(test_libertine_launch |
92 | - "/usr/bin/python3" "-m" "testtools.run" "libertine_launch_tests" |
93 | - ) |
94 | - set_tests_properties(test_libertine_launch |
95 | - PROPERTIES |
96 | - ENVIRONMENT |
97 | - "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
98 | -endif() |
99 | +add_test(test_libertine_launch |
100 | + "/usr/bin/python3" "-m" "testtools.run" "libertine_launch_tests" |
101 | +) |
102 | +set_tests_properties(test_libertine_launch |
103 | + PROPERTIES |
104 | + ENVIRONMENT |
105 | + "GI_TYPELIB_PATH=${CMAKE_BINARY_DIR}/liblibertine;LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/liblibertine:${LD_LIBRARY_PATH};CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR};PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/python;CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}") |
106 | |
107 | === modified file 'tests/unit/libertine_launch_tests.py' |
108 | --- tests/unit/libertine_launch_tests.py 2015-12-18 20:31:46 +0000 |
109 | +++ tests/unit/libertine_launch_tests.py 2016-07-08 00:12:35 +0000 |
110 | @@ -18,34 +18,32 @@ |
111 | import subprocess |
112 | import tempfile |
113 | |
114 | +from libertine import LibertineApplication |
115 | + |
116 | from testtools import TestCase |
117 | from testtools.matchers import Equals, NotEquals |
118 | |
119 | class TestLibertineLaunch(TestCase): |
120 | - |
121 | def setUp(self): |
122 | super(TestLibertineLaunch, self).setUp() |
123 | self.cmake_source_dir = os.environ['CMAKE_SOURCE_DIR'] |
124 | self.cmake_binary_dir = os.environ['CMAKE_BINARY_DIR'] |
125 | |
126 | - # Set the paths to the config file |
127 | container_config_path = os.path.join(self.cmake_binary_dir, 'tests', 'unit', 'libertine-config') |
128 | - container_config_file = os.path.join(container_config_path, 'libertine', 'ContainersConfig.json') |
129 | |
130 | # Set necessary enviroment variables |
131 | os.environ['XDG_DATA_HOME'] = container_config_path |
132 | os.environ['XDG_RUNTIME_DIR'] = tempfile.mkdtemp() |
133 | - os.environ['DISPLAY'] = ':0' |
134 | os.environ['PATH'] = (self.cmake_source_dir + '/tests/mocks:' + |
135 | self.cmake_source_dir + '/tools:' + os.environ['PATH']) |
136 | |
137 | self.addCleanup(self.cleanup) |
138 | |
139 | - # Make a mock container |
140 | + # Lets figure out how to really mock this.... |
141 | cli_cmd = self.cmake_source_dir + '/tools/libertine-container-manager create -i test -n Test -t mock' |
142 | args = shlex.split(cli_cmd) |
143 | subprocess.Popen(args).wait() |
144 | - |
145 | + |
146 | def cleanup(self): |
147 | shutil.rmtree(os.environ['XDG_RUNTIME_DIR']) |
148 | |
149 | @@ -53,46 +51,26 @@ |
150 | ''' |
151 | Base line test to ensure launching an app in an existing container works. |
152 | ''' |
153 | - cli_cmd = self.cmake_source_dir + '/tools/libertine-launch test true' |
154 | - args = shlex.split(cli_cmd) |
155 | - p = subprocess.Popen(args) |
156 | - p.wait() |
157 | - |
158 | - self.assertThat(p.returncode, Equals(0)) |
159 | + la = LibertineApplication('test', 'true') |
160 | + la.launch_application() |
161 | |
162 | def test_launch_app_nonexistent_container(self): |
163 | ''' |
164 | Test to make sure that things gracefully handle a non-existing container. |
165 | ''' |
166 | - cli_cmd = self.cmake_source_dir + '/tools/libertine-launch test1 true' |
167 | - args = shlex.split(cli_cmd) |
168 | - p = subprocess.Popen(args) |
169 | - p.wait() |
170 | - |
171 | - # Should fail due to nonexistent container |
172 | - self.assertThat(p.returncode, Equals(1)) |
173 | + la = LibertineApplication('test1', 'true') |
174 | + self.assertRaises(RuntimeError, la.launch_application) |
175 | |
176 | def test_launch_good_app(self): |
177 | ''' |
178 | Test to make sure that launching an app actually works. |
179 | ''' |
180 | - cli_cmd = self.cmake_source_dir + '/tools/libertine-launch test mock_app' |
181 | - args = shlex.split(cli_cmd) |
182 | - p = subprocess.Popen(args) |
183 | - p.wait() |
184 | - |
185 | - self.assertThat(p.returncode, Equals(0)) |
186 | - self.assertThat(os.path.exists(os.path.join(os.environ['XDG_RUNTIME_DIR'], 'mock')), Equals(True)) |
187 | - |
188 | - os.remove(os.path.join(os.path.join(os.environ['XDG_RUNTIME_DIR'], 'mock'))) |
189 | + la = LibertineApplication('test', 'mock_app') |
190 | + la.launch_application() |
191 | |
192 | def test_launch_bad_app(self): |
193 | ''' |
194 | Test to make sure launching an app that doesn't exist doesn't break things |
195 | ''' |
196 | - cli_cmd = self.cmake_source_dir + '/tools/libertine-launch test foo' |
197 | - args = shlex.split(cli_cmd) |
198 | - p = subprocess.Popen(args) |
199 | - p.wait() |
200 | - |
201 | - self.assertThat(p.returncode, Equals(1)) |
202 | + la = LibertineApplication('test', 'foo') |
203 | + self.assertRaises(FileNotFoundError, la.launch_application) |
204 | |
205 | === modified file 'tools/libertine-launch' |
206 | --- tools/libertine-launch 2016-06-28 20:16:55 +0000 |
207 | +++ tools/libertine-launch 2016-07-08 00:12:35 +0000 |
208 | @@ -21,11 +21,9 @@ |
209 | import random |
210 | import string |
211 | import libertine.utils |
212 | -import psutil |
213 | import shlex |
214 | import time |
215 | - |
216 | -from libertine import LibertineContainer |
217 | +from libertine import LibertineApplication |
218 | |
219 | |
220 | def get_session_socket_path(session_socket_name): |
221 | @@ -60,13 +58,6 @@ |
222 | set_env_socket_path(socket_path, 'MALIIT_SERVER_ADDRESS') |
223 | |
224 | |
225 | -def launch_libertine_session_bridge(session_socket_path): |
226 | - libertine_session_bridge_cmd = "libertine-session-bridge " + session_socket_path |
227 | - |
228 | - args = shlex.split(libertine_session_bridge_cmd) |
229 | - return psutil.Popen(args) |
230 | - |
231 | - |
232 | def detect_session_bridge_socket(session_socket_path): |
233 | retries = 0 |
234 | |
235 | @@ -87,6 +78,8 @@ |
236 | help='exec line') |
237 | args = arg_parser.parse_args() |
238 | |
239 | + la = LibertineApplication(args.container_id, args.app_exec_line) |
240 | + |
241 | # remove problematic environment variables |
242 | for e in ['QT_QPA_PLATFORM', 'LD_LIBRARY_PATH', 'FAKECHROOT_BASE', 'FAKECHROOT_CMD_SUBST']: |
243 | if e in os.environ: |
244 | @@ -94,9 +87,8 @@ |
245 | |
246 | dbus_socket_path = get_dbus_session_socket_path() |
247 | maliit_socket_path = get_maliit_session_socket_path() |
248 | - session_bridge_arguments = dbus_socket_path + ' ' + maliit_socket_path |
249 | |
250 | - session_bridge = launch_libertine_session_bridge(session_bridge_arguments) |
251 | + la.launch_session_bridge([dbus_socket_path, maliit_socket_path]) |
252 | |
253 | set_dbus_env_socket_path(dbus_socket_path) |
254 | set_maliit_env_socket_path(maliit_socket_path) |
255 | @@ -104,11 +96,4 @@ |
256 | # should detect the maliit socket, but dont know if its around or not here. |
257 | detect_session_bridge_socket(dbus_socket_path) |
258 | |
259 | - container = LibertineContainer(args.container_id) |
260 | - |
261 | - try: |
262 | - container.launch_application(args.app_exec_line) |
263 | - except: |
264 | - raise |
265 | - finally: |
266 | - session_bridge.terminate() |
267 | + la.launch_application() |
268 | |
269 | === modified file 'tools/libertine-session-bridge' |
270 | --- tools/libertine-session-bridge 2016-06-27 15:59:41 +0000 |
271 | +++ tools/libertine-session-bridge 2016-07-08 00:12:35 +0000 |
272 | @@ -123,7 +123,12 @@ |
273 | accept_new_connection(host_session_socket_path_map[sock], sock) |
274 | |
275 | else: |
276 | - data = sock.recv(4096) |
277 | + try: |
278 | + data = sock.recv(4096) |
279 | + except: |
280 | + close_connections(sock) |
281 | + continue |
282 | + |
283 | if len(data) == 0: |
284 | close_connections(sock) |
285 | continue |
FAILED: Continuous integration, rev:259 /jenkins. canonical. com/libertine/ job/lp- libertine- ci/21/ /jenkins. canonical. com/libertine/ job/build/ 109/console /jenkins. canonical. com/libertine/ job/lp- generic- update- mp/88/console /jenkins. canonical. com/libertine/ job/build- 0-fetch/ 112 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= vivid+overlay/ 97 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= xenial+ overlay/ 97 /jenkins. canonical. com/libertine/ job/build- 1-sourcepkg/ release= yakkety/ 97 /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= vivid+overlay/ 90/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= xenial+ overlay/ 90/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=amd64, release= yakkety/ 90/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= vivid+overlay/ 90/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= xenial+ overlay/ 90/console /jenkins. canonical. com/libertine/ job/build- 2-binpkg/ arch=i386, release= yakkety/ 90/console
https:/
Executed test runs:
FAILURE: https:/
None: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/libertine/ job/lp- libertine- ci/21/rebuild
https:/