1 /*
2 ** Copyright (C) 2021 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
3 **
4 ** This program is free software; you can redistribute it and/or modify it
5 ** under the terms of the GNU General Public License as published by the
6 ** Free Software Foundation; either version 3, or (at your option) any
7 ** later version.
8 **
9 ** This program is distributed in the hope that it will be useful,
10 ** but WITHOUT ANY WARRANTY; without even the implied warranty of
11 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 ** GNU General Public License for more details.
13 **
14 ** You should have received a copy of the GNU General Public License
15 ** along with this program; if not, write to the Free Software Foundation,
16 ** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 **
18 */
19
20 #include "mu-query-threads.hh"
21 #include "mu-msg-fields.h"
22
23 #include <set>
24 #include <unordered_set>
25 #include <list>
26 #include <cassert>
27 #include <cstring>
28 #include <iostream>
29 #include <iomanip>
30
31
32 #include <utils/mu-option.hh>
33
34 using namespace Mu;
35
36 struct Container {
37 using Containers = std::vector<Container*>;
38
39 Container() = default;
ContainerContainer40 Container(Option<QueryMatch&> msg): query_match{msg} {}
41 Container(const Container&) = delete;
42 Container(Container&&) = default;
43
add_childContainer44 void add_child (Container& new_child) {
45 new_child.parent = this;
46 children.emplace_back(&new_child);
47 }
remove_childContainer48 void remove_child (Container& child) {
49 children.erase(find_child(child));
50 assert(!has_child(child));
51 }
52
find_childContainer53 Containers::iterator find_child (Container& child) {
54 return std::find_if(children.begin(), children.end(),
55 [&](auto&& c){ return c == &child; });
56 }
find_childContainer57 Containers::const_iterator find_child (Container& child) const {
58 return std::find_if(children.begin(), children.end(),
59 [&](auto&& c){ return c == &child; });
60 }
has_childContainer61 bool has_child (Container& child) const {
62 return find_child(child) != children.cend();
63 }
64
is_reachableContainer65 bool is_reachable(Container* other) const {
66 auto up{ur_parent()};
67 return up && up == other->ur_parent();
68 }
for_each_childContainer69 template <typename Func> void for_each_child (Func&& func) {
70 auto it{children.rbegin()};
71 while (it != children.rend()) {
72 auto next = std::next(it);
73 func(*it);
74 it = next;
75 }
76 }
77 // During sorting, this is the cached value for the (recursive) date-key
78 // of this container -- ie.. either the one from the first of its
79 // children, or from its query-match, if it has no children.
80 //
81 // Note that the sub-root-levels of threads are always sorted by date,
82 // in ascending order, regardless of whatever sorting was specified for
83 // the root-level.
84
85
86 std::string thread_date_key;
87
88
89 Option<QueryMatch&> query_match;
90 bool is_nuked{};
91 Container* parent{};
92 Containers children;
93
94 using ContainerVec = std::vector<Container*>;
95
96 private:
ur_parentContainer97 const Container* ur_parent() const {
98 assert(this->parent != this);
99 return parent ? parent->ur_parent() : this;
100 }
101 };
102
103 using Containers = Container::Containers;
104 using ContainerVec = Container::ContainerVec;
105
106 static std::ostream&
operator <<(std::ostream & os,const Container & container)107 operator<<(std::ostream& os, const Container& container)
108 {
109 os << "container: " << std::right << std::setw(10) << &container
110 << ": parent: " << std::right << std::setw(10) << container.parent
111 << " [" << container.thread_date_key << "]"
112 << "\n children: ";
113
114 for (auto&& c: container.children)
115 os << std::right << std::setw(10) << c << " ";
116
117 os << (container.is_nuked ? " nuked" : "");
118
119 if (container.query_match)
120 os << "\n " << container.query_match.value();
121
122 return os;
123 }
124
125
126 using IdTable = std::unordered_map<std::string, Container>;
127 using DupTable = std::multimap<std::string, Container>;
128
129 static void
handle_duplicates(IdTable & id_table,DupTable & dup_table)130 handle_duplicates (IdTable& id_table, DupTable& dup_table)
131 {
132 size_t n{};
133
134 for (auto&& dup: dup_table) {
135 const auto msgid{dup.first};
136 auto it = id_table.find(msgid);
137 if (it == id_table.end())
138 continue;
139
140 // add duplicates as fake children
141 char buf[32];
142 ::snprintf(buf, sizeof(buf), "dup-%zu", ++n);
143 it->second.add_child(
144 id_table.emplace(buf, std::move(dup.second)).first->second);
145 }
146 }
147
148 template <typename QueryResultsType>
149 static IdTable
determine_id_table(QueryResultsType & qres)150 determine_id_table (QueryResultsType& qres)
151 {
152 // 1. For each query_match
153 IdTable id_table;
154 DupTable dups;
155 for (auto&& mi: qres) {
156 const auto msgid{mi.message_id().value_or(*mi.path())};
157 // Step 0 (non-JWZ): filter out dups, handle those at the end
158 if (mi.query_match().has_flag(QueryMatch::Flags::Duplicate)) {
159 dups.emplace(msgid, mi.query_match());
160 continue;
161 }
162 // 1.A If id_table contains an empty Container for this ID:
163 // Store this query_match (query_match) in the Container's query_match (value) slot.
164 // Else:
165 // Create a new Container object holding this query_match (query-match);
166 // Index the Container by Query_Match-ID
167 auto c_it = id_table.find(msgid);
168 auto& container = [&]()->Container& {
169 if (c_it != id_table.end()) {
170 if (!c_it->second.query_match) // hmm, dup?
171 c_it->second.query_match = mi.query_match();
172 return c_it->second;
173 } else {
174 // Else:
175 // Create a new Container object holding this query_match (query-match);
176 // Index the Container by Query_Match-ID
177 return id_table.emplace(msgid, mi.query_match()).first->second;
178 }
179 }();
180
181 // We sort by date (ascending), *except* for the root; we don't
182 // know what query_matchs will be at the root level yet, so remember
183 // both. Moreover, even when sorting the top-level in descending
184 // order, still sort the thread levels below that in ascending
185 // order.
186 container.thread_date_key = container.query_match->date_key =
187 mi.date().value_or("");
188 // initial guess for the thread-date; might be updated
189 // later.
190
191 // remember the subject, we use it to determine the (sub)thread subject
192 container.query_match->subject = mi.subject().value_or("");
193
194 // 1.B
195 // For each element in the query_match's References field:
196 Container* parent_ref_container{};
197 for (const auto& ref: mi.references()) {
198 // grand_<n>-parent -> grand_<n-1>-parent -> ... -> parent.
199
200 // Find a Container object for the given Query_Match-ID; If it exists, use it;
201 // otherwise make one with a null Query_Match.
202 auto ref_container = [&]()->Container* {
203 auto ref_it = id_table.find(ref);
204 if (ref_it == id_table.end())
205 ref_it = id_table.emplace(ref,Nothing).first;
206 return &ref_it->second;
207 }();
208
209 // Link the References field's Containers together in the order implied
210 // by the References header.
211 // * If they are already linked, don't change the existing links.
212 //
213 // * Do not add a link if adding that link would introduce a loop: that is,
214 // before asserting A->B, search down the children of B to see if A is
215 // reachable, and also search down the children of A to see if B is
216 // reachable. If either is already reachable as a child of the other,
217 // don't add the link.
218 if (parent_ref_container && !ref_container->parent) {
219 if (!parent_ref_container->is_reachable(ref_container))
220 parent_ref_container->add_child(*ref_container);
221 // else
222 // g_message ("%u: reachable %s -> %s", __LINE__, msgid.c_str(), ref.c_str());
223 }
224
225 parent_ref_container = ref_container;
226 }
227
228 // Add the query_match to the chain.
229 if (parent_ref_container && !container.parent) {
230 if (!parent_ref_container->is_reachable(&container))
231 parent_ref_container->add_child(container);
232 // else
233 // g_message ("%u: reachable %s -> parent", __LINE__, msgid.c_str());
234 }
235 }
236
237 // non-JWZ: add duplicate messages.
238 handle_duplicates (id_table, dups);
239
240 return id_table;
241 }
242
243 /// Recursively walk all containers under the root set.
244 /// For each container:
245 ///
246 /// If it is an empty container with no children, nuke it.
247 ///
248 /// Note: Normally such containers won't occur, but they can show up when two
249 /// query_matchs have References lines that disagree. For example, assuming A and
250 /// B are query_matchs, and 1, 2, and 3 are references for query_matchs we haven't
251 /// seen:
252 ///
253 /// A has references: 1, 2, 3
254 /// B has references: 1, 3
255 ///
256 /// There is ambiguity as to whether 3 is a child of 1 or of 2. So,
257 /// depending on the processing order, we might end up with either
258 ///
259 /// -- 1
260 /// |-- 2
261 /// \-- 3
262 /// |-- A
263 /// \-- B
264 ///
265 /// or
266 ///
267 /// -- 1
268 /// |-- 2 <--- non root childless container!
269 /// \-- 3
270 /// |-- A
271 /// \-- B
272 ///
273 /// If the Container has no Query_Match, but does have children, remove this
274 /// container but promote its children to this level (that is, splice them in
275 /// to the current child list.)
276 ///
277 /// Do not promote the children if doing so would promote them to the root
278 /// set -- unless there is only one child, in which case, do.
279
280 static void
prune(Container * child)281 prune (Container* child)
282 {
283 Container *container{child->parent};
284
285 for (auto& grandchild: child->children) {
286 grandchild->parent = container;
287 if (container)
288 container->children.emplace_back(grandchild);
289 }
290
291 child->children.clear();
292 child->is_nuked = true;
293
294 if (container)
295 container->remove_child(*child);
296 }
297
298
299 static bool
prune_empty_containers(Container & container)300 prune_empty_containers (Container& container)
301 {
302 Containers to_prune;
303
304 container.for_each_child([&](auto& child){
305 if (prune_empty_containers(*child))
306 to_prune.emplace_back(child);
307 });
308
309 for (auto& child: to_prune)
310 prune (child);
311
312 // Never nuke these.
313 if (container.query_match)
314 return false;
315
316 // If it is an empty container with no children, nuke it.
317 //
318 // If the Container is empty, but does have children, remove this
319 // container but promote its children to this level (that is, splice them in
320 // to the current child list.)
321 //
322 // Do not promote the children if doing so would promote them to the root
323 // set -- unless there is only one child, in which case, do.
324 //const auto rootset_child{!container.parent->parent};
325 if (container.parent || container.children.size() <= 1)
326 return true; // splice/nuke it.
327
328 return false;
329 }
330
331
332 static void
prune_empty_containers(IdTable & id_table)333 prune_empty_containers (IdTable& id_table)
334 {
335 for (auto&& item: id_table) {
336
337 auto& child(item.second);
338 if (child.parent)
339 continue; // not a root child.
340
341 if (prune_empty_containers(item.second))
342 prune(&child);
343 }
344 }
345
346 //
347 // Sorting.
348 //
349
350
351 /// Register some information about a match (i.e., message) that we can use for
352 /// subsequent queries.
353 using ThreadPath = std::vector<unsigned>;
354 inline std::string
to_string(const ThreadPath & tpath,size_t digits)355 to_string (const ThreadPath& tpath, size_t digits)
356 {
357 std::string str;
358 str.reserve(tpath.size() * digits);
359
360 bool first{true};
361 for (auto&& segm: tpath) {
362 str += format("%s%0*x", first ? "" : ":", (int)digits, segm);
363 first = false;
364 }
365
366 return str;
367 }
368
369 static bool // compare subjects, ignore anything before the last ':<space>*'
subject_matches(const std::string & sub1,const std::string & sub2)370 subject_matches (const std::string& sub1, const std::string& sub2)
371 {
372 auto search_str =[](const std::string&s) -> const char* {
373 const auto pos = s.find_last_of(':');
374 if (pos == std::string::npos)
375 return s.c_str();
376 else {
377 const auto pos2 = s.find_first_not_of(' ', pos + 1);
378 return s.c_str() + (pos2 == std::string::npos ? pos : pos2);
379 }
380 };
381
382 //g_debug ("'%s' '%s'", search_str(sub1), search_str(sub2));
383 return g_strcmp0(search_str(sub1), search_str(sub2)) == 0;
384 }
385
386 static bool
update_container(Container & container,bool descending,ThreadPath & tpath,size_t seg_size,const std::string & prev_subject="")387 update_container (Container& container, bool descending,
388 ThreadPath& tpath, size_t seg_size,
389 const std::string& prev_subject="")
390 {
391 if (!container.children.empty()) {
392 Container* first = container.children.front();
393 if (first->query_match)
394 first->query_match->flags |= QueryMatch::Flags::First;
395 Container* last = container.children.back();
396 if (last->query_match)
397 last->query_match->flags |= QueryMatch::Flags::Last;
398 }
399
400 if (!container.query_match)
401 return false; // nothing else to do.
402
403 auto& qmatch(*container.query_match);
404 if (!container.parent)
405 qmatch.flags |= QueryMatch::Flags::Root;
406 else if (!container.parent->query_match)
407 qmatch.flags |= QueryMatch::Flags::Orphan;
408
409 if (!container.children.empty())
410 qmatch.flags |= QueryMatch::Flags::HasChild;
411
412 if (qmatch.has_flag(QueryMatch::Flags::Root) || prev_subject.empty() ||
413 !subject_matches(prev_subject, qmatch.subject))
414 qmatch.flags |= QueryMatch::Flags::ThreadSubject;
415
416 if (descending && container.parent) {
417 // trick xapian by giving it "inverse" sorting key so our
418 // ascending-date sorted threads stay in that order
419 tpath.back() = ((1U << (4 * seg_size)) - 1) - tpath.back();
420 }
421
422 qmatch.thread_path = to_string(tpath, seg_size);
423 qmatch.thread_level = tpath.size() - 1;
424
425 // ensure thread root comes before its children
426 if (descending)
427 qmatch.thread_path += ":z";
428
429 return true;
430 }
431
432
433 static void
update_containers(Containers & children,bool descending,ThreadPath & tpath,size_t seg_size,std::string & prev_subject)434 update_containers (Containers& children, bool descending, ThreadPath& tpath,
435 size_t seg_size, std::string& prev_subject)
436 {
437 size_t idx{0};
438
439 for (auto&& c: children) {
440 tpath.emplace_back(idx++);
441 if (c->query_match) {
442 update_container(*c, descending, tpath, seg_size,
443 prev_subject);
444 prev_subject = c->query_match->subject;
445 }
446 update_containers(c->children, descending, tpath, seg_size,
447 prev_subject);
448 tpath.pop_back();
449 }
450 }
451
452 static void
update_containers(ContainerVec & root_vec,bool descending,size_t n)453 update_containers (ContainerVec& root_vec, bool descending, size_t n)
454 {
455 ThreadPath tpath;
456 tpath.reserve (n);
457
458 const auto seg_size = static_cast<size_t>(std::ceil(std::log2(n)/4.0));
459 /*note: 4 == std::log2(16)*/
460
461 size_t idx{0};
462 for (auto&& c: root_vec) {
463 tpath.emplace_back(idx++);
464 std::string prev_subject;
465 if (update_container(*c, descending, tpath, seg_size))
466 prev_subject = c->query_match->subject;
467 update_containers(c->children, descending, tpath, seg_size,
468 prev_subject);
469 tpath.pop_back();
470 }
471 }
472
473
474 static void
sort_container(Container & container)475 sort_container (Container& container)
476 {
477 // 1. childless container.
478 if (container.children.empty())
479 return; // no children; nothing to sort.
480
481 // 2. container with children.
482 // recurse, depth-first: sort the children
483 for (auto& child: container.children)
484 sort_container(*child);
485
486 // now sort this level.
487 std::sort(container.children.begin(), container.children.end(),
488 [&](auto&& c1, auto&& c2) {
489 return c1->thread_date_key < c2->thread_date_key;
490 });
491
492 // and 'bubble up' the date of the *newest* message with a date. We
493 // reasonably assume that it's later than its parent.
494 const auto& newest_date = container.children.back()->thread_date_key;
495 if (!newest_date.empty())
496 container.thread_date_key = newest_date;
497 }
498
499
500 static void
sort_siblings(IdTable & id_table,bool descending)501 sort_siblings (IdTable& id_table, bool descending)
502 {
503 if (id_table.empty())
504 return;
505
506 // unsorted vec of root containers. We can
507 // only sort these _after_ sorting the children.
508 ContainerVec root_vec;
509 for (auto&& item: id_table) {
510 if (!item.second.parent && !item.second.is_nuked)
511 root_vec.emplace_back(&item.second);
512 }
513
514 // now sort all threads _under_ the root set (by date/ascending)
515 for (auto&& c: root_vec)
516 sort_container(*c);
517
518 // and then sort the root set.
519 //
520 // The difference with the sub-root containers is that at the top-level,
521 // we can sort either in ascending or descending order, while on the
522 // subroot level it's always in ascending order.
523 //
524 // Note that unless we're testing, _xapian_ will handle
525 // the ascending/descending of the top level.
526 std::sort(root_vec.begin(), root_vec.end(), [&](auto&& c1, auto&& c2) {
527 #ifdef BUILD_TESTS
528 if (descending)
529 return c2->thread_date_key < c1->thread_date_key;
530 else
531 #endif /*BUILD_TESTS*/
532 return c1->thread_date_key < c2->thread_date_key;
533 });
534
535 // now all is sorted... final step is to determine thread paths and
536 // other flags.
537 update_containers (root_vec, descending, id_table.size());
538 }
539
540 static std::ostream&
operator <<(std::ostream & os,const IdTable & id_table)541 operator<<(std::ostream& os, const IdTable& id_table)
542 {
543 os << "------------------------------------------------\n";
544 for (auto&& item: id_table) {
545 os << item.first << " => " << item.second << "\n";
546 }
547 os << "------------------------------------------------\n";
548
549 std::set<std::string> ids;
550 for (auto&& item: id_table) {
551 if (item.second.query_match)
552 ids.emplace(item.second.query_match->thread_path);
553 }
554
555 for (auto&& id: ids) {
556 auto it = std::find_if(id_table.begin(), id_table.end(), [&](auto&& item) {
557 return item.second.query_match &&
558 item.second.query_match->thread_path == id;
559 });
560 assert(it != id_table.end());
561 os << it->first << ": " << it->second << '\n';
562 }
563 return os;
564 }
565
566
567 template<typename Results> static void
calculate_threads_real(Results & qres,bool descending)568 calculate_threads_real (Results& qres, bool descending)
569 {
570 // Step 1: build the id_table
571 auto id_table{determine_id_table(qres)};
572
573 if (g_test_verbose())
574 std::cout << "*** id-table(1):\n" << id_table << "\n";
575
576 // // Step 2: get the root set
577 // // Step 3: discard id_table
578 // Nope: id-table owns the containers.
579 // Step 4: prune empty containers
580 prune_empty_containers(id_table);
581
582 // Step 5: group root-set by subject.
583 // Not implemented.
584
585 // Step 6: we're done threading
586
587 // Step 7: sort siblings. The segment-size is the number of hex-digits
588 // in the thread-path string (so we can lexically compare them.)
589 sort_siblings(id_table, descending);
590
591 // Step 7a:. update querymatches
592 for (auto&& item: id_table) {
593 Container& c{item.second};
594 if (c.query_match)
595 c.query_match->thread_date = c.thread_date_key;
596 }
597 // if (g_test_verbose())
598 // std::cout << "*** id-table(2):\n" << id_table << "\n";
599 }
600
601 void
calculate_threads(Mu::QueryResults & qres,bool descending)602 Mu::calculate_threads (Mu::QueryResults& qres, bool descending)
603 {
604 calculate_threads_real(qres, descending);
605 }
606
607 #ifdef BUILD_TESTS
608
609 struct MockQueryResult {
MockQueryResultMockQueryResult610 MockQueryResult(const std::string& message_id_arg,
611 const std::string& date_arg,
612 const std::vector<std::string>& refs_arg={}):
613 message_id_{message_id_arg},
614 date_{date_arg},
615 refs_{refs_arg}
616 {}
MockQueryResultMockQueryResult617 MockQueryResult(const std::string& message_id_arg,
618 const std::vector<std::string>& refs_arg={}):
619 MockQueryResult(message_id_arg, "", refs_arg) {}
message_idMockQueryResult620 Option<std::string> message_id() const { return message_id_;}
pathMockQueryResult621 Option<std::string> path() const { return path_;}
dateMockQueryResult622 Option<std::string> date() const { return date_;}
subjectMockQueryResult623 Option<std::string> subject() const { return subject_;}
query_matchMockQueryResult624 QueryMatch& query_match() { return query_match_;}
query_matchMockQueryResult625 const QueryMatch& query_match() const { return query_match_;}
referencesMockQueryResult626 const std::vector<std::string>& references() const { return refs_;}
627
628 std::string path_;
629 std::string message_id_;
630 QueryMatch query_match_{};
631 std::string date_;
632 std::string subject_;
633 std::vector<std::string> refs_;
634 };
635
636 using MockQueryResults = std::vector<MockQueryResult>;
637
638
639 G_GNUC_UNUSED static std::ostream&
operator <<(std::ostream & os,const MockQueryResults & qrs)640 operator<<(std::ostream& os, const MockQueryResults& qrs)
641 {
642 for (auto&& mi: qrs)
643 os << mi.query_match().thread_path << " :: "
644 << mi.message_id().value_or("<none>") << std::endl;
645
646 return os;
647 }
648
649 static void
calculate_threads(MockQueryResults & qres,bool descending)650 calculate_threads (MockQueryResults& qres, bool descending)
651 {
652 calculate_threads_real(qres, descending);
653 }
654
655 using Expected = std::vector<std::pair<std::string, std::string>>;
656
657
658 static void
assert_thread_paths(const MockQueryResults & qrs,const Expected & expected)659 assert_thread_paths (const MockQueryResults& qrs, const Expected& expected)
660 {
661 for (auto&& exp: expected) {
662 auto it = std::find_if(qrs.begin(), qrs.end(), [&](auto&& qr){
663 return qr.message_id().value_or("") == exp.first ||
664 qr.path().value_or("") == exp.first;
665 });
666 g_assert_true (it != qrs.end());
667 g_assert_cmpstr(exp.second.c_str(), ==,
668 it->query_match().thread_path.c_str());
669 }
670 }
671
672 static void
test_sort_ascending()673 test_sort_ascending()
674 {
675 auto results = MockQueryResults {
676 MockQueryResult{ "m1", "1", {"m2"} },
677 MockQueryResult{ "m2", "2", {"m3"} },
678 MockQueryResult{ "m3", "3", {}},
679 MockQueryResult{ "m4", "4", {}}
680 };
681
682 calculate_threads(results, false);
683
684 assert_thread_paths (results, {
685 { "m1", "0:0:0"},
686 { "m2", "0:0" },
687 { "m3", "0" },
688 { "m4", "1" }
689 });
690 }
691
692 static void
test_sort_descending()693 test_sort_descending()
694 {
695 auto results = MockQueryResults {
696 MockQueryResult{ "m1", "1", {"m2"} },
697 MockQueryResult{ "m2", "2", {"m3"} },
698 MockQueryResult{ "m3", "3", {}},
699 MockQueryResult{ "m4", "4", {}}
700 };
701
702 calculate_threads(results, true);
703
704 assert_thread_paths (results, {
705 { "m1", "1:f:f:z"},
706 { "m2", "1:f:z" },
707 { "m3", "1:z" },
708 { "m4", "0:z" }
709 });
710 }
711
712 static void
test_id_table_inconsistent()713 test_id_table_inconsistent()
714 {
715 auto results = MockQueryResults {
716 MockQueryResult{ "m1", "1", {"m2"} }, // 1->2
717 MockQueryResult{ "m2", "2", {"m1"} }, // 2->1
718 MockQueryResult{ "m3", "3", {"m3"} }, // self ref
719 MockQueryResult{ "m4", "4", {"m3", "m5"} },
720 MockQueryResult{ "m5", "5", {"m4", "m4"} }, // dup parent
721 };
722
723 calculate_threads(results, false);
724 assert_thread_paths (results, {
725 { "m2", "0"},
726 { "m1", "0:0"},
727 { "m3", "1"},
728 { "m5", "1:0"},
729 { "m4", "1:0:0"},
730 });
731 }
732
733 static void
test_dups_dup_last()734 test_dups_dup_last()
735 {
736 MockQueryResult r1 { "m1", "1", {} };
737 r1.query_match().flags |= QueryMatch::Flags::Leader;
738 r1.path_ = "/path1";
739
740 MockQueryResult r1_dup { "m1", "1", {} };
741 r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
742 r1_dup.path_ = "/path2";
743
744 auto results = MockQueryResults {r1, r1_dup };
745
746 calculate_threads(results, false);
747
748 assert_thread_paths (results, {
749 { "/path1", "0"},
750 { "/path2", "0:0" },
751 });
752 }
753
754 static void
test_dups_dup_first()755 test_dups_dup_first()
756 {
757 // now dup becomes the leader; this will _demote_
758 // r1.
759
760 MockQueryResult r1_dup { "m1", "1", {} };
761 r1_dup.query_match().flags |= QueryMatch::Flags::Duplicate;
762 r1_dup.path_ = "/path1";
763
764 MockQueryResult r1 { "m1", "1", {} };
765 r1.query_match().flags |= QueryMatch::Flags::Leader;
766 r1.path_ = "/path2";
767
768 auto results = MockQueryResults { r1_dup, r1 };
769
770 calculate_threads(results, false);
771
772 assert_thread_paths (results, {
773 { "/path2", "0"},
774 { "/path1", "0:0" },
775 });
776 }
777
778
779 static void
test_do_not_prune_root_empty_with_children()780 test_do_not_prune_root_empty_with_children()
781 {
782 // m7 should not be nuked
783 auto results = MockQueryResults {
784 MockQueryResult{ "x1", "1", {"m7"} },
785 MockQueryResult{ "x2", "2", {"m7"} },
786 };
787
788 calculate_threads(results, false);
789
790 assert_thread_paths (results, {
791 { "x1", "0:0"},
792 { "x2", "0:1" },
793 });
794 }
795
796 static void
test_prune_root_empty_with_child()797 test_prune_root_empty_with_child()
798 {
799 // m7 should be nuked
800 auto results = MockQueryResults {
801 MockQueryResult{ "m1", "1", {"m7"} },
802 };
803
804 calculate_threads(results, false);
805
806 assert_thread_paths (results, {
807 { "m1", "0"},
808 });
809 }
810
811 static void
test_prune_empty_with_children()812 test_prune_empty_with_children()
813 {
814 // m6 should be nuked
815 auto results = MockQueryResults {
816 MockQueryResult{ "m1", "1", {"m7", "m6"} },
817 MockQueryResult{ "m2", "2", {"m7", "m6"} },
818 };
819
820 calculate_threads(results, false);
821
822 assert_thread_paths (results, {
823 { "m1", "0:0"},
824 { "m2", "0:1" },
825 });
826 }
827
828
829 static void
test_thread_info_ascending()830 test_thread_info_ascending()
831 {
832 auto results = MockQueryResults {
833 MockQueryResult{ "m1", "5", {}},
834 MockQueryResult{ "m2", "1", {}},
835 MockQueryResult{ "m3", "3", {"m2"}},
836 MockQueryResult{ "m4", "2", {"m2"}},
837 // orphan siblings
838 MockQueryResult{ "m10", "6", {"m9"}},
839 MockQueryResult{ "m11", "7", {"m9"}},
840 };
841 calculate_threads(results, false);
842
843 assert_thread_paths (results, {
844 { "m2", "0" }, // 2
845 { "m4", "0:0" }, // 2
846 { "m3", "0:1" }, // 3
847 { "m1", "1" }, // 5
848
849 { "m10", "2:0" }, // 6
850 { "m11", "2:1" }, // 7
851 });
852
853 g_assert_true (results[0].query_match().has_flag(
854 QueryMatch::Flags::Root));
855 g_assert_true (results[1].query_match().has_flag(
856 QueryMatch::Flags::Root | QueryMatch::Flags::HasChild));
857 g_assert_true (results[2].query_match().has_flag(
858 QueryMatch::Flags::Last));
859 g_assert_true (results[3].query_match().has_flag(
860 QueryMatch::Flags::First));
861 g_assert_true (results[4].query_match().has_flag(
862 QueryMatch::Flags::Orphan | QueryMatch::Flags::First));
863 g_assert_true (results[5].query_match().has_flag(
864 QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
865 }
866
867 static void
test_thread_info_descending()868 test_thread_info_descending()
869 {
870 auto results = MockQueryResults {
871 MockQueryResult{ "m1", "5", {}},
872 MockQueryResult{ "m2", "1", {}},
873 MockQueryResult{ "m3", "3", {"m2"}},
874 MockQueryResult{ "m4", "2", {"m2"}},
875 // orphan siblings
876 MockQueryResult{ "m10", "6", {"m9"}},
877 MockQueryResult{ "m11", "7", {"m9"}},
878 };
879 calculate_threads(results, true/*descending*/);
880
881 assert_thread_paths (results, {
882 { "m1", "1:z" }, // 5
883 { "m2", "2:z" }, // 2
884 { "m4", "2:f:z" }, // 2
885 { "m3", "2:e:z" }, // 3
886
887 { "m10", "0:f:z" }, // 6
888 { "m11", "0:e:z" }, // 7
889 });
890 g_assert_true (results[0].query_match().has_flag(
891 QueryMatch::Flags::Root));
892 g_assert_true (results[1].query_match().has_flag(
893 QueryMatch::Flags::Root | QueryMatch::Flags::HasChild));
894 g_assert_true (results[2].query_match().has_flag(
895 QueryMatch::Flags::Last));
896 g_assert_true (results[3].query_match().has_flag(
897 QueryMatch::Flags::First));
898
899 g_assert_true (results[4].query_match().has_flag(
900 QueryMatch::Flags::Orphan | QueryMatch::Flags::Last));
901 g_assert_true (results[5].query_match().has_flag(
902 QueryMatch::Flags::Orphan | QueryMatch::Flags::First));
903
904 }
905
906
907 int
main(int argc,char * argv[])908 main (int argc, char *argv[]) try
909 {
910 g_test_init (&argc, &argv, NULL);
911
912 g_test_add_func ("/threader/sort/ascending", test_sort_ascending);
913 g_test_add_func ("/threader/sort/decending", test_sort_descending);
914
915 g_test_add_func ("/threader/id-table-inconsistent", test_id_table_inconsistent);
916 g_test_add_func ("/threader/dups/dup-last", test_dups_dup_last);
917 g_test_add_func ("/threader/dups/dup-first", test_dups_dup_first);
918
919 g_test_add_func ("/threader/prune/do-not-prune-root-empty-with-children",
920 test_do_not_prune_root_empty_with_children);
921 g_test_add_func ("/threader/prune/prune-root-empty-with-child",
922 test_prune_root_empty_with_child);
923 g_test_add_func ("/threader/prune/prune-empty-with-children",
924 test_prune_empty_with_children);
925
926 g_test_add_func ("/threader/thread-info/ascending",
927 test_thread_info_ascending);
928 g_test_add_func ("/threader/thread-info/descending",
929 test_thread_info_descending);
930
931 return g_test_run ();
932 } catch (const std::runtime_error& re) {
933 std::cerr << re.what() << "\n";
934 return 1;
935 } catch (...) {
936 std::cerr << "caught exception\n";
937 return 1;
938 }
939
940 #endif /*BUILD_TESTS*/
941