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