Merge lp:~stolowski/unity-scope-click/two-columns-layout into lp:unity-scope-click
- two-columns-layout
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | dobey | ||||
Approved revision: | 415 | ||||
Merged at revision: | 407 | ||||
Proposed branch: | lp:~stolowski/unity-scope-click/two-columns-layout | ||||
Merge into: | lp:unity-scope-click | ||||
Diff against target: |
451 lines (+238/-12) 3 files modified
libclickscope/click/preview.cpp (+121/-10) libclickscope/click/preview.h (+38/-1) libclickscope/tests/test_preview.cpp (+79/-1) |
||||
To merge this branch: | bzr merge lp:~stolowski/unity-scope-click/two-columns-layout | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
PS Jenkins bot | continuous-integration | Needs Fixing | |
dobey (community) | Approve | ||
Review via email: mp+283670@code.launchpad.net |
Commit message
Support two column layouts in the previews.
Description of the change
Support two column layouts in the previews.
PS Jenkins bot (ps-jenkins) wrote : | # |
- 410. By Paweł Stołowski
-
Fixed InstallingPreview
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:410
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
dobey (dobey) wrote : | # |
I'm a bit worried that we have multiple new methods being added here, and some changes to some previews, but there are no changes to the tests. Can we get some tests added for the new methods, and tests that ensure the correct version of the pushPackagePrev
- 411. By Paweł Stołowski
-
pushWidgets method not needed after all
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:411
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 412. By Paweł Stołowski
-
Check that register_layout is called
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:412
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 413. By Paweł Stołowski
-
Basic check for widget caching
- 414. By Paweł Stołowski
-
Real test for column layout and cache
- 415. By Paweł Stołowski
-
Match preview widget ids too
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:414
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Paweł Stołowski (stolowski) wrote : | # |
> I'm a bit worried that we have multiple new methods being added here, and some
> changes to some previews, but there are no changes to the tests. Can we get
> some tests added for the new methods, and tests that ensure the correct
> version of the pushPackagePrev
> preview types, perhaps?
Ok, I added some tests, mostly for the new pushPackagePrev
The best test would be to just set call expectations on register_layout() and push() of the reply objects, but that's not possible without spendings tons of time on enhancing current tests. The problem is it's not currently easily possible with InstalledPreview test, because of the entire "run with qt" story in InstalledPrevie
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:415
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Preview Diff
1 | === modified file 'libclickscope/click/preview.cpp' |
2 | --- libclickscope/click/preview.cpp 2015-11-24 18:23:23 +0000 |
3 | +++ libclickscope/click/preview.cpp 2016-01-28 14:37:47 +0000 |
4 | @@ -45,6 +45,7 @@ |
5 | #include <unity/scopes/PreviewReply.h> |
6 | #include <unity/scopes/Variant.h> |
7 | #include <unity/scopes/VariantBuilder.h> |
8 | +#include <unity/scopes/ColumnLayout.h> |
9 | |
10 | #include <QDebug> |
11 | |
12 | @@ -56,6 +57,70 @@ |
13 | |
14 | namespace click { |
15 | |
16 | +void CachedPreviewWidgets::push(unity::scopes::PreviewWidget const &widget) |
17 | +{ |
18 | + widgets.push_back(widget); |
19 | + widgets_lookup.insert(widget.id()); |
20 | +} |
21 | + |
22 | +void CachedPreviewWidgets::push(unity::scopes::PreviewWidgetList const &widgetList) |
23 | +{ |
24 | + for (auto const& widget: widgetList) |
25 | + { |
26 | + push(widget); |
27 | + } |
28 | +} |
29 | + |
30 | +void CachedPreviewWidgets::flush(unity::scopes::PreviewReplyProxy const& reply) |
31 | +{ |
32 | + // A safeguard: if a new widget gets added but missing in the layout, we will get a warning |
33 | + // in the logs and layouts will not be registered (single column with all widgets will be used). |
34 | + if (widgets.size() != layout.singleColumn.column1.size() || |
35 | + widgets.size() != layout.twoColumns.column1.size() + layout.twoColumns.column2.size()) |
36 | + { |
37 | + qWarning() << "Number of column layouts doesn't match the number of widgets"; |
38 | + } |
39 | + else |
40 | + { |
41 | + layout.registerLayouts(reply); |
42 | + } |
43 | + reply->push(widgets); |
44 | + widgets.clear(); |
45 | + widgets_lookup.clear(); |
46 | +} |
47 | + |
48 | +bool CachedPreviewWidgets::has(std::string const& widget) const |
49 | +{ |
50 | + return widgets_lookup.find(widget) != widgets_lookup.end(); |
51 | +} |
52 | + |
53 | +void WidgetsInColumns::registerLayouts(unity::scopes::PreviewReplyProxy const& reply) |
54 | +{ |
55 | + unity::scopes::ColumnLayout layout1col(1); |
56 | + layout1col.add_column(singleColumn.column1); |
57 | + |
58 | + unity::scopes::ColumnLayout layout2col(2); |
59 | + layout2col.add_column(twoColumns.column1); |
60 | + layout2col.add_column(twoColumns.column2); |
61 | + |
62 | + try |
63 | + { |
64 | + reply->register_layout({layout1col, layout2col}); |
65 | + } |
66 | + catch (unity::LogicException const& e) |
67 | + { |
68 | + qWarning() << "Failed to register layout:" << QString::fromStdString(e.what()); |
69 | + } |
70 | +} |
71 | + |
72 | +void WidgetsInColumns::appendToColumn(std::vector<std::string>& column, unity::scopes::PreviewWidgetList const& widgets) |
73 | +{ |
74 | + for (auto const& widget: widgets) |
75 | + { |
76 | + column.push_back(widget.id()); |
77 | + } |
78 | +} |
79 | + |
80 | DepartmentUpdater::DepartmentUpdater(const std::shared_ptr<click::DepartmentsDb>& depts) |
81 | : depts(depts) |
82 | { |
83 | @@ -237,6 +302,39 @@ |
84 | reply->push(descriptionWidgets(details)); |
85 | } |
86 | |
87 | +void PreviewStrategy::pushPackagePreviewWidgets(CachedPreviewWidgets &cache, |
88 | + const PackageDetails& details, |
89 | + const scopes::PreviewWidgetList& button_area_widgets) |
90 | +{ |
91 | + cache.push(headerWidgets(details)); |
92 | + cache.layout.singleColumn.column1.push_back("hdr"); |
93 | + cache.layout.twoColumns.column1.push_back("hdr"); |
94 | + |
95 | + cache.push(button_area_widgets); |
96 | + cache.layout.appendToColumn(cache.layout.singleColumn.column1, button_area_widgets); |
97 | + cache.layout.appendToColumn(cache.layout.twoColumns.column1, button_area_widgets); |
98 | + |
99 | + auto const screenshots = screenshotsWidgets(details); |
100 | + cache.push(screenshots); |
101 | + cache.layout.appendToColumn(cache.layout.singleColumn.column1, screenshots); |
102 | + cache.layout.appendToColumn(cache.layout.twoColumns.column1, screenshots); |
103 | + |
104 | + auto descr = descriptionWidgets(details); |
105 | + if (!descr.empty()) |
106 | + { |
107 | + cache.push(descr); |
108 | + cache.layout.appendToColumn(cache.layout.singleColumn.column1, descr); |
109 | + |
110 | + // for two-columns we need to split the widgets, summary goes to 1st column, everything else to 2nd |
111 | + if (descr.front().id() == "summary") |
112 | + { |
113 | + descr.pop_front(); |
114 | + cache.layout.twoColumns.column1.push_back("summary"); |
115 | + } |
116 | + cache.layout.appendToColumn(cache.layout.twoColumns.column2, descr); |
117 | + } |
118 | +} |
119 | + |
120 | PreviewStrategy::~PreviewStrategy() |
121 | { |
122 | } |
123 | @@ -463,7 +561,7 @@ |
124 | { |
125 | scopes::PreviewWidgetList widgets; |
126 | |
127 | - scopes::PreviewWidget rating("summary", "reviews"); |
128 | + scopes::PreviewWidget rating("reviews", "reviews"); |
129 | scopes::VariantBuilder builder; |
130 | |
131 | if (reviewlist.size() > 0) { |
132 | @@ -647,7 +745,7 @@ |
133 | if (login_error) { |
134 | reply->push(loginErrorWidgets(details)); |
135 | } else { |
136 | - pushPackagePreviewWidgets(reply, details, progressBarWidget(object_path)); |
137 | + pushPackagePreviewWidgets(cachedWidgets, details, progressBarWidget(object_path)); |
138 | startLauncherAnimation(details); |
139 | } |
140 | }, |
141 | @@ -655,11 +753,15 @@ |
142 | click::Reviews::Error error) { |
143 | if (!login_error) { |
144 | if (error == click::Reviews::Error::NoError) { |
145 | - reply->push(reviewsWidgets(reviewlist)); |
146 | + auto const revs = reviewsWidgets(reviewlist); |
147 | + cachedWidgets.push(revs); |
148 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.singleColumn.column1, revs); |
149 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.twoColumns.column1, revs); |
150 | } else { |
151 | qDebug() << "There was an error getting reviews for:" << result["name"].get_string().c_str(); |
152 | } |
153 | } |
154 | + cachedWidgets.flush(reply); |
155 | reply->finished(); |
156 | }); |
157 | break; |
158 | @@ -732,7 +834,7 @@ |
159 | review.rating = 0; |
160 | std::string widget_id; |
161 | // We use a try/catch here, as scope_data() can be a dict, but not have |
162 | - // the values we need, which will result in an exception thrown. |
163 | + // the values we need, which will result in an exception thrown. |
164 | try { |
165 | auto metadict = metadata.scope_data().get_dict(); |
166 | review.rating = metadict["rating"].get_int(); |
167 | @@ -801,7 +903,7 @@ |
168 | getApplicationUri(manifest, [this, reply, manifest, app_name, &review, userid](const std::string& uri) { |
169 | populateDetails([this, reply, uri, manifest, app_name](const PackageDetails &details){ |
170 | store_department(details); |
171 | - pushPackagePreviewWidgets(reply, details, createButtons(uri, manifest)); |
172 | + pushPackagePreviewWidgets(cachedWidgets, details, createButtons(uri, manifest)); |
173 | }, |
174 | [this, reply, &review, manifest, userid](const ReviewList& reviewlist, |
175 | click::Reviews::Error error) { |
176 | @@ -828,13 +930,19 @@ |
177 | review_input.push_back(rating); |
178 | } |
179 | } |
180 | - reply->push(review_input); |
181 | + cachedWidgets.push(review_input); |
182 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.singleColumn.column1, review_input); |
183 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.twoColumns.column1, review_input); |
184 | |
185 | if (error == click::Reviews::Error::NoError) { |
186 | - reply->push(reviewsWidgets(reviews)); |
187 | + auto const revs = reviewsWidgets(reviews); |
188 | + cachedWidgets.push(revs); |
189 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.singleColumn.column1, revs); |
190 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.twoColumns.column1, revs); |
191 | } else { |
192 | qDebug() << "There was an error getting reviews for:" << result["name"].get_string().c_str(); |
193 | } |
194 | + cachedWidgets.flush(reply); |
195 | reply->finished(); |
196 | }); |
197 | }); |
198 | @@ -972,7 +1080,6 @@ |
199 | }); |
200 | } |
201 | |
202 | - |
203 | scopes::PreviewWidgetList PurchasingPreview::purchasingWidgets(const PackageDetails &/*details*/) |
204 | { |
205 | scopes::PreviewWidgetList widgets; |
206 | @@ -1130,14 +1237,18 @@ |
207 | button_widgets = progressBarWidget(found_object_path); |
208 | } |
209 | qDebug() << "Pushed button action widgets."; |
210 | - pushPackagePreviewWidgets(reply, found_details, button_widgets); |
211 | + pushPackagePreviewWidgets(cachedWidgets, found_details, button_widgets); |
212 | qDebug() << "Pushed package details widgets."; |
213 | if (reviewserror == click::Reviews::Error::NoError) { |
214 | qDebug() << "Pushing reviews widgets."; |
215 | - reply->push(reviewsWidgets(reviewlist)); |
216 | + auto const revs = reviewsWidgets(reviewlist); |
217 | + cachedWidgets.push(revs); |
218 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.singleColumn.column1, revs); |
219 | + cachedWidgets.layout.appendToColumn(cachedWidgets.layout.twoColumns.column1, revs); |
220 | } else { |
221 | qDebug() << "There was an error getting reviews for:" << result["name"].get_string().c_str(); |
222 | } |
223 | + cachedWidgets.flush(reply); |
224 | reply->finished(); |
225 | qDebug() << "---------- Finished reply for:" << result["name"].get_string().c_str(); |
226 | }); |
227 | |
228 | === modified file 'libclickscope/click/preview.h' |
229 | --- libclickscope/click/preview.h 2015-11-24 18:23:23 +0000 |
230 | +++ libclickscope/click/preview.h 2016-01-28 14:37:47 +0000 |
231 | @@ -45,6 +45,7 @@ |
232 | #include <unity/scopes/Result.h> |
233 | #include <unity/scopes/ScopeBase.h> |
234 | #include <unity/util/DefinesPtrs.h> |
235 | +#include <set> |
236 | |
237 | namespace scopes = unity::scopes; |
238 | |
239 | @@ -54,6 +55,35 @@ |
240 | class PreviewStrategy; |
241 | class DepartmentsDb; |
242 | |
243 | +struct WidgetsInColumns |
244 | +{ |
245 | + struct { |
246 | + std::vector<std::string> column1; |
247 | + } singleColumn; |
248 | + struct { |
249 | + std::vector<std::string> column1; |
250 | + std::vector<std::string> column2; |
251 | + } twoColumns; |
252 | + |
253 | + void registerLayouts(unity::scopes::PreviewReplyProxy const& reply); |
254 | + void appendToColumn(std::vector<std::string>& column, unity::scopes::PreviewWidgetList const& widgets); |
255 | +}; |
256 | + |
257 | +class CachedPreviewWidgets |
258 | +{ |
259 | +public: |
260 | + void push(unity::scopes::PreviewWidget const &widget); |
261 | + void push(unity::scopes::PreviewWidgetList const &widgetList); |
262 | + void flush(unity::scopes::PreviewReplyProxy const& reply); |
263 | + bool has(std::string const& widget) const; |
264 | + |
265 | + WidgetsInColumns layout; |
266 | + |
267 | +private: |
268 | + std::list<unity::scopes::PreviewWidget> widgets; |
269 | + std::unordered_set<std::string> widgets_lookup; |
270 | +}; |
271 | + |
272 | class DepartmentUpdater |
273 | { |
274 | protected: |
275 | @@ -137,6 +167,10 @@ |
276 | |
277 | virtual void cancelled(); |
278 | virtual void run(unity::scopes::PreviewReplyProxy const& reply) = 0; |
279 | + virtual void pushPackagePreviewWidgets(CachedPreviewWidgets &reply, |
280 | + const PackageDetails& details, |
281 | + const scopes::PreviewWidgetList& button_area_widgets); |
282 | + |
283 | protected: |
284 | virtual void populateDetails(std::function<void(const PackageDetails &)> details_callback, |
285 | std::function<void(const click::ReviewList&, |
286 | @@ -164,6 +198,7 @@ |
287 | virtual void invalidateScope(const std::string& scope_id); |
288 | |
289 | scopes::Result result; |
290 | + |
291 | QSharedPointer<click::web::Client> client; |
292 | QSharedPointer<click::Index> index; |
293 | click::web::Cancellable index_operation; |
294 | @@ -205,6 +240,7 @@ |
295 | std::string download_sha512; |
296 | QSharedPointer<click::Downloader> downloader; |
297 | std::shared_ptr<click::DepartmentsDb> depts_db; |
298 | + CachedPreviewWidgets cachedWidgets; |
299 | void startLauncherAnimation(const PackageDetails& details); |
300 | }; |
301 | |
302 | @@ -228,6 +264,7 @@ |
303 | const click::Manifest& manifest); |
304 | private: |
305 | scopes::ActionMetadata metadata; |
306 | + CachedPreviewWidgets cachedWidgets; |
307 | }; |
308 | |
309 | class InstalledScopePreview : public PreviewStrategy |
310 | @@ -272,7 +309,6 @@ |
311 | virtual ~UninstallConfirmationPreview(); |
312 | |
313 | void run(unity::scopes::PreviewReplyProxy const& reply) override; |
314 | - |
315 | }; |
316 | |
317 | class UninstalledPreview : public PreviewStrategy, public DepartmentUpdater |
318 | @@ -290,6 +326,7 @@ |
319 | void run(unity::scopes::PreviewReplyProxy const& reply) override; |
320 | protected: |
321 | PackageDetails found_details; |
322 | + CachedPreviewWidgets cachedWidgets; |
323 | std::string found_object_path; |
324 | virtual click::Downloader* get_downloader(const QSharedPointer<click::network::AccessManager>& nam); |
325 | virtual scopes::PreviewWidgetList uninstalledActionButtonWidgets(const PackageDetails &details); |
326 | |
327 | === modified file 'libclickscope/tests/test_preview.cpp' |
328 | --- libclickscope/tests/test_preview.cpp 2015-11-24 18:23:23 +0000 |
329 | +++ libclickscope/tests/test_preview.cpp 2016-01-28 14:37:47 +0000 |
330 | @@ -42,6 +42,7 @@ |
331 | #include <boost/locale/time_zone.hpp> |
332 | |
333 | using namespace ::testing; |
334 | +using ::testing::Matcher; |
335 | using namespace unity::scopes; |
336 | |
337 | class FakeResult : public Result |
338 | @@ -158,6 +159,81 @@ |
339 | |
340 | } |
341 | |
342 | +MATCHER_P(PreviewWidgetsListMatchers, widgets, "") { |
343 | + if (arg.size() == widgets.size()) { |
344 | + auto it1 = widgets.begin(); |
345 | + auto it2 = arg.begin(); |
346 | + while (*it1 != it2->id()) { |
347 | + *result_listener << "Preview widgets don't match: " << it2->id() << ", expected " << *it1; |
348 | + return false; |
349 | + } |
350 | + return true; |
351 | + } |
352 | + *result_listener << "Preview widgets list don't match: "; |
353 | + for (auto const& widget: arg) { |
354 | + *result_listener << widget.id() << ", "; |
355 | + } |
356 | + *result_listener << "expected: "; |
357 | + for (auto const& widget: widgets) { |
358 | + *result_listener << widget << ", "; |
359 | + } |
360 | + return false; |
361 | +} |
362 | + |
363 | +MATCHER_P(LayoutMatches, layouts, "") { |
364 | + if (arg.size() != layouts.size()) { |
365 | + *result_listener << "Layout lists sizes don't match, " << arg.size() << " vs " << layouts.size(); |
366 | + return false; |
367 | + } |
368 | + auto it1 = layouts.begin(); |
369 | + auto it2 = arg.begin(); |
370 | + while (it1 != layouts.end()) { |
371 | + if (it1->serialize() != it2->serialize()) { |
372 | + *result_listener << "Layouts don't match: " << unity::scopes::Variant(it1->serialize()).serialize_json() << " vs " |
373 | + << unity::scopes::Variant(it2->serialize()).serialize_json(); |
374 | + return false; |
375 | + } |
376 | + it1++; |
377 | + it2++; |
378 | + } |
379 | + return true; |
380 | +} |
381 | + |
382 | +TEST_F(PreviewStrategyTest, testPushCachedWidgets) |
383 | +{ |
384 | + unity::scopes::testing::MockPreviewReply reply; |
385 | + std::shared_ptr<unity::scopes::testing::MockPreviewReply> replyptr{&reply, [](unity::scopes::testing::MockPreviewReply*){}}; |
386 | + |
387 | + FakeResult result{vm}; |
388 | + FakePreview preview{result}; |
389 | + click::PackageDetails details; |
390 | + details.main_screenshot_url = "sshot1"; |
391 | + details.license = "GPL"; |
392 | + details.company_name = "Ubuntu"; |
393 | + details.website = "http://ubuntu.com"; |
394 | + details.changelog = "Foo"; |
395 | + details.version = "0.1"; |
396 | + details.download_url = "http://ubuntu.com/"; |
397 | + details.description = "Foo"; |
398 | + |
399 | + click::CachedPreviewWidgets cache; |
400 | + scopes::PreviewWidget buttons("buttons", "actions"); |
401 | + |
402 | + unity::scopes::ColumnLayout single_column(1); |
403 | + single_column.add_column({"hdr","buttons","screenshots","summary","other_metadata","updates_table","whats_new"}); |
404 | + unity::scopes::ColumnLayout two_columns(2); |
405 | + two_columns.add_column({"hdr","buttons","screenshots","summary"}); |
406 | + two_columns.add_column({"other_metadata","updates_table","whats_new"}); |
407 | + unity::scopes::ColumnLayoutList expected_layout {single_column, two_columns}; |
408 | + |
409 | + std::vector<std::string> expected_widgets {"hdr", "buttons", "screenshots", "summary", "other_metadata", "updates_table", "whats_new"}; |
410 | + |
411 | + EXPECT_CALL(*replyptr, register_layout(Matcher<unity::scopes::ColumnLayoutList const&>(LayoutMatches(expected_layout)))); |
412 | + EXPECT_CALL(*replyptr, push(Matcher<unity::scopes::PreviewWidgetList const&>(PreviewWidgetsListMatchers(expected_widgets)))); |
413 | + preview.pushPackagePreviewWidgets(cache, details, {buttons}); |
414 | + cache.flush(replyptr); |
415 | +} |
416 | + |
417 | class PreviewStrategyDescriptionTest : public PreviewStrategyTest |
418 | { |
419 | public: |
420 | @@ -391,6 +467,7 @@ |
421 | EXPECT_CALL(preview, progressBarWidget(_)) |
422 | .Times(1) |
423 | .WillOnce(Return(response)); |
424 | + EXPECT_CALL(*replyptr, register_layout(_)); |
425 | preview.run(replyptr); |
426 | preview.fake_downloader->activate_callback(); |
427 | } |
428 | @@ -404,6 +481,7 @@ |
429 | EXPECT_CALL(preview, uninstalledActionButtonWidgets(_)) |
430 | .Times(1) |
431 | .WillOnce(Return(response)); |
432 | + EXPECT_CALL(*replyptr, register_layout(_)); |
433 | preview.run(replyptr); |
434 | preview.fake_downloader->activate_callback(); |
435 | } |
436 | @@ -483,6 +561,7 @@ |
437 | : click::InstalledPreview(result, metadata, client, pay_package, depts) { |
438 | |
439 | } |
440 | + |
441 | using click::InstalledPreview::createButtons; |
442 | MOCK_METHOD0(isRefundable, bool()); |
443 | }; |
444 | @@ -520,7 +599,6 @@ |
445 | ASSERT_EQ(get_action_from_widgets(widgets, 0, 1), "uninstall_click"); |
446 | } |
447 | |
448 | - |
449 | class FakeCancelPurchasePreview : public click::CancelPurchasePreview { |
450 | public: |
451 | FakeCancelPurchasePreview(const unity::scopes::Result& result, bool installed) |
FAILED: Continuous integration, rev:409 jenkins. qa.ubuntu. com/job/ unity-scope- click-ci/ 694/ jenkins. qa.ubuntu. com/job/ unity-scope- click-wily- amd64-ci/ 98/console jenkins. qa.ubuntu. com/job/ unity-scope- click-wily- armhf-ci/ 94/console
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/unity- scope-click- ci/694/ rebuild
http://