Merge lp:~stolowski/unity-scopes-shell/diff-updates into lp:unity-scopes-shell

Proposed by Paweł Stołowski
Status: Merged
Approved by: Marcus Tomlinson
Approved revision: 283
Merged at revision: 276
Proposed branch: lp:~stolowski/unity-scopes-shell/diff-updates
Merge into: lp:unity-scopes-shell
Prerequisite: lp:~stolowski/unity-scopes-shell/in-card-activation
Diff against target: 1077 lines (+555/-134)
16 files modified
debian/control.in (+1/-1)
po/POTFILES.in (+0/-10)
src/Unity/CMakeLists.txt (+1/-0)
src/Unity/categories.cpp (+35/-50)
src/Unity/categories.h (+5/-2)
src/Unity/resultsmap.cpp (+65/-0)
src/Unity/resultsmap.h (+48/-0)
src/Unity/resultsmodel.cpp (+75/-2)
src/Unity/resultsmodel.h (+4/-0)
src/Unity/scope.cpp (+40/-30)
src/Unity/scope.h (+3/-3)
tests/data/CMakeLists.txt (+1/-0)
tests/data/mock-scope-manyresults/CMakeLists.txt (+6/-0)
tests/data/mock-scope-manyresults/mock-scope-manyresults.cpp (+185/-0)
tests/data/mock-scope-manyresults/mock-scope-manyresults.ini.in (+14/-0)
tests/resultstest.cpp (+72/-36)
To merge this branch: bzr merge lp:~stolowski/unity-scopes-shell/diff-updates
Reviewer Review Type Date Requested Status
Marcus Tomlinson (community) Approve
PS Jenkins bot (community) continuous-integration Needs Fixing
Review via email: mp+273554@code.launchpad.net

Commit message

Apply updates to results model instead of clearing. Removed obsolete code which deals with special categories.

Description of the change

Apply updates to results model instead of clearing.
These changes are available for testing in silo 20.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
278. By Paweł Stołowski

Minor test improvement

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
279. By Paweł Stołowski

Oops, forgot to commit

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Marcus Tomlinson (marcustomlinson) wrote :

Hmmm, some scopes seems to crash now. I'm testing on krillin rc-proposed #147, between the standard shell and unity-api packages and those from silo 20.

With your silo, BBC, BBC Sport, and cnet scopes (probably more) crash as soon as you change the department.

review: Needs Fixing
Revision history for this message
Paweł Stołowski (stolowski) wrote :

> Hmmm, some scopes seems to crash now. I'm testing on krillin rc-proposed #147,
> between the standard shell and unity-api packages and those from silo 20.
>
> With your silo, BBC, BBC Sport, and cnet scopes (probably more) crash as soon
> as you change the department.

Yeah, I can reproduce. Looks like it's related to vertical journal, the last message in the dash log before it crashes comes from unity8 and says:
"VerticalJournal only supports removal from the end of the model"

Need to check with Albert about what can be done about it. Unfortunately, according to him, veritcal journal is "unfixable" in that respect, so I may need to resort to full model clearing for this kind of renderer...

Revision history for this message
Paweł Stołowski (stolowski) wrote :

Okay, silo 20 now has the fix for unity8 crash caused by unexpected model updates from plugin.

280. By Paweł Stołowski

Merged trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Marcus Tomlinson (marcustomlinson) wrote :

There seems to be a few issues with aggregator scopes now. It looks like while new results are being loaded the first category's results get duplicated. Then when the refresh completes, the duplicates disappear.

The most noticeable scope I see this with is the Photos scope. All results in the My Photos category at the top get duplicated while the refresh is busy, then suddenly disappear when its done.

For aggregators that load quickly I think we get away with not revealing this issue because the duplicates get removed before the new set is displayed. Perhaps we also get away with duplicating results in a top category that is full as we don't see the extras being appended to the end.

Other scope's I've seen this on (although harder to catch) are:

* Food (duplicate "Add your fitbit account" item)
* Nearby (deplete "Where am I" result)

review: Needs Fixing
Revision history for this message
Marcus Tomlinson (marcustomlinson) wrote :

> There seems to be a few issues with aggregator scopes now. It looks like while
> new results are being loaded the first category's results get duplicated. Then
> when the refresh completes, the duplicates disappear.
>
> The most noticeable scope I see this with is the Photos scope. All results in
> the My Photos category at the top get duplicated while the refresh is busy,
> then suddenly disappear when its done.
>
> For aggregators that load quickly I think we get away with not revealing this
> issue because the duplicates get removed before the new set is displayed.
> Perhaps we also get away with duplicating results in a top category that is
> full as we don't see the extras being appended to the end.
>
> Other scope's I've seen this on (although harder to catch) are:
>
> * Food (duplicate "Add your fitbit account" item)
> * Nearby (deplete "Where am I" result)

That last "deplete" should have been "duplicate" as well (auto-correct fail)

281. By Paweł Stołowski

Use only one timer

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Paweł Stołowski (stolowski) wrote :

> There seems to be a few issues with aggregator scopes now. It looks like while
> new results are being loaded the first category's results get duplicated. Then
> when the refresh completes, the duplicates disappear.
>
> The most noticeable scope I see this with is the Photos scope. All results in
> the My Photos category at the top get duplicated while the refresh is busy,
> then suddenly disappear when its done.
>
> For aggregators that load quickly I think we get away with not revealing this
> issue because the duplicates get removed before the new set is displayed.
> Perhaps we also get away with duplicating results in a top category that is
> full as we don't see the extras being appended to the end.
>
> Other scope's I've seen this on (although harder to catch) are:
>
> * Food (duplicate "Add your fitbit account" item)
> * Nearby (deplete "Where am I" result)

I debugged this and it turned out it's due to the way these scopes work... They create *unique* category ids on every search (an id, plus a timestamp each time), e.g. "nominatim-how:declared:first_result:incoming_template::surface19.10.2015_14:55:26:265".

Now, this creates a serious issue with the existing implementation of the plugin, where we never remove categories, only move them down (and hide) in the model when they are empty - effectively a leak of resources. I think we could stop shell plugin from caching category objects indefinitely, but I would definately leave it for a separate MP if we deem it a good idea.

I talked to Kyle about this problem and he is going to fix the aggregator scopes (or ask respective people from their team to do that), which should fix the issue you saw. Apparently this MP needs to wait till aggregators are updated, otherwise the experience will be suboptimal.

Revision history for this message
Paweł Stołowski (stolowski) wrote :

> > There seems to be a few issues with aggregator scopes now. It looks like
> while
> > new results are being loaded the first category's results get duplicated.
> Then
> > when the refresh completes, the duplicates disappear.
> >
> > The most noticeable scope I see this with is the Photos scope. All results
> in
> > the My Photos category at the top get duplicated while the refresh is busy,
> > then suddenly disappear when its done.
> >
> > For aggregators that load quickly I think we get away with not revealing
> this
> > issue because the duplicates get removed before the new set is displayed.
> > Perhaps we also get away with duplicating results in a top category that is
> > full as we don't see the extras being appended to the end.
> >
> > Other scope's I've seen this on (although harder to catch) are:
> >
> > * Food (duplicate "Add your fitbit account" item)
> > * Nearby (deplete "Where am I" result)
>
> I debugged this and it turned out it's due to the way these scopes work...
> They create *unique* category ids on every search (an id, plus a timestamp
> each time), e.g. "nominatim-
> how:declared:first_result:incoming_template::surface19.10.2015_14:55:26:265".
>
> Now, this creates a serious issue with the existing implementation of the
> plugin, where we never remove categories, only move them down (and hide) in
> the model when they are empty - effectively a leak of resources. I think we
> could stop shell plugin from caching category objects indefinitely, but I
> would definately leave it for a separate MP if we deem it a good idea.
>
> I talked to Kyle about this problem and he is going to fix the aggregator
> scopes (or ask respective people from their team to do that), which should fix
> the issue you saw. Apparently this MP needs to wait till aggregators are
> updated, otherwise the experience will be suboptimal.

Related bug for Kyle's aggregators: https://bugs.launchpad.net/scope-aggregator/+bug/1507666

Revision history for this message
Michi Henning (michihenning) wrote :

In effect, this constitutes a memory leak in the shell, so it needs fixing regardless. There has to be some upper limit on the number of categories that cached for each scope. Some largish number (maybe 200?) per scope should do.

We also should add something to the scopes api doc to day "don't do this".

282. By Paweł Stołowski

Increse typing timeout to 700ms

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
283. By Paweł Stołowski

Merged trunk

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Marcus Tomlinson (marcustomlinson) wrote :

I'm happy with these changes, looks really nice. Good job Pawel!

review: Approve
284. By Paweł Stołowski

Merged trunk

285. By Paweł Stołowski

Bump unity-api dep

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/control.in'
2--- debian/control.in 2015-11-02 10:11:24 +0000
3+++ debian/control.in 2015-11-02 10:11:24 +0000
4@@ -8,7 +8,7 @@
5 dh-python,
6 libboost-python-dev,
7 libboost-regex-dev,
8- libunity-api-dev (>= 7.101),
9+ libunity-api-dev (>= 7.102),
10 libunity-scopes-dev (>= 1.0.1~),
11 libgsettings-qt-dev (>= 0.1),
12 libqtdbustest1-dev (>= 0.2),
13
14=== modified file 'po/POTFILES.in'
15--- po/POTFILES.in 2015-11-02 10:11:24 +0000
16+++ po/POTFILES.in 2015-09-22 11:04:14 +0000
17@@ -59,18 +59,11 @@
18 src/python/scope_harness/results-view-py.cpp
19 src/python/scope_harness/preview-widget-py.cpp
20 src/python/scope_harness/preview-widget-list-py.cpp
21-<<<<<<< TREE
22 src/Unity/previewwidgetmodel.cpp
23 src/Unity/departmentnode.cpp
24 src/Unity/logintoaccount.cpp
25 src/Unity/overviewcategories.cpp
26 src/Unity/scope.cpp
27-=======
28-src/Unity/previewwidgetmodel.cpp
29-src/Unity/departmentnode.cpp
30-src/Unity/overviewcategories.cpp
31-src/Unity/scope.cpp
32->>>>>>> MERGE-SOURCE
33 src/Unity/overviewscope.cpp
34 src/Unity/collectors.cpp
35 src/Unity/previewmodel.cpp
36@@ -133,10 +126,7 @@
37 src/Unity/department.h
38 src/Unity/scope.h
39 src/Unity/previewstack.h
40-<<<<<<< TREE
41 src/Unity/logintoaccount.h
42-=======
43->>>>>>> MERGE-SOURCE
44 src/Unity/overviewcategories.h
45 src/Unity/locationservice.h
46 src/Unity/overviewscope.h
47
48=== modified file 'src/Unity/CMakeLists.txt'
49--- src/Unity/CMakeLists.txt 2015-09-08 15:08:55 +0000
50+++ src/Unity/CMakeLists.txt 2015-11-02 10:11:24 +0000
51@@ -29,6 +29,7 @@
52 previewmodel.cpp
53 previewstack.cpp
54 previewwidgetmodel.cpp
55+ resultsmap.cpp
56 resultsmodel.cpp
57 scope.cpp
58 scopes.cpp
59
60=== modified file 'src/Unity/categories.cpp'
61--- src/Unity/categories.cpp 2015-11-02 10:11:24 +0000
62+++ src/Unity/categories.cpp 2015-11-02 10:11:24 +0000
63@@ -46,20 +46,13 @@
64 class CategoryData
65 {
66 public:
67- CategoryData(scopes::Category::SCPtr const& category): m_isSpecial(false)
68+ CategoryData(scopes::Category::SCPtr const& category)
69 {
70 setCategory(category);
71 }
72
73 CategoryData(CategoryData const& other) = delete;
74
75- // constructor for special (shell-overriden) categories
76- CategoryData(QString const& id, QString const& title, QString const& icon, QString const& rawTemplate, QObject* countObject):
77- m_catId(id), m_catTitle(title), m_catIcon(icon), m_rawTemplate(rawTemplate.toStdString()), m_countObject(countObject), m_isSpecial(true)
78- {
79- parseTemplate(m_rawTemplate, &m_rendererTemplate, &m_components);
80- }
81-
82 void setCategory(scopes::Category::SCPtr const& category)
83 {
84 m_category = category;
85@@ -211,11 +204,6 @@
86 return 0;
87 }
88
89- bool isSpecial() const
90- {
91- return m_isSpecial;
92- }
93-
94 static bool parseTemplate(std::string const& raw_template, QJsonValue* renderer, QJsonValue* components)
95 {
96 // lazy init of the defaults
97@@ -260,7 +248,6 @@
98 QJsonValue m_components;
99 QSharedPointer<ResultsModel> m_resultsModel;
100 QPointer<QObject> m_countObject;
101- bool m_isSpecial;
102
103 static QJsonValue mergeOverrides(QJsonValue const& defaultVal, QJsonValue const& overrideVal)
104 {
105@@ -299,7 +286,8 @@
106 QJsonValue* CategoryData::DEFAULTS = nullptr;
107
108 Categories::Categories(QObject* parent)
109- : unity::shell::scopes::CategoriesInterface(parent)
110+ : unity::shell::scopes::CategoriesInterface(parent),
111+ m_categoryIndex(0)
112 {
113 }
114
115@@ -333,25 +321,16 @@
116 return -1;
117 }
118
119-int Categories::getFirstEmptyCategoryIndex() const
120-{
121- for (int i = 0; i < m_categories.size(); i++) {
122- if (m_categories[i]->isSpecial()) {
123- continue;
124- }
125- if (m_categories[i]->resultsModelCount() == 0) {
126- return i;
127- }
128- }
129-
130- return m_categories.size();
131-}
132-
133 void Categories::registerCategory(const scopes::Category::SCPtr& category, QSharedPointer<ResultsModel> resultsModel)
134 {
135 // do we already have a category with this id?
136+ if (m_registeredCategories.find(category->id()) != m_registeredCategories.end()) {
137+ return;
138+ }
139+ m_registeredCategories.insert(category->id());
140+
141 int index = getCategoryIndex(QString::fromStdString(category->id()));
142- int emptyIndex = getFirstEmptyCategoryIndex();
143+ int emptyIndex = m_categoryIndex++;
144 if (index >= 0) {
145 // re-registering an existing category will move it after the first non-empty category
146 if (emptyIndex < index) {
147@@ -374,8 +353,9 @@
148 m_categories.insert(emptyIndex, catData);
149 endInsertRows();
150 } else {
151+ // the category has already been registered for current search,
152+ // check if any attributes of the category changed
153 QSharedPointer<CategoryData> catData = m_categories[index];
154- // check if any attributes of the category changed
155 QVector<int> changedRoles(catData->updateAttributes(category));
156
157 if (changedRoles.size() > 0) {
158@@ -463,6 +443,30 @@
159 dataChanged(changeStart, changeEnd, roles);
160 }
161
162+void Categories::markNewSearch()
163+{
164+ m_categoryIndex = 0;
165+ m_registeredCategories.clear();
166+ for (auto model: m_categoryResults) {
167+ model->markNewSearch();
168+ }
169+}
170+
171+void Categories::purgeResults()
172+{
173+ QVector<int> roles;
174+ roles.append(RoleCount);
175+
176+ for (auto it = m_categoryResults.begin(); it != m_categoryResults.end(); it++) {
177+ auto model = it.value();
178+ if (model->needsPurging()) {
179+ model->clearResults();
180+
181+ QModelIndex idx(index(getCategoryIndex(QString::fromStdString(it.key()))));
182+ Q_EMIT dataChanged(idx, idx, roles);
183+ }
184+ }
185+}
186
187 bool Categories::parseTemplate(std::string const& raw_template, QJsonValue* renderer, QJsonValue* components)
188 {
189@@ -494,25 +498,6 @@
190 return false;
191 }
192
193-void Categories::addSpecialCategory(QString const& categoryId, QString const& name, QString const& icon, QString const& rawTemplate, QObject* countObject)
194-{
195- int index = getCategoryIndex(categoryId);
196- if (index >= 0) {
197- qWarning("ERROR! Category with id \"%s\" already exists!", categoryId.toStdString().c_str());
198- } else {
199- QSharedPointer<CategoryData> catData(new CategoryData(categoryId, name, icon, rawTemplate, countObject));
200- // prepend the category
201- beginInsertRows(QModelIndex(), 0, 0);
202- m_categories.prepend(catData);
203- endInsertRows();
204-
205- if (countObject) {
206- m_countObjects[countObject] = categoryId;
207- QObject::connect(countObject, SIGNAL(countChanged()), this, SLOT(countChanged()));
208- }
209- }
210-}
211-
212 void Categories::countChanged()
213 {
214 QObject* countObject = sender();
215
216=== modified file 'src/Unity/categories.h'
217--- src/Unity/categories.h 2015-11-02 10:11:24 +0000
218+++ src/Unity/categories.h 2015-11-02 10:11:24 +0000
219@@ -26,6 +26,7 @@
220
221 #include <QSharedPointer>
222 #include <QJsonValue>
223+#include <set>
224
225 #include <unity/scopes/Category.h>
226
227@@ -55,12 +56,13 @@
228 int rowCount(const QModelIndex& parent = QModelIndex()) const override;
229
230 Q_INVOKABLE bool overrideCategoryJson(QString const& categoryId, QString const& json) override;
231- Q_INVOKABLE void addSpecialCategory(QString const& categoryId, QString const& name, QString const& icon, QString const& rawTemplate, QObject* countObject) override;
232
233 QSharedPointer<ResultsModel> lookupCategory(std::string const& category_id);
234 void registerCategory(const unity::scopes::Category::SCPtr& category, QSharedPointer<ResultsModel> model);
235 void updateResultCount(const QSharedPointer<ResultsModel>& resultsModel);
236 void clearAll();
237+ void markNewSearch();
238+ void purgeResults();
239 void updateResult(unity::scopes::Result const& result, QString const& categoryId, unity::scopes::Result const& updated_result);
240
241 static bool parseTemplate(std::string const& raw_template, QJsonValue* renderer, QJsonValue* components);
242@@ -70,11 +72,12 @@
243
244 private:
245 int getCategoryIndex(QString const& categoryId) const;
246- int getFirstEmptyCategoryIndex() const;
247
248 QList<QSharedPointer<CategoryData>> m_categories;
249 QMap<std::string, QSharedPointer<ResultsModel>> m_categoryResults;
250 QMap<QObject*, QString> m_countObjects;
251+ std::set<std::string> m_registeredCategories;
252+ int m_categoryIndex;
253 };
254
255 } // namespace scopes_ng
256
257=== added file 'src/Unity/resultsmap.cpp'
258--- src/Unity/resultsmap.cpp 1970-01-01 00:00:00 +0000
259+++ src/Unity/resultsmap.cpp 2015-11-02 10:11:24 +0000
260@@ -0,0 +1,65 @@
261+/*
262+ * Copyright (C) 2015 Canonical, Ltd.
263+ *
264+ * Authors:
265+ * Pawel Stolowski <pawel.stolowski@canonical.com>
266+ *
267+ * This program is free software; you can redistribute it and/or modify
268+ * it under the terms of the GNU General Public License as published by
269+ * the Free Software Foundation; version 3.
270+ *
271+ * This program is distributed in the hope that it will be useful,
272+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
273+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
274+ * GNU General Public License for more details.
275+ *
276+ * You should have received a copy of the GNU General Public License
277+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
278+ */
279+
280+#include "resultsmap.h"
281+#include <cassert>
282+
283+ResultsMap::ResultsMap(QList<std::shared_ptr<unity::scopes::Result>> const &results)
284+{
285+ rebuild(results);
286+}
287+
288+ResultsMap::ResultsMap(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const &results)
289+{
290+ int pos = 0;
291+ for (auto result: results) {
292+ std::shared_ptr<unity::scopes::Result> res = result;
293+ assert(res);
294+ const ResultPos rpos { res, pos++ };
295+ m_results.insert({result->uri(), rpos });
296+ }
297+}
298+
299+void ResultsMap::rebuild(QList<std::shared_ptr<unity::scopes::Result>> const &results)
300+{
301+ m_results.clear();
302+ int pos = 0;
303+ for (auto result: results) {
304+ assert(result);
305+ const ResultPos rpos { result, pos++ };
306+ m_results.insert({result->uri(), rpos });
307+ }
308+}
309+
310+int ResultsMap::find(std::shared_ptr<unity::scopes::Result> const& result) const
311+{
312+ assert(result);
313+ auto it = m_results.find(result->uri());
314+ if (it != m_results.end()) {
315+ assert(it->second.result);
316+ while (it != m_results.end() && it->second.result->uri() == result->uri())
317+ {
318+ if (*(it->second.result) == *result) {
319+ return it->second.index;
320+ }
321+ ++it;
322+ }
323+ }
324+ return -1;
325+}
326
327=== added file 'src/Unity/resultsmap.h'
328--- src/Unity/resultsmap.h 1970-01-01 00:00:00 +0000
329+++ src/Unity/resultsmap.h 2015-11-02 10:11:24 +0000
330@@ -0,0 +1,48 @@
331+/*
332+ * Copyright (C) 2015 Canonical, Ltd.
333+ *
334+ * Authors:
335+ * Pawel Stolowski <pawel.stolowski@canonical.com>
336+ *
337+ * This program is free software; you can redistribute it and/or modify
338+ * it under the terms of the GNU General Public License as published by
339+ * the Free Software Foundation; version 3.
340+ *
341+ * This program is distributed in the hope that it will be useful,
342+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
343+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
344+ * GNU General Public License for more details.
345+ *
346+ * You should have received a copy of the GNU General Public License
347+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
348+ */
349+
350+#ifndef NG_RESULTS_MAP_H
351+#define NGRESULTS_MAP_H
352+
353+#include <QList>
354+#include <memory>
355+#include <unity/scopes/CategorisedResult.h>
356+#include <map>
357+
358+/**
359+ Helper class for Result -> row lookups, maintains a multimap internally
360+ allowing for duplicated Result uris.
361+*/
362+class ResultsMap
363+{
364+ public:
365+ ResultsMap(QList<std::shared_ptr<unity::scopes::Result>> const &results);
366+ ResultsMap(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const &results);
367+ int find(std::shared_ptr<unity::scopes::Result> const& result) const;
368+
369+ void rebuild(QList<std::shared_ptr<unity::scopes::Result>> const &results);
370+ private:
371+ struct ResultPos {
372+ std::shared_ptr<unity::scopes::Result> result;
373+ int index;
374+ };
375+ std::multimap<std::string, ResultPos> m_results;
376+};
377+
378+#endif
379
380=== modified file 'src/Unity/resultsmodel.cpp'
381--- src/Unity/resultsmodel.cpp 2015-11-02 10:11:24 +0000
382+++ src/Unity/resultsmodel.cpp 2015-11-02 10:11:24 +0000
383@@ -23,7 +23,9 @@
384 // local
385 #include "utils.h"
386 #include "iconutils.h"
387+#include "resultsmap.h"
388
389+#include <map>
390 #include <QDebug>
391
392 namespace scopes_ng {
393@@ -33,6 +35,7 @@
394 ResultsModel::ResultsModel(QObject* parent)
395 : unity::shell::scopes::ResultsModelInterface(parent)
396 , m_maxAttributes(2)
397+ , m_purge(true)
398 {
399 }
400
401@@ -70,10 +73,70 @@
402 m_maxAttributes = count;
403 }
404
405+void ResultsModel::addUpdateResults(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const& results)
406+{
407+ if (results.count() == 0) {
408+ return;
409+ }
410+
411+ m_purge = false;
412+
413+ const int oldCount = m_results.count();
414+
415+ ResultsMap newResultsMap(results);
416+
417+ int row = 0;
418+ // iterate over old (i.e. currently visible) results, remove results which are no longer present in new set
419+ for (auto it = m_results.begin(); it != m_results.end(); ) {
420+ int newPos = newResultsMap.find(*it);
421+ bool haveNow = (newPos >= 0);
422+ if (!haveNow) {
423+ // delete row
424+ beginRemoveRows(QModelIndex(), row, row);
425+ it = m_results.erase(it);
426+ endRemoveRows();
427+ } else {
428+ ++it;
429+ ++row;
430+ }
431+ }
432+
433+ ResultsMap oldResultsMap(m_results);
434+
435+ // iterate over new results
436+ for (row = 0; row<results.count(); ++row) {
437+ const int oldPos = oldResultsMap.find(results[row]);
438+ const bool hadBefore = (oldPos >= 0);
439+ if (hadBefore) {
440+ if (row != oldPos) {
441+ // move row
442+ beginMoveRows(QModelIndex(), oldPos, oldPos, QModelIndex(), row + (row > oldPos ? 1 : 0));
443+ m_results.move(oldPos, row);
444+ oldResultsMap.rebuild(m_results);
445+ endMoveRows();
446+ }
447+ } else {
448+ // insert row
449+ beginInsertRows(QModelIndex(), row, row);
450+ m_results.insert(row, results[row]);
451+ oldResultsMap.rebuild(m_results);
452+ endInsertRows();
453+ }
454+ }
455+
456+ if (oldCount != m_results.count()) {
457+ Q_EMIT countChanged();
458+ }
459+}
460+
461 void ResultsModel::addResults(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const& results)
462 {
463- if (results.count() == 0) return;
464-
465+ if (results.count() == 0) {
466+ return;
467+ }
468+
469+ m_purge = false;
470+
471 beginInsertRows(QModelIndex(), m_results.count(), m_results.count() + results.count() - 1);
472 Q_FOREACH(std::shared_ptr<scopes::CategorisedResult> const& result, results) {
473 m_results.append(result);
474@@ -264,4 +327,14 @@
475 }
476 }
477
478+void ResultsModel::markNewSearch()
479+{
480+ m_purge = true;
481+}
482+
483+bool ResultsModel::needsPurging() const
484+{
485+ return m_purge;
486+}
487+
488 } // namespace scopes_ng
489
490=== modified file 'src/Unity/resultsmodel.h'
491--- src/Unity/resultsmodel.h 2015-11-02 10:11:24 +0000
492+++ src/Unity/resultsmodel.h 2015-11-02 10:11:24 +0000
493@@ -45,6 +45,7 @@
494 QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
495
496 void addResults(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const&);
497+ void addUpdateResults(QList<std::shared_ptr<unity::scopes::CategorisedResult>> const&);
498 void clearResults();
499
500 /* getters */
501@@ -58,6 +59,8 @@
502
503 QHash<int, QByteArray> roleNames() const override;
504 void updateResult(unity::scopes::Result const& result, unity::scopes::Result const& updatedResult);
505+ void markNewSearch();
506+ bool needsPurging() const;
507
508 private:
509 QVariant componentValue(unity::scopes::Result const* result, std::string const& fieldName) const;
510@@ -67,6 +70,7 @@
511 QList<std::shared_ptr<unity::scopes::Result>> m_results;
512 QString m_categoryId;
513 int m_maxAttributes;
514+ bool m_purge;
515 };
516
517 } // namespace scopes_ng
518
519=== modified file 'src/Unity/scope.cpp'
520--- src/Unity/scope.cpp 2015-11-02 10:11:24 +0000
521+++ src/Unity/scope.cpp 2015-11-02 10:11:24 +0000
522@@ -67,9 +67,8 @@
523
524 using namespace unity;
525
526-const int AGGREGATION_TIMEOUT = 110;
527-const int TYPING_TIMEOUT = 300;
528-const int CLEAR_TIMEOUT = 240;
529+const int TYPING_TIMEOUT = 700;
530+const int SEARCH_PROCESSING_DELAY = 1000;
531 const int RESULTS_TTL_SMALL = 30000; // 30 seconds
532 const int RESULTS_TTL_MEDIUM = 300000; // 5 minutes
533 const int RESULTS_TTL_LARGE = 3600000; // 1 hour
534@@ -85,7 +84,7 @@
535 , m_isActive(false)
536 , m_searchInProgress(false)
537 , m_resultsDirty(false)
538- , m_delayedClear(false)
539+ , m_delayedSearchProcessing(false)
540 , m_hasNavigation(false)
541 , m_hasAltNavigation(false)
542 , m_favorite(false)
543@@ -109,10 +108,8 @@
544 m_typingTimer.setInterval(TYPING_TIMEOUT);
545 }
546 QObject::connect(&m_typingTimer, &QTimer::timeout, this, &Scope::typingFinished);
547- m_aggregatorTimer.setSingleShot(true);
548- QObject::connect(&m_aggregatorTimer, SIGNAL(timeout()), this, SLOT(flushUpdates()));
549- m_clearTimer.setSingleShot(true);
550- QObject::connect(&m_clearTimer, SIGNAL(timeout()), this, SLOT(flushUpdates()));
551+ m_searchProcessingDelayTimer.setSingleShot(true);
552+ QObject::connect(&m_searchProcessingDelayTimer, SIGNAL(timeout()), this, SLOT(flushUpdates()));
553 m_invalidateTimer.setSingleShot(true);
554 m_invalidateTimer.setTimerType(Qt::CoarseTimer);
555 QObject::connect(&m_invalidateTimer, &QTimer::timeout, this, &Scope::invalidateResults);
556@@ -146,14 +143,14 @@
557 }
558
559 if (status == CollectorBase::Status::INCOMPLETE) {
560- if (!m_aggregatorTimer.isActive()) {
561+ if (!m_searchProcessingDelayTimer.isActive()) {
562 // the longer we've been waiting for the results, the shorter the timeout
563 qint64 inProgressMs = pushEvent->msecsSinceStart();
564 double mult = 1.0 / std::max(1, static_cast<int>((inProgressMs / 150) + 1));
565- m_aggregatorTimer.start(AGGREGATION_TIMEOUT * mult);
566+ m_searchProcessingDelayTimer.start(SEARCH_PROCESSING_DELAY * mult);
567 }
568 } else { // status in [FINISHED, ERROR]
569- m_aggregatorTimer.stop();
570+ m_searchProcessingDelayTimer.stop();
571
572 flushUpdates(true);
573
574@@ -229,6 +226,7 @@
575 case scopes::ActivationResponse::UpdateResult:
576 m_categories->updateResult(*result, categoryId, response->updated_result());
577 Q_EMIT updateResultRequested();
578+ break;
579 case scopes::ActivationResponse::UpdatePreview:
580 handlePreviewUpdate(result, response->updated_widgets());
581 break;
582@@ -345,21 +343,29 @@
583
584 void Scope::flushUpdates(bool finalize)
585 {
586- if (m_delayedClear) {
587- // TODO: here we could do resultset diffs
588- m_categories->clearAll();
589- m_delayedClear = false;
590- }
591-
592- if (m_clearTimer.isActive()) {
593- m_clearTimer.stop();
594+ if (m_delayedSearchProcessing) {
595+ m_delayedSearchProcessing = false;
596 }
597
598 if (m_status != Status::Okay) {
599 setStatus(Status::Okay);
600 }
601+
602+ // if no results have been received so far (and we're not in the finalizing step of search), then
603+ // don't process the results as this will inevitably make the dash empty.
604+ if (m_cachedResults.empty() && !finalize) {
605+ return;
606+ }
607+
608+ qDebug() << "flushUpdates:" << id() << "#results =" << m_cachedResults.count() << "finalize:" << finalize;
609+
610 processResultSet(m_cachedResults); // clears the result list
611
612+ if (finalize) {
613+ m_category_results.clear();
614+ m_categories->purgeResults(); // remove results for categories which were not present in new resultset
615+ }
616+
617 // process departments
618 if (m_rootDepartment && m_rootDepartment != m_lastRootDepartment) {
619 // build / append to the tree
620@@ -573,14 +579,15 @@
621 // this will keep the list of categories in order
622 QVector<scopes::Category::SCPtr> categories;
623
624- // split the result_set by category_id
625- QMap<std::string, QList<std::shared_ptr<scopes::CategorisedResult>>> category_results;
626+ // split the result_set by category_id; note that processResultSet may get called more than once
627+ // for single search request, all the contents of m_category_results accumulate until new search
628+ // is requested, so that addUpdateResults() can properly update affected models.
629 while (!result_set.empty()) {
630 auto result = result_set.takeFirst();
631- if (!category_results.contains(result->category()->id())) {
632+ if (!categories.contains(result->category())) {
633 categories.append(result->category());
634 }
635- category_results[result->category()->id()].append(std::move(result));
636+ m_category_results[result->category()->id()].append(std::move(result));
637 }
638
639 Q_FOREACH(scopes::Category::SCPtr const& category, categories) {
640@@ -588,12 +595,11 @@
641 if (category_model == nullptr) {
642 category_model.reset(new ResultsModel(m_categories.data()));
643 category_model->setCategoryId(QString::fromStdString(category->id()));
644- category_model->addResults(category_results[category->id()]);
645+ category_model->addResults(m_category_results[category->id()]);
646 m_categories->registerCategory(category, category_model);
647 } else {
648- // FIXME: only update when we know it's necessary
649 m_categories->registerCategory(category, QSharedPointer<ResultsModel>());
650- category_model->addResults(category_results[category->id()]);
651+ category_model->addUpdateResults(m_category_results[category->id()]);
652 m_categories->updateResultCount(category_model);
653 }
654 }
655@@ -612,10 +618,11 @@
656 void Scope::invalidateLastSearch()
657 {
658 m_searchController->invalidate();
659- if (m_aggregatorTimer.isActive()) {
660- m_aggregatorTimer.stop();
661+ if (m_searchProcessingDelayTimer.isActive()) {
662+ m_searchProcessingDelayTimer.stop();
663 }
664 m_cachedResults.clear();
665+ m_category_results.clear();
666 }
667
668 void Scope::startTtlTimer()
669@@ -695,8 +702,11 @@
670 m_initialQueryDone = true;
671
672 invalidateLastSearch();
673- m_delayedClear = true;
674- m_clearTimer.start(CLEAR_TIMEOUT);
675+ m_delayedSearchProcessing = true;
676+ m_category_results.clear();
677+ m_categories->markNewSearch();
678+
679+ m_searchProcessingDelayTimer.start(SEARCH_PROCESSING_DELAY);
680 /* There are a few objects associated with searches:
681 * 1) SearchResultReceiver 2) ResultCollector 3) PushEvent
682 *
683
684=== modified file 'src/Unity/scope.h'
685--- src/Unity/scope.h 2015-11-02 10:11:24 +0000
686+++ src/Unity/scope.h 2015-11-02 10:11:24 +0000
687@@ -231,12 +231,13 @@
688 bool m_isActive;
689 bool m_searchInProgress;
690 bool m_resultsDirty;
691- bool m_delayedClear;
692+ bool m_delayedSearchProcessing;
693 bool m_hasNavigation;
694 bool m_hasAltNavigation;
695 bool m_favorite;
696 bool m_initialQueryDone;
697
698+ QMap<std::string, QList<std::shared_ptr<unity::scopes::CategorisedResult>>> m_category_results;
699 std::unique_ptr<CollectionController> m_searchController;
700 std::unique_ptr<CollectionController> m_activationController;
701 unity::scopes::ScopeProxy m_proxy;
702@@ -253,8 +254,7 @@
703 QSharedPointer<DepartmentNode> m_departmentTree;
704 QSharedPointer<DepartmentNode> m_altNavTree;
705 QTimer m_typingTimer;
706- QTimer m_aggregatorTimer;
707- QTimer m_clearTimer;
708+ QTimer m_searchProcessingDelayTimer;
709 QTimer m_invalidateTimer;
710 QList<std::shared_ptr<unity::scopes::CategorisedResult>> m_cachedResults;
711 QMultiMap<QString, Department*> m_departmentModels;
712
713=== modified file 'tests/data/CMakeLists.txt'
714--- tests/data/CMakeLists.txt 2014-12-01 14:13:38 +0000
715+++ tests/data/CMakeLists.txt 2015-11-02 10:11:24 +0000
716@@ -4,6 +4,7 @@
717 add_subdirectory(mock-scope-double-nav)
718 add_subdirectory(mock-scope-info)
719 add_subdirectory(mock-scope-ttl)
720+add_subdirectory(mock-scope-manyresults)
721 add_subdirectory(scopes)
722
723 configure_file(Runtime.ini.in Runtime.ini @ONLY)
724
725=== added directory 'tests/data/mock-scope-manyresults'
726=== added file 'tests/data/mock-scope-manyresults/CMakeLists.txt'
727--- tests/data/mock-scope-manyresults/CMakeLists.txt 1970-01-01 00:00:00 +0000
728+++ tests/data/mock-scope-manyresults/CMakeLists.txt 2015-11-02 10:11:24 +0000
729@@ -0,0 +1,6 @@
730+include_directories(${SCOPESLIB_INCLUDE_DIRS})
731+
732+add_library(mock-scope-manyresults MODULE mock-scope-manyresults.cpp)
733+target_link_libraries(mock-scope-manyresults ${SCOPESLIB_LDFLAGS})
734+
735+configure_file(mock-scope-manyresults.ini.in mock-scope-manyresults.ini)
736
737=== added file 'tests/data/mock-scope-manyresults/mock-scope-manyresults.cpp'
738--- tests/data/mock-scope-manyresults/mock-scope-manyresults.cpp 1970-01-01 00:00:00 +0000
739+++ tests/data/mock-scope-manyresults/mock-scope-manyresults.cpp 2015-11-02 10:11:24 +0000
740@@ -0,0 +1,185 @@
741+/*
742+ * Copyright (C) 2015 Canonical, Ltd.
743+ *
744+ * This program is free software; you can redistribute it and/or modify
745+ * it under the terms of the GNU General Public License as published by
746+ * the Free Software Foundation; version 3.
747+ *
748+ * This program is distributed in the hope that it will be useful,
749+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
750+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
751+ * GNU General Public License for more details.
752+ *
753+ * You should have received a copy of the GNU General Public License
754+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
755+ *
756+ * Authors:
757+ * Pawel Stolowski <pawel.stolowski@canonical.com>
758+ */
759+
760+#include <unity-scopes.h>
761+
762+#include <iostream>
763+#include <thread>
764+
765+#define EXPORT __attribute__ ((visibility ("default")))
766+
767+using namespace std;
768+using namespace unity::scopes;
769+
770+// Example scope A: replies synchronously to a query. (Replies are returned before returning from the run() method.)
771+
772+class MyQuery : public SearchQueryBase
773+{
774+public:
775+ MyQuery(CannedQuery const& query, SearchMetadata const& metadata, VariantMap const& settings) :
776+ SearchQueryBase(query, metadata),
777+ query_(query.query_string()),
778+ settings_(settings)
779+ {
780+ }
781+
782+ ~MyQuery() noexcept
783+ {
784+ }
785+
786+ virtual void cancelled() override
787+ {
788+ }
789+
790+ virtual void run(SearchReplyProxy const& reply) override
791+ {
792+ CategoryRenderer meta_rndr(R"({"schema-version": 1, "components": {"title": "title", "art": "art", "subtitle": "subtitle", "emblem": "icon", "mascot": "mascot"}})");
793+ auto cat1 = reply->register_category("cat1", "Category 1", "", meta_rndr);
794+ auto cat2 = reply->register_category("cat2", "Category 2", "", meta_rndr);
795+
796+ if (query_ == "search1")
797+ {
798+ // five results with uris 0..4 in a single category cat1
799+ for (int i = 0; i<5; i++) {
800+ CategorisedResult res(cat1);
801+ res.set_uri("cat1_uri" + std::to_string(i));
802+ res.set_title("result " + std::to_string(i) + " for: \"" + query_ + "\"");
803+ reply->push(res);
804+ }
805+ std::this_thread::sleep_for(std::chrono::seconds(1));
806+ }
807+ else if (query_ == "search2")
808+ {
809+ const int start = 3;
810+ // five results with uris 3..7 in categories cat1 and cat2
811+ for (int i = 0; i<5; i++) {
812+ {
813+ CategorisedResult res(cat1);
814+ res.set_uri("cat1_uri" + std::to_string(start + i));
815+ res.set_title("result " + std::to_string(start + i) + " for: \"" + query_ + "\"");
816+ reply->push(res);
817+ }
818+ {
819+ CategorisedResult res(cat2);
820+ res.set_uri("cat2_uri" + std::to_string(start + i));
821+ res.set_title("result " + std::to_string(start + i) + " for: \"" + query_ + "\"");
822+ reply->push(res);
823+ }
824+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
825+ }
826+ }
827+ else if (query_ == "search3")
828+ {
829+ const int start = 3;
830+ // five results with uris 7..3 in categories cat1 and cat2
831+ for (int i = 4; i>=0; i--) {
832+ {
833+ CategorisedResult res(cat1);
834+ res.set_uri("cat1_uri" + std::to_string(start + i));
835+ res.set_title("result " + std::to_string(start + i) + " for: \"" + query_ + "\"");
836+ reply->push(res);
837+ }
838+ {
839+ CategorisedResult res(cat2);
840+ res.set_uri("cat2_uri" + std::to_string(start + i));
841+ res.set_title("result " + std::to_string(start + i) + " for: \"" + query_ + "\"");
842+ reply->push(res);
843+ }
844+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
845+ }
846+ }
847+ else if (query_ == "search4")
848+ {
849+ // one result with uri 5 in cat2
850+ {
851+ CategorisedResult res(cat2);
852+ res.set_uri("cat2_uri5");
853+ res.set_title("result5 for: \"" + query_ + "\"");
854+ reply->push(res);
855+ }
856+ }
857+
858+ }
859+
860+private:
861+ string query_;
862+ VariantMap settings_;
863+};
864+
865+class MyPreview : public PreviewQueryBase
866+{
867+public:
868+ MyPreview(Result const& result, ActionMetadata const& metadata) :
869+ PreviewQueryBase(result, metadata),
870+ scope_data_(metadata.scope_data())
871+ {
872+ }
873+
874+ ~MyPreview() noexcept
875+ {
876+ }
877+
878+ virtual void cancelled() override
879+ {
880+ }
881+
882+ virtual void run(PreviewReplyProxy const&) override
883+ {
884+ }
885+
886+private:
887+ Variant scope_data_;
888+};
889+
890+class MyScope : public ScopeBase
891+{
892+public:
893+ virtual SearchQueryBase::UPtr search(CannedQuery const& q, SearchMetadata const& metadata) override
894+ {
895+ SearchQueryBase::UPtr query(new MyQuery(q, metadata, settings()));
896+ return query;
897+ }
898+
899+ virtual PreviewQueryBase::UPtr preview(Result const& result, ActionMetadata const& metadata) override
900+ {
901+ PreviewQueryBase::UPtr query(new MyPreview(result, metadata));
902+ return query;
903+ }
904+};
905+
906+extern "C"
907+{
908+
909+ EXPORT
910+ unity::scopes::ScopeBase*
911+ // cppcheck-suppress unusedFunction
912+ UNITY_SCOPE_CREATE_FUNCTION()
913+ {
914+ return new MyScope;
915+ }
916+
917+ EXPORT
918+ void
919+ // cppcheck-suppress unusedFunction
920+ UNITY_SCOPE_DESTROY_FUNCTION(unity::scopes::ScopeBase* scope_base)
921+ {
922+ delete scope_base;
923+ }
924+
925+}
926
927=== added file 'tests/data/mock-scope-manyresults/mock-scope-manyresults.ini.in'
928--- tests/data/mock-scope-manyresults/mock-scope-manyresults.ini.in 1970-01-01 00:00:00 +0000
929+++ tests/data/mock-scope-manyresults/mock-scope-manyresults.ini.in 2015-11-02 10:11:24 +0000
930@@ -0,0 +1,14 @@
931+[ScopeConfig]
932+DisplayName = mock.DisplayName
933+Description = mock.Description
934+Art = /mock.Art
935+Icon = /mock.Icon
936+SearchHint = mock.SearchHint
937+HotKey = mock.HotKey
938+Author = mock.Author
939+
940+[Appearance]
941+PageHeader.Logo = http://assets.ubuntu.com/sites/ubuntu/1110/u/img/logos/logo-ubuntu-orange.svg
942+PageHeader.ForegroundColor = white
943+PageHeader.Background = color://black
944+ShapeImages = false
945
946=== modified file 'tests/resultstest.cpp'
947--- tests/resultstest.cpp 2015-11-02 10:11:24 +0000
948+++ tests/resultstest.cpp 2015-11-02 10:11:24 +0000
949@@ -101,7 +101,8 @@
950 shr::CustomRegistry::Parameters({
951 TEST_DATA_DIR "mock-scope/mock-scope.ini",
952 TEST_DATA_DIR "mock-scope-info/mock-scope-info.ini",
953- TEST_DATA_DIR "mock-scope-ttl/mock-scope-ttl.ini"
954+ TEST_DATA_DIR "mock-scope-ttl/mock-scope-ttl.ini",
955+ TEST_DATA_DIR "mock-scope-manyresults/mock-scope-manyresults.ini"
956 })
957 );
958 }
959@@ -573,41 +574,6 @@
960 );
961 }
962
963-// FIXME Add code to harness to test special categories
964-// void testSpecialCategory()
965-// {
966-// auto resultsView = m_harness->resultsView();
967-// resultsView->setActiveScope("mock-scope");
968-// resultsView->setQuery("");
969-//
970-// auto categories = resultsView->raw_categories();
971-// QString rawTemplate(R"({"schema-version": 1, "template": {"category-layout": "special"}})");
972-// CountObject* countObject = new CountObject(categories);
973-// categories->addSpecialCategory("special", "Special", "", rawTemplate, countObject);
974-//
975-// // should have 2 categories now
976-// QCOMPARE(categories->rowCount(), 2);
977-// QCOMPARE(categories->data(categories->index(0), ss::CategoriesInterface::Roles::RoleCount).toInt(), 0);
978-// countObject->setCount(1);
979-// QCOMPARE(categories->data(categories->index(0), ss::CategoriesInterface::Roles::RoleCount).toInt(), 1);
980-//
981-// qRegisterMetaType<QVector<int>>();
982-// QSignalSpy spy(categories, SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)));
983-//
984-// countObject->setCountAsync(13);
985-// QCOMPARE(categories->data(categories->index(0), ss::CategoriesInterface::Roles::RoleCount).toInt(), 1);
986-// QTRY_COMPARE(categories->data(categories->index(0), ss::CategoriesInterface::Roles::RoleCount).toInt(), 13);
987-//
988-// // expecting a few dataChanged signals, count should have changed
989-// bool countChanged = false;
990-// while (!spy.empty() && !countChanged) {
991-// QList<QVariant> arguments = spy.takeFirst();
992-// auto roles = arguments.at(2).value<QVector<int>>();
993-// countChanged |= roles.contains(ss::CategoriesInterface::Roles::RoleCount);
994-// }
995-// QCOMPARE(countChanged, true);
996-// }
997-
998 void testCategoryWithRating()
999 {
1000 auto resultsView = m_harness->resultsView();
1001@@ -1027,6 +993,76 @@
1002 QCOMPARE(resultsView->status(), ss::ScopeInterface::Status::NoInternet);
1003 }
1004
1005+ void testResultsModelChanges()
1006+ {
1007+ auto resultsView = m_harness->resultsView();
1008+ resultsView->setActiveScope("mock-scope-manyresults");
1009+ resultsView->setQuery("search1");
1010+ QVERIFY_MATCHRESULT(
1011+ shm::CategoryListMatcher()
1012+ .hasExactly(1)
1013+ .category(shm::CategoryMatcher("cat1")
1014+ .result(shm::ResultMatcher("cat1_uri0"))
1015+ .result(shm::ResultMatcher("cat1_uri1"))
1016+ .result(shm::ResultMatcher("cat1_uri2"))
1017+ .result(shm::ResultMatcher("cat1_uri3"))
1018+ .result(shm::ResultMatcher("cat1_uri4"))
1019+ )
1020+ .match(resultsView->categories())
1021+ );
1022+
1023+ resultsView->setQuery("search2");
1024+ QVERIFY_MATCHRESULT(
1025+ shm::CategoryListMatcher()
1026+ .hasExactly(2)
1027+ .category(shm::CategoryMatcher("cat1")
1028+ .result(shm::ResultMatcher("cat1_uri3"))
1029+ .result(shm::ResultMatcher("cat1_uri4"))
1030+ .result(shm::ResultMatcher("cat1_uri5"))
1031+ .result(shm::ResultMatcher("cat1_uri6"))
1032+ .result(shm::ResultMatcher("cat1_uri7"))
1033+ )
1034+ .category(shm::CategoryMatcher("cat2")
1035+ .result(shm::ResultMatcher("cat2_uri3"))
1036+ .result(shm::ResultMatcher("cat2_uri4"))
1037+ .result(shm::ResultMatcher("cat2_uri5"))
1038+ .result(shm::ResultMatcher("cat2_uri6"))
1039+ .result(shm::ResultMatcher("cat2_uri7"))
1040+ )
1041+ .match(resultsView->categories())
1042+ );
1043+
1044+ resultsView->setQuery("search3");
1045+ QVERIFY_MATCHRESULT(
1046+ shm::CategoryListMatcher()
1047+ .hasExactly(2)
1048+ .category(shm::CategoryMatcher("cat1")
1049+ .result(shm::ResultMatcher("cat1_uri7"))
1050+ .result(shm::ResultMatcher("cat1_uri6"))
1051+ .result(shm::ResultMatcher("cat1_uri5"))
1052+ .result(shm::ResultMatcher("cat1_uri4"))
1053+ .result(shm::ResultMatcher("cat1_uri3"))
1054+ )
1055+ .category(shm::CategoryMatcher("cat2")
1056+ .result(shm::ResultMatcher("cat2_uri7"))
1057+ .result(shm::ResultMatcher("cat2_uri6"))
1058+ .result(shm::ResultMatcher("cat2_uri5"))
1059+ .result(shm::ResultMatcher("cat2_uri4"))
1060+ .result(shm::ResultMatcher("cat2_uri3"))
1061+ )
1062+ .match(resultsView->categories())
1063+ );
1064+
1065+ resultsView->setQuery("search4");
1066+ QVERIFY_MATCHRESULT(
1067+ shm::CategoryListMatcher()
1068+ .hasExactly(1)
1069+ .category(shm::CategoryMatcher("cat2")
1070+ .result(shm::ResultMatcher("cat2_uri5"))
1071+ )
1072+ .match(resultsView->categories())
1073+ );
1074+ }
1075 };
1076
1077 QTEST_GUILESS_MAIN(ResultsTest)

Subscribers

People subscribed via source and target branches

to all changes: