[geary/wip/794174-conversation-monitor-max-cpu: 3/3] Add unit tests for ConversationMonitor, fix a few issues.



commit b8f228ca38fb93071be1c03470dd069bf4e8a38b
Author: Michael James Gratton <mike vee net>
Date:   Wed Apr 4 15:58:44 2018 +1000

    Add unit tests for ConversationMonitor, fix a few issues.
    
    * src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
      Don't check the folder blacklist for external folder signals when the
      signal is received, do it when the operation is executed so it's fresh.
    
    * src/engine/app/conversation-monitor/app-fill-window-operation.vala
      (FillWindowOperation): Only re-fill the conversation if we get a full
      load, anything else means we hit the end of the folder.
    
    * test/engine/app/app-conversation-monitor-test.vala: New unit tests
      for ConversationMonitor.
    
    * test/engine/api/geary-account-mock.vala,
      test/engine/api/geary-folder-mock.vala: Mix in MockObject mock up all
      method calls.
    
    * test/engine/api/geary-email-identifier-mock.vala: Fix the comparator
      method when the other instance is null.

 src/engine/app/app-conversation-monitor.vala       |   12 +-
 .../app-fill-window-operation.vala                 |    2 +-
 test/CMakeLists.txt                                |    1 +
 test/engine/api/geary-account-mock.vala            |  158 +++++--
 test/engine/api/geary-email-identifier-mock.vala   |    2 +-
 test/engine/api/geary-folder-mock.vala             |   32 ++-
 test/engine/app/app-conversation-monitor-test.vala |  456 ++++++++++++++++++++
 test/meson.build                                   |    1 +
 test/test-engine.vala                              |    2 +
 9 files changed, 609 insertions(+), 57 deletions(-)
---
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index 4234825..b7541f5 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -802,8 +802,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
 
     private void on_account_email_appended(Folder folder,
                                            Gee.Collection<EmailIdentifier> added) {
-        if (folder != this.base_folder &&
-            !get_search_folder_blacklist().contains(folder.path)) {
+        if (folder != this.base_folder) {
             this.queue.add(new ExternalAppendOperation(this, folder, added));
         }
     }
@@ -813,8 +812,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
         // ExternalAppendOperation will check to determine if the
         // email is relevant for some existing conversation before
         // adding it, which is what we want here.
-        if (folder != this.base_folder &&
-            !get_search_folder_blacklist().contains(folder.path)) {
+        if (folder != this.base_folder) {
             this.queue.add(new ExternalAppendOperation(this, folder, inserted));
         }
     }
@@ -824,16 +822,14 @@ public class Geary.App.ConversationMonitor : BaseObject {
         // ExternalAppendOperation will check to determine if the
         // email is relevant for some existing conversation before
         // adding it, which is what we want here.
-        if (folder != this.base_folder &&
-            !get_search_folder_blacklist().contains(folder.path)) {
+        if (folder != this.base_folder) {
             this.queue.add(new ExternalAppendOperation(this, folder, inserted));
         }
     }
 
     private void on_account_email_removed(Folder folder,
                                           Gee.Collection<EmailIdentifier> removed) {
-        if (folder != this.base_folder &&
-            !get_search_folder_blacklist().contains(folder.path)) {
+        if (folder != this.base_folder) {
             this.queue.add(new RemoveOperation(this, folder, removed));
         }
     }
diff --git a/src/engine/app/conversation-monitor/app-fill-window-operation.vala 
b/src/engine/app/conversation-monitor/app-fill-window-operation.vala
index 456680c..5348984 100644
--- a/src/engine/app/conversation-monitor/app-fill-window-operation.vala
+++ b/src/engine/app/conversation-monitor/app-fill-window-operation.vala
@@ -49,7 +49,7 @@ private class Geary.App.FillWindowOperation : ConversationOperation {
         // Check to see if we need any more, but only if we actually
         // loaded some, so we don't keep loop loading when we have
         // already loaded all in the folder.
-        if (loaded > 0) {
+        if (loaded == num_to_load) {
             this.monitor.check_window_count();
         }
     }
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 2d16ff0..f1ad286 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -24,6 +24,7 @@ set(TEST_ENGINE_SRC
   engine/api/geary-attachment-test.vala
   engine/api/geary-engine-test.vala
   engine/app/app-conversation-test.vala
+  engine/app/app-conversation-monitor-test.vala
   engine/app/app-conversation-set-test.vala
   engine/imap/command/imap-create-command-test.vala
   engine/imap/response/imap-namespace-response-test.vala
diff --git a/test/engine/api/geary-account-mock.vala b/test/engine/api/geary-account-mock.vala
index 2ddd799..ebc2552 100644
--- a/test/engine/api/geary-account-mock.vala
+++ b/test/engine/api/geary-account-mock.vala
@@ -5,7 +5,7 @@
  * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
-public class Geary.MockAccount : Account {
+public class Geary.MockAccount : Account, MockObject {
 
 
     public class MockSearchQuery : SearchQuery {
@@ -31,100 +31,176 @@ public class Geary.MockAccount : Account {
     }
 
 
+    protected Gee.Queue<ExpectedCall> expected {
+        get; set; default = new Gee.LinkedList<ExpectedCall>();
+    }
+
+
     public MockAccount(string name, AccountInformation information) {
         base(name, information);
     }
 
     public override async void open_async(Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        void_call("open_async", { cancellable });
     }
 
     public override async void close_async(Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        void_call("close_async", { cancellable });
     }
 
     public override bool is_open() {
-        return false;
+        try {
+            return boolean_call("is_open", {}, false);
+        } catch (Error err) {
+            return false;
+        }
     }
 
     public override async void rebuild_async(Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        void_call("rebuild_async", { cancellable });
     }
 
     public override async void start_outgoing_client()
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        void_call("start_outgoing_client", {});
     }
 
     public override async void start_incoming_client()
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        void_call("start_incoming_client", {});
     }
 
-    public override Gee.Collection<Geary.Folder> list_matching_folders(Geary.FolderPath? parent)
+    public override Gee.Collection<Folder> list_matching_folders(FolderPath? parent)
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return object_call<Gee.Collection<Folder>>(
+            "get_containing_folders_async", {parent}, Gee.List.empty<Folder>()
+        );
     }
 
-    public override Gee.Collection<Geary.Folder> list_folders() throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override Gee.Collection<Folder> list_folders() throws Error {
+        return object_call<Gee.Collection<Folder>>(
+            "list_folders", {}, Gee.List.empty<Folder>()
+        );
     }
 
     public override Geary.ContactStore get_contact_store() {
         return new MockContactStore();
     }
 
-    public override async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null)
+    public override async bool folder_exists_async(FolderPath path,
+                                                   Cancellable? cancellable = null)
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return boolean_call("folder_exists_async", {path, cancellable}, false);
     }
 
-    public override async Geary.Folder fetch_folder_async(Geary.FolderPath path,
-        Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override async Folder fetch_folder_async(FolderPath path,
+                                                    Cancellable? cancellable = null)
+    throws Error {
+        return object_or_throw_call<Folder>(
+            "fetch_folder_async",
+            {path, cancellable},
+            new EngineError.NOT_FOUND("Mock call")
+        );
     }
 
-    public override async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
-        Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
-    }
-
-    public override async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = 
null)
+    public override Folder? get_special_folder(SpecialFolderType special)
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return object_call<Folder?>(
+            "get_special_folder", {box_arg(special)}, null
+        );
     }
 
-    public override async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_search_message_id_async(
-        Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok,
-        Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? flag_blacklist,
-        Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override async Folder get_required_special_folder_async(SpecialFolderType special,
+                                                                   Cancellable? cancellable = null)
+    throws Error {
+        return object_or_throw_call<Folder>(
+            "get_required_special_folder_async",
+            {box_arg(special), cancellable},
+            new EngineError.NOT_FOUND("Mock call")
+        );
     }
 
-    public override async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id,
-        Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override async void send_email_async(ComposedEmail composed,
+                                                Cancellable? cancellable = null)
+        throws Error {
+        void_call("send_email_async", {composed, cancellable});
     }
 
-    public override Geary.SearchQuery open_search(string query, Geary.SearchQuery.Strategy strategy) {
-        return new MockSearchQuery();
+    public override async Gee.MultiMap<Email,FolderPath?>?
+        local_search_message_id_async(RFC822.MessageID message_id,
+                                      Email.Field requested_fields,
+                                      bool partial_ok,
+                                      Gee.Collection<FolderPath?>? folder_blacklist,
+                                      EmailFlags? flag_blacklist,
+                                      Cancellable? cancellable = null)
+        throws Error {
+        return object_call<Gee.MultiMap<Email,FolderPath?>?>(
+            "local_search_message_id_async",
+            {
+                message_id,
+                box_arg(requested_fields),
+                box_arg(partial_ok),
+                folder_blacklist,
+                flag_blacklist,
+                cancellable
+            },
+            null
+        );
+    }
+
+    public override async Email local_fetch_email_async(EmailIdentifier email_id,
+                                                        Email.Field required_fields,
+                                                        Cancellable? cancellable = null)
+        throws Error {
+        return object_or_throw_call<Email>(
+            "local_fetch_email_async",
+            {email_id, box_arg(required_fields), cancellable},
+            new EngineError.NOT_FOUND("Mock call")
+        );
     }
 
-    public override async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
-        int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
-        Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws 
Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override SearchQuery open_search(string query, SearchQuery.Strategy strategy) {
+        return new MockSearchQuery();
     }
 
-    public override async Gee.Set<string>? get_search_matches_async(Geary.SearchQuery query,
-        Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+    public override async Gee.Collection<EmailIdentifier>?
+        local_search_async(SearchQuery query,
+                           int limit = 100,
+                           int offset = 0,
+                           Gee.Collection<FolderPath?>? folder_blacklist = null,
+                           Gee.Collection<EmailIdentifier>? search_ids = null,
+                           Cancellable? cancellable = null)
+        throws Error {
+        return object_call<Gee.Collection<EmailIdentifier>?>(
+            "local_search_async",
+            {
+                query,
+                box_arg(limit),
+                box_arg(offset),
+                folder_blacklist,
+                search_ids,
+                cancellable
+            },
+            null
+        );
+    }
+
+    public override async Gee.Set<string>?
+        get_search_matches_async(SearchQuery query,
+                                 Gee.Collection<EmailIdentifier> ids,
+                                 Cancellable? cancellable = null)
+        throws Error {
+        return object_call<Gee.Set<string>?>(
+            "get_search_matches_async", {query, ids, cancellable}, null
+        );
     }
 
     public override async Gee.MultiMap<EmailIdentifier, FolderPath>?
         get_containing_folders_async(Gee.Collection<EmailIdentifier> ids,
                                      Cancellable? cancellable) throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return object_call<Gee.MultiMap<EmailIdentifier, FolderPath>?>(
+            "get_containing_folders_async", {ids, cancellable}, null
+        );
     }
 
 }
diff --git a/test/engine/api/geary-email-identifier-mock.vala 
b/test/engine/api/geary-email-identifier-mock.vala
index adda6ed..83367ea 100644
--- a/test/engine/api/geary-email-identifier-mock.vala
+++ b/test/engine/api/geary-email-identifier-mock.vala
@@ -18,7 +18,7 @@ public class Geary.MockEmailIdentifer : EmailIdentifier {
 
     public override int natural_sort_comparator(Geary.EmailIdentifier other) {
         MockEmailIdentifer? other_mock = other as MockEmailIdentifer;
-        return (other_mock == null) ? -1 : other_mock.id - this.id;
+        return (other_mock == null) ? 1 : this.id - other_mock.id;
     }
 
 }
diff --git a/test/engine/api/geary-folder-mock.vala b/test/engine/api/geary-folder-mock.vala
index 55b8e5b..fee33d4 100644
--- a/test/engine/api/geary-folder-mock.vala
+++ b/test/engine/api/geary-folder-mock.vala
@@ -5,7 +5,8 @@
  * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
-public class Geary.MockFolder : Folder {
+public class Geary.MockFolder : Folder, MockObject {
+
 
     public override Account account {
         get { return this._account; }
@@ -27,6 +28,11 @@ public class Geary.MockFolder : Folder {
         get { return this._opening_monitor; }
     }
 
+    protected Gee.Queue<ExpectedCall> expected {
+        get; set; default = new Gee.LinkedList<ExpectedCall>();
+    }
+
+
     private Account _account;
     private FolderProperties _properties;
     private FolderPath _path;
@@ -52,8 +58,12 @@ public class Geary.MockFolder : Folder {
 
     public override async bool open_async(Folder.OpenFlags open_flags,
                                  Cancellable? cancellable = null)
-    throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        throws Error {
+        return boolean_call(
+            "open_async",
+            { int_arg(open_flags), cancellable },
+            false
+        );
     }
 
     public override async void wait_for_remote_async(Cancellable? cancellable = null)
@@ -63,7 +73,9 @@ public class Geary.MockFolder : Folder {
 
     public override async bool close_async(Cancellable? cancellable = null)
     throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return boolean_call(
+            "close_async", { cancellable }, false
+        );
     }
 
     public override async void wait_for_close_async(Cancellable? cancellable = null)
@@ -78,7 +90,11 @@ public class Geary.MockFolder : Folder {
                                Folder.ListFlags flags,
                                Cancellable? cancellable = null)
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return object_call<Gee.List<Email>?>(
+            "list_email_by_id_async",
+            {initial_id, int_arg(count), box_arg(required_fields), box_arg(flags), cancellable},
+            null
+        );
     }
 
     public override async Gee.List<Geary.Email>?
@@ -87,7 +103,11 @@ public class Geary.MockFolder : Folder {
                                       Folder.ListFlags flags,
                                       Cancellable? cancellable = null)
         throws Error {
-        throw new EngineError.UNSUPPORTED("Mock method");
+        return object_call<Gee.List<Email>?>(
+            "list_email_by_sparse_id_async",
+            {ids, box_arg(required_fields), box_arg(flags), cancellable},
+            null
+        );
     }
 
     public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>?
diff --git a/test/engine/app/app-conversation-monitor-test.vala 
b/test/engine/app/app-conversation-monitor-test.vala
new file mode 100644
index 0000000..52e4f9c
--- /dev/null
+++ b/test/engine/app/app-conversation-monitor-test.vala
@@ -0,0 +1,456 @@
+/*
+ * Copyright 2018 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+
+class Geary.App.ConversationMonitorTest : TestCase {
+
+
+    AccountInformation? account_info = null;
+    MockAccount? account = null;
+    MockFolder? base_folder = null;
+    MockFolder? other_folder = null;
+
+
+    public ConversationMonitorTest() {
+        base("Geary.App.ConversationMonitorTest");
+        add_test("start_stop_monitoring", start_stop_monitoring);
+        add_test("open_error", open_error);
+        add_test("load_single_message", load_single_message);
+        add_test("load_multiple_messages", load_multiple_messages);
+        add_test("load_related_message", load_related_message);
+        add_test("base_folder_message_appended", base_folder_message_appended);
+        add_test("base_folder_message_removed", base_folder_message_removed);
+        add_test("external_folder_message_appended", external_folder_message_appended);
+    }
+
+    public override void set_up() {
+        this.account_info = new AccountInformation(
+            "account_01",
+            File.new_for_path("/tmp"),
+            File.new_for_path("/tmp")
+        );
+        this.account = new MockAccount("test", this.account_info);
+        this.base_folder = new MockFolder(
+            this.account,
+            null,
+            new MockFolderRoot("base"),
+            SpecialFolderType.NONE,
+            null
+        );
+        this.other_folder = new MockFolder(
+            this.account,
+            null,
+            new MockFolderRoot("other"),
+            SpecialFolderType.NONE,
+            null
+        );
+    }
+
+    public void start_stop_monitoring() throws Error {
+        ConversationMonitor monitor = new ConversationMonitor(
+            this.base_folder, Folder.OpenFlags.NONE, Email.Field.NONE, 10
+        );
+        Cancellable test_cancellable = new Cancellable();
+
+        this.base_folder.expect_call(
+            "open_async",
+            { MockObject.int_arg(Folder.OpenFlags.NONE), test_cancellable }
+        );
+        this.base_folder.expect_call("list_email_by_id_async");
+        this.base_folder.expect_call("close_async");
+
+        monitor.start_monitoring_async.begin(
+            test_cancellable, (obj, res) => { async_complete(res); }
+        );
+        monitor.start_monitoring_async.end(async_result());
+
+        monitor.stop_monitoring_async.begin(
+            test_cancellable, (obj, res) => { async_complete(res); }
+        );
+        monitor.stop_monitoring_async.end(async_result());
+
+        this.base_folder.assert_expectations();
+    }
+
+    public void open_error() throws Error {
+        ConversationMonitor monitor = new ConversationMonitor(
+            this.base_folder, Folder.OpenFlags.NONE, Email.Field.NONE, 10
+        );
+
+        ExpectedCall open = this.base_folder
+            .expect_call("open_async")
+            .throws(new EngineError.SERVER_UNAVAILABLE("Mock error"));
+
+        monitor.start_monitoring_async.begin(
+            null, (obj, res) => { async_complete(res); }
+        );
+        try {
+            monitor.start_monitoring_async.end(async_result());
+            assert_not_reached();
+        } catch (Error err) {
+            assert_error(open.throw_error, err);
+        }
+
+        this.base_folder.assert_expectations();
+    }
+
+    public void load_single_message() throws Error {
+        Email e1 = setup_email(1);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.base_folder.path);
+
+        ConversationMonitor monitor = setup_monitor({e1}, paths);
+
+        assert_int(1, monitor.size, "Conversation count");
+        assert_non_null(monitor.window_lowest, "Lowest window id");
+        assert_equal(e1.id, monitor.window_lowest, "Lowest window id");
+
+        Conversation c1 = Geary.Collection.get_first(monitor.read_only_view);
+        assert_equal(e1, c1.get_email_by_id(e1.id), "Email not present in conversation");
+    }
+
+    public void load_multiple_messages() throws Error {
+        Email e1 = setup_email(1, null);
+        Email e2 = setup_email(2, null);
+        Email e3 = setup_email(3, null);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.base_folder.path);
+        paths.set(e2.id, this.base_folder.path);
+        paths.set(e3.id, this.base_folder.path);
+
+        ConversationMonitor monitor = setup_monitor({e3, e2, e1}, paths);
+
+        assert_int(3, monitor.size, "Conversation count");
+        assert_non_null(monitor.window_lowest, "Lowest window id");
+        assert_equal(e1.id, monitor.window_lowest, "Lowest window id");
+    }
+
+    public void load_related_message() throws Error {
+        Email e1 = setup_email(1);
+        Email e2 = setup_email(2, e1);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.other_folder.path);
+        paths.set(e2.id, this.base_folder.path);
+
+        Gee.MultiMap<Email,FolderPath> related_paths =
+            new Gee.HashMultiMap<Email,FolderPath>();
+        related_paths.set(e1, this.other_folder.path);
+        related_paths.set(e2, this.base_folder.path);
+
+        ConversationMonitor monitor = setup_monitor({e2}, paths, {related_paths});
+
+        assert_int(1, monitor.size, "Conversation count");
+        assert_non_null(monitor.window_lowest, "Lowest window id");
+        assert_equal(e2.id, monitor.window_lowest, "Lowest window id");
+
+        Conversation c1 = Geary.Collection.get_first(monitor.read_only_view);
+        assert_equal(e1, c1.get_email_by_id(e1.id), "Related email not present in conversation");
+        assert_equal(e2, c1.get_email_by_id(e2.id), "In folder not present in conversation");
+    }
+
+    public void base_folder_message_appended() throws Error {
+        Email e1 = setup_email(1);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.base_folder.path);
+
+        ConversationMonitor monitor = setup_monitor();
+        assert_int(0, monitor.size, "Initial conversation count");
+
+        this.base_folder.expect_call("list_email_by_sparse_id_async")
+            .returns_object(new Gee.ArrayList<Email>.wrap({e1}));
+
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("local_search_message_id_async");
+
+        this.account.expect_call("get_containing_folders_async")
+            .returns_object(paths);
+
+        this.base_folder.email_appended(new Gee.ArrayList<EmailIdentifier>.wrap({e1.id}));
+
+        wait_for_signal(monitor, "conversations-added");
+        this.base_folder.assert_expectations();
+        this.account.assert_expectations();
+
+        assert_int(1, monitor.size, "Conversation count");
+    }
+
+    public void base_folder_message_removed() throws Error {
+        Email e1 = setup_email(1);
+        Email e2 = setup_email(2, e1);
+        Email e3 = setup_email(3);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.other_folder.path);
+        paths.set(e2.id, this.base_folder.path);
+        paths.set(e3.id, this.base_folder.path);
+
+        Gee.MultiMap<Email,FolderPath> e2_related_paths =
+            new Gee.HashMultiMap<Email,FolderPath>();
+        e2_related_paths.set(e1, this.other_folder.path);
+        e2_related_paths.set(e2, this.base_folder.path);
+
+        ConversationMonitor monitor = setup_monitor(
+            {e3, e2}, paths, {null, e2_related_paths}
+        );
+        assert_int(2, monitor.size, "Initial conversation count");
+        print("monitor.window_lowest: %s", monitor.window_lowest.to_string());
+        assert_equal(e2.id, monitor.window_lowest, "Lowest window id");
+
+        // Removing a message will trigger another async load
+        this.base_folder.expect_call("list_email_by_id_async");
+        this.account.expect_call("get_containing_folders_async");
+        this.base_folder.expect_call("list_email_by_id_async");
+
+        this.base_folder.email_removed(new Gee.ArrayList<EmailIdentifier>.wrap({e2.id}));
+        wait_for_signal(monitor, "conversations-removed");
+        assert_int(1, monitor.size, "Conversation count");
+        assert_equal(e3.id, monitor.window_lowest, "Lowest window id");
+
+        this.base_folder.email_removed(new Gee.ArrayList<EmailIdentifier>.wrap({e3.id}));
+        wait_for_signal(monitor, "conversations-removed");
+        assert_int(0, monitor.size, "Conversation count");
+        assert_null(monitor.window_lowest, "Lowest window id");
+
+        // Close the monitor to cancel the final load so it does not
+        // error out during later tests
+        this.base_folder.expect_call("close_async");
+        monitor.stop_monitoring_async.begin(
+            null, (obj, res) => { async_complete(res); }
+        );
+        monitor.stop_monitoring_async.end(async_result());
+    }
+
+    public void external_folder_message_appended() throws Error {
+        Email e1 = setup_email(1);
+        Email e2 = setup_email(2, e1);
+        Email e3 = setup_email(3, e1);
+
+        Gee.MultiMap<EmailIdentifier,FolderPath> paths =
+            new Gee.HashMultiMap<EmailIdentifier,FolderPath>();
+        paths.set(e1.id, this.base_folder.path);
+        paths.set(e2.id, this.base_folder.path);
+        paths.set(e3.id, this.other_folder.path);
+
+        Gee.MultiMap<Email,FolderPath> related_paths =
+            new Gee.HashMultiMap<Email,FolderPath>();
+        related_paths.set(e1, this.base_folder.path);
+        related_paths.set(e3, this.other_folder.path);
+
+        ConversationMonitor monitor = setup_monitor({e1}, paths);
+        assert_int(1, monitor.size, "Initial conversation count");
+
+        this.other_folder.expect_call("open_async");
+        this.other_folder.expect_call("list_email_by_sparse_id_async")
+            .returns_object(new Gee.ArrayList<Email>.wrap({e3}));
+        this.other_folder.expect_call("list_email_by_sparse_id_async")
+            .returns_object(new Gee.ArrayList<Email>.wrap({e3}));
+        this.other_folder.expect_call("close_async");
+
+        // ExternalAppendOperation's blacklist check
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+
+        /////////////////////////////////////////////////////////
+        // First call to expand_conversations_async for e3's refs
+
+        // LocalSearchOperationAppendOperation's blacklist check
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+
+        // Search for e1's ref
+        this.account.expect_call("local_search_message_id_async")
+            .returns_object(related_paths);
+
+        // Search for e2's ref
+        this.account.expect_call("local_search_message_id_async");
+
+        //////////////////////////////////////////////////////////
+        // Second call to expand_conversations_async for e1's refs
+
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("get_special_folder");
+        this.account.expect_call("local_search_message_id_async");
+
+        // Finally, the call to process_email_complete_async
+
+        this.account.expect_call("get_containing_folders_async")
+            .returns_object(paths);
+
+        // Should not be added, since it's actually in the base folder
+        this.account.email_appended(
+            this.base_folder,
+            new Gee.ArrayList<EmailIdentifier>.wrap({e2.id})
+        );
+
+        // Should be added, since it's an external message
+        this.account.email_appended(
+            this.other_folder,
+            new Gee.ArrayList<EmailIdentifier>.wrap({e3.id})
+        );
+
+        wait_for_signal(monitor, "conversations-added");
+        this.base_folder.assert_expectations();
+        this.other_folder.assert_expectations();
+        this.account.assert_expectations();
+
+        assert_int(1, monitor.size, "Conversation count");
+
+        Conversation c1 = Geary.Collection.get_first(monitor.read_only_view);
+        assert_int(2, c1.get_count(), "Conversation message count");
+        assert_equal(e3, c1.get_email_by_id(e3.id),
+                     "Appended email not present in conversation");
+    }
+
+    private Email setup_email(int id, Email? references = null) {
+        Email email = new Email(new MockEmailIdentifer(id));
+        DateTime now = new DateTime.now_local();
+        Geary.RFC822.MessageID mid = new Geary.RFC822.MessageID(
+            "test%d@localhost".printf(id)
+        );
+
+        Geary.RFC822.MessageIDList refs_list = null;
+        if (references != null) {
+            refs_list = new Geary.RFC822.MessageIDList.single(
+                references.message_id
+            );
+        }
+        email.set_send_date(new Geary.RFC822.Date.from_date_time(now));
+        email.set_email_properties(new MockEmailProperties(now));
+        email.set_full_references(mid, null, refs_list);
+        return email;
+    }
+
+    private ConversationMonitor
+        setup_monitor(Email[] base_folder_email = {},
+                      Gee.MultiMap<EmailIdentifier,FolderPath>? paths = null,
+                      Gee.MultiMap<Email,FolderPath>[] related_paths = {})
+        throws Error {
+        ConversationMonitor monitor = new ConversationMonitor(
+            this.base_folder, Folder.OpenFlags.NONE, Email.Field.NONE, 10
+        );
+        Cancellable test_cancellable = new Cancellable();
+
+        /*
+         * The process for loading messages looks roughly like this:
+         * - load_by_id_async
+         *   - base_folder.list_email_by_id_async
+         *   - process_email_async
+         *     - gets all related messages from listing
+         *     - expand_conversations_async
+         *       - get_search_folder_blacklist (i.e. account.get_special_folder × 3)
+         *       - foreach related: account.local_search_message_id_async
+         *       - process_email_async
+         *         - process_email_complete_async
+         *           - get_containing_folders_async
+         */
+
+        this.base_folder.expect_call("open_async");
+        ExpectedCall list_call = this.base_folder
+            .expect_call("list_email_by_id_async")
+            .returns_object(new Gee.ArrayList<Email>.wrap(base_folder_email));
+
+        if (base_folder_email.length > 0) {
+            // expand_conversations_async calls
+            // Account:get_special_folder() in
+            // get_search_folder_blacklist, and the default
+            // implementation of that calls get_special_folder.
+            this.account.expect_call("get_special_folder");
+            this.account.expect_call("get_special_folder");
+            this.account.expect_call("get_special_folder");
+
+            Gee.List<RFC822.MessageID> base_email_ids =
+                new Gee.ArrayList<RFC822.MessageID>();
+            foreach (Email base_email in base_folder_email) {
+                base_email_ids.add(base_email.message_id);
+            }
+                
+            int base_i = 0;
+            bool has_related = (
+                base_folder_email.length == related_paths.length
+            );
+            bool found_related = false;
+            Gee.Set<RFC822.MessageID> seen_ids = new Gee.HashSet<RFC822.MessageID>();
+            foreach (Email base_email in base_folder_email) {
+                ExpectedCall call =
+                    this.account.expect_call("local_search_message_id_async");
+                seen_ids.add(base_email.message_id);
+                if (has_related && related_paths[base_i] != null) {
+                    call.returns_object(related_paths[base_i++]);
+                    found_related = true;
+                }
+
+                foreach (RFC822.MessageID ancestor in base_email.get_ancestors()) {
+                    if (!seen_ids.contains(ancestor) && !base_email_ids.contains(ancestor)) {
+                        this.account.expect_call("local_search_message_id_async");
+                        seen_ids.add(ancestor);
+                    }
+                }
+            }
+
+            // Second call to expand_conversations_async will be made
+            // if any related were loaded
+            if (found_related) {
+                this.account.expect_call("get_special_folder");
+                this.account.expect_call("get_special_folder");
+                this.account.expect_call("get_special_folder");
+
+                seen_ids.clear();
+                foreach (Gee.MultiMap<Email,FolderPath> related in related_paths) {
+                    if (related != null) {
+                        foreach (Email email in related.get_keys()) {
+                            if (!base_email_ids.contains(email.message_id)) {
+                                foreach (RFC822.MessageID ancestor in email.get_ancestors()) {
+                                    if (!seen_ids.contains(ancestor)) {
+                                        this.account.expect_call("local_search_message_id_async");
+                                        seen_ids.add(ancestor);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            ExpectedCall contains =
+            this.account.expect_call("get_containing_folders_async");
+            if (paths != null) {
+                contains.returns_object(paths);
+            }
+        }
+
+        monitor.start_monitoring_async.begin(
+            test_cancellable, (obj, res) => { async_complete(res); }
+        );
+        monitor.start_monitoring_async.end(async_result());
+
+        if (base_folder_email.length == 0) {
+            wait_for_call(list_call);
+        } else {
+            wait_for_signal(monitor, "conversations-added");
+        }
+
+        this.base_folder.assert_expectations();
+        this.account.assert_expectations();
+
+        return monitor;
+    }
+
+}
diff --git a/test/meson.build b/test/meson.build
index 7c44a8b..b844d0d 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -20,6 +20,7 @@ geary_test_engine_sources = [
   'engine/api/geary-attachment-test.vala',
   'engine/api/geary-engine-test.vala',
   'engine/app/app-conversation-test.vala',
+  'engine/app/app-conversation-monitor-test.vala',
   'engine/app/app-conversation-set-test.vala',
   'engine/imap/command/imap-create-command-test.vala',
   'engine/imap/response/imap-namespace-response-test.vala',
diff --git a/test/test-engine.vala b/test/test-engine.vala
index d5c06bc..ab4a0ef 100644
--- a/test/test-engine.vala
+++ b/test/test-engine.vala
@@ -28,6 +28,8 @@ int main(string[] args) {
     engine.add_suite(new Geary.TimeoutManagerTest().get_suite());
     engine.add_suite(new Geary.App.ConversationTest().get_suite());
     engine.add_suite(new Geary.App.ConversationSetTest().get_suite());
+    // Depends on ConversationTest and ConversationSetTest passing
+    engine.add_suite(new Geary.App.ConversationMonitorTest().get_suite());
     engine.add_suite(new Geary.HTML.UtilTest().get_suite());
     engine.add_suite(new Geary.Imap.DeserializerTest().get_suite());
     engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite());



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]