Merge branch 'wip/794174-conversation-monitor-max-cpu'. Fixes Bug 794174.
This commit is contained in:
commit
2149f74c99
18 changed files with 1098 additions and 203 deletions
|
|
@ -576,15 +576,6 @@ public abstract class Geary.Folder : BaseObject {
|
|||
* closed, even if it's not open.
|
||||
*/
|
||||
public abstract async void wait_for_close_async(Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* Find the lowest- and highest-ordered {@link EmailIdentifier}s in the
|
||||
* folder, among the given set of EmailIdentifiers that may or may not be
|
||||
* in the folder. If none of the given set are in the folder, return null.
|
||||
*/
|
||||
public abstract async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
|
||||
Cancellable? cancellable = null) throws Error;
|
||||
|
||||
/**
|
||||
* List a number of contiguous emails in the folder's vector.
|
||||
|
|
|
|||
|
|
@ -105,11 +105,31 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
get; private set; default = new ConversationSet();
|
||||
}
|
||||
|
||||
/** The oldest message from the base folder in the loaded window. */
|
||||
internal EmailIdentifier? window_lowest {
|
||||
owned get {
|
||||
return (this.window.is_empty) ? null : this.window.first();
|
||||
}
|
||||
}
|
||||
|
||||
private Geary.Email.Field required_fields;
|
||||
private Geary.Folder.OpenFlags open_flags;
|
||||
private ConversationOperationQueue queue = null;
|
||||
private Cancellable? operation_cancellable = null;
|
||||
|
||||
// Set of known, in-folder emails, explicitly loaded for the
|
||||
// monitor's window. This exists purely to support the
|
||||
// window_lowest property above, but we need to maintain a sorted
|
||||
// set of all known messages since if the last known email is
|
||||
// removed, we won't know what the next lowest is. Only email
|
||||
// listed by one of the load_by_*_id methods are added here. Other
|
||||
// in-folder messages pulled in for a conversation aren't added,
|
||||
// since they may not be within the load window.
|
||||
private Gee.SortedSet<EmailIdentifier> window =
|
||||
new Gee.TreeSet<EmailIdentifier>((a,b) => {
|
||||
return a.natural_sort_comparator(b);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Fired when a message load has started.
|
||||
|
|
@ -370,31 +390,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
return flags;
|
||||
}
|
||||
|
||||
/** Returns the lowest id of any seen email in the base folder. */
|
||||
internal async Geary.EmailIdentifier? get_lowest_email_id_async()
|
||||
throws Error {
|
||||
Geary.EmailIdentifier? earliest_id = null;
|
||||
if (!this.conversations.is_empty) {
|
||||
// XXX this is the only caller of
|
||||
// Folder.find_boundaries_async in the whole code base,
|
||||
// and the amount of DB work it's doing in the case of
|
||||
// MinimalFolder's implementation is
|
||||
// staggering. ConversationSet should be simply
|
||||
// maintaining an ordered list of message ids that exist
|
||||
// in the base folder and use that instead, which will be
|
||||
// faster and remove a whole lot of pointless code
|
||||
// overhead in Folder implementations.
|
||||
yield this.base_folder.find_boundaries_async(
|
||||
conversations.get_email_identifiers(),
|
||||
out earliest_id,
|
||||
null,
|
||||
this.operation_cancellable
|
||||
);
|
||||
}
|
||||
return earliest_id;
|
||||
}
|
||||
|
||||
/** Loads messages from the base folder by identifier range. */
|
||||
/** Loads messages from the base folder into the window. */
|
||||
internal async int load_by_id_async(EmailIdentifier? initial_id,
|
||||
int count,
|
||||
Folder.ListFlags flags = Folder.ListFlags.NONE)
|
||||
|
|
@ -407,17 +403,22 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
|
||||
int load_count = 0;
|
||||
try {
|
||||
Gee.Collection<Geary.Email>? email =
|
||||
Gee.Collection<Geary.Email>? messages =
|
||||
yield this.base_folder.list_email_by_id_async(
|
||||
initial_id, count, required_fields, flags,
|
||||
this.operation_cancellable
|
||||
);
|
||||
|
||||
if (email != null) {
|
||||
load_count = email.size;
|
||||
if (messages != null && !messages.is_empty) {
|
||||
load_count = messages.size;
|
||||
|
||||
foreach (Email email in messages) {
|
||||
this.window.add(email.id);
|
||||
}
|
||||
|
||||
yield process_email_async(messages, new ProcessJobContext(true));
|
||||
}
|
||||
|
||||
yield process_email_async(email, new ProcessJobContext(true));
|
||||
} catch (Error err) {
|
||||
notify_scan_completed();
|
||||
throw err;
|
||||
|
|
@ -426,7 +427,7 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
return load_count;
|
||||
}
|
||||
|
||||
/** Loads messages from the base folder by identifier. */
|
||||
/** Loads messages from the base folder into the window. */
|
||||
internal async void load_by_sparse_id(Gee.Collection<EmailIdentifier> ids,
|
||||
Folder.ListFlags flags = Folder.ListFlags.NONE)
|
||||
throws Error {
|
||||
|
|
@ -437,12 +438,18 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
}
|
||||
|
||||
try {
|
||||
yield process_email_async(
|
||||
Gee.Collection<Geary.Email>? messages =
|
||||
yield this.base_folder.list_email_by_sparse_id_async(
|
||||
ids, required_fields, flags, this.operation_cancellable
|
||||
),
|
||||
new ProcessJobContext(true)
|
||||
);
|
||||
);
|
||||
|
||||
if (messages != null && !messages.is_empty) {
|
||||
foreach (Email email in messages) {
|
||||
this.window.add(email.id);
|
||||
}
|
||||
|
||||
yield process_email_async(messages, new ProcessJobContext(true));
|
||||
}
|
||||
} catch (Error err) {
|
||||
notify_scan_completed();
|
||||
throw err;
|
||||
|
|
@ -521,6 +528,23 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
}
|
||||
}
|
||||
|
||||
/** Notifies of removed conversations and removes emails from the window. */
|
||||
internal void removed(Gee.Collection<Conversation> removed,
|
||||
Gee.MultiMap<Conversation, Email> trimmed,
|
||||
Gee.Collection<EmailIdentifier>? base_folder_removed) {
|
||||
foreach (Conversation conversation in trimmed.get_keys()) {
|
||||
notify_conversation_trimmed(conversation, trimmed.get(conversation));
|
||||
}
|
||||
|
||||
if (removed.size > 0) {
|
||||
notify_conversations_removed(removed);
|
||||
}
|
||||
|
||||
if (base_folder_removed != null) {
|
||||
this.window.remove_all(base_folder_removed);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check conversations to see if they still exist in the base folder.
|
||||
*
|
||||
|
|
@ -548,17 +572,6 @@ public class Geary.App.ConversationMonitor : BaseObject {
|
|||
return evaporated;
|
||||
}
|
||||
|
||||
internal void notify_emails_removed(Gee.Collection<Conversation> removed,
|
||||
Gee.MultiMap<Conversation, Email> trimmed) {
|
||||
foreach (Conversation conversation in trimmed.get_keys()) {
|
||||
notify_conversation_trimmed(conversation, trimmed.get(conversation));
|
||||
}
|
||||
|
||||
if (removed.size > 0) {
|
||||
notify_conversations_removed(removed);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void notify_scan_started() {
|
||||
scan_started();
|
||||
}
|
||||
|
|
@ -789,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));
|
||||
}
|
||||
}
|
||||
|
|
@ -800,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));
|
||||
}
|
||||
}
|
||||
|
|
@ -811,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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,16 +42,14 @@ private class Geary.App.FillWindowOperation : ConversationOperation {
|
|||
num_to_load, this.monitor.base_folder.to_string()
|
||||
);
|
||||
|
||||
EmailIdentifier? earliest_id =
|
||||
yield this.monitor.get_lowest_email_id_async();
|
||||
int loaded = yield this.monitor.load_by_id_async(
|
||||
earliest_id, num_to_load
|
||||
this.monitor.window_lowest, num_to_load
|
||||
);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,8 @@ private class Geary.App.InsertOperation : ConversationOperation {
|
|||
}
|
||||
|
||||
public override async void execute_async() throws Error {
|
||||
Geary.EmailIdentifier? lowest = yield this.monitor.get_lowest_email_id_async();
|
||||
Geary.EmailIdentifier? lowest = this.monitor.window_lowest;
|
||||
Gee.Collection<EmailIdentifier>? to_insert = null;
|
||||
|
||||
if (lowest != null) {
|
||||
to_insert = new Gee.LinkedList<EmailIdentifier>();
|
||||
foreach (EmailIdentifier inserted in this.inserted_ids) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,11 @@ private class Geary.App.RemoveOperation : ConversationOperation {
|
|||
}
|
||||
|
||||
// Fire signals, clean up
|
||||
this.monitor.notify_emails_removed(removed, trimmed);
|
||||
this.monitor.removed(
|
||||
removed,
|
||||
trimmed,
|
||||
(this.source_folder == this.monitor.base_folder) ? this.removed_ids : null
|
||||
);
|
||||
|
||||
// Check we still have enough conversations if any were
|
||||
// removed
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ private class Geary.App.ReseedOperation : ConversationOperation {
|
|||
}
|
||||
|
||||
public override async void execute_async() throws Error {
|
||||
EmailIdentifier? earliest_id =
|
||||
yield this.monitor.get_lowest_email_id_async();
|
||||
EmailIdentifier? earliest_id = this.monitor.window_lowest;
|
||||
if (earliest_id != null) {
|
||||
debug("Reseeding starting from Email ID %s on opened %s",
|
||||
earliest_id.to_string(), this.monitor.base_folder.to_string());
|
||||
|
|
|
|||
|
|
@ -282,34 +282,6 @@ private class Geary.SmtpOutboxFolder :
|
|||
yield remove_email_async(list, cancellable);
|
||||
}
|
||||
|
||||
public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
SmtpOutboxEmailIdentifier? outbox_low = null;
|
||||
SmtpOutboxEmailIdentifier? outbox_high = null;
|
||||
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
|
||||
foreach (Geary.EmailIdentifier id in ids) {
|
||||
SmtpOutboxEmailIdentifier? outbox_id = id as SmtpOutboxEmailIdentifier;
|
||||
if (outbox_id == null)
|
||||
continue;
|
||||
|
||||
OutboxRow? row = do_fetch_row_by_ordering(cx, outbox_id.ordering, cancellable);
|
||||
if (row == null)
|
||||
continue;
|
||||
|
||||
if (outbox_low == null || outbox_id.ordering < outbox_low.ordering)
|
||||
outbox_low = outbox_id;
|
||||
if (outbox_high == null || outbox_id.ordering > outbox_high.ordering)
|
||||
outbox_high = outbox_id;
|
||||
}
|
||||
|
||||
return Db.TransactionOutcome.DONE;
|
||||
}, cancellable);
|
||||
|
||||
low = outbox_low;
|
||||
high = outbox_high;
|
||||
}
|
||||
|
||||
public override Geary.Folder.OpenState get_open_state() {
|
||||
return is_open() ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,25 +48,7 @@ private class Geary.ImapDB.SearchFolder : Geary.SearchFolder, Geary.FolderSuppor
|
|||
exclude_folder(folder);
|
||||
}
|
||||
}
|
||||
|
||||
public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
low = null;
|
||||
high = null;
|
||||
|
||||
// This shouldn't require a result_mutex lock since there's no yield.
|
||||
Gee.TreeSet<ImapDB.SearchEmailIdentifier> in_folder = Geary.traverse<Geary.EmailIdentifier>(ids)
|
||||
.cast_object<ImapDB.SearchEmailIdentifier>()
|
||||
.filter(id => id in search_results)
|
||||
.to_tree_set();
|
||||
|
||||
if (in_folder.size > 0) {
|
||||
low = in_folder.first();
|
||||
high = in_folder.last();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
|
||||
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
|
||||
int result_mutex_token = yield result_mutex.claim_async();
|
||||
|
|
|
|||
|
|
@ -1053,31 +1053,6 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
|
|||
this.update_flags_timer.start();
|
||||
}
|
||||
|
||||
public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
low = null;
|
||||
high = null;
|
||||
|
||||
Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? map
|
||||
= yield account.get_containing_folders_async(ids, cancellable);
|
||||
|
||||
if (map != null) {
|
||||
Gee.ArrayList<Geary.EmailIdentifier> in_folder = new Gee.ArrayList<Geary.EmailIdentifier>();
|
||||
foreach (Geary.EmailIdentifier id in map.get_keys()) {
|
||||
if (path in map.get(id))
|
||||
in_folder.add(id);
|
||||
}
|
||||
|
||||
if (in_folder.size > 0) {
|
||||
Gee.SortedSet<Geary.EmailIdentifier> sorted = Geary.EmailIdentifier.sort(in_folder);
|
||||
|
||||
low = sorted.first();
|
||||
high = sorted.last();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void on_email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids) {
|
||||
notify_email_locally_complete(email_ids);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
# Copyright 2016 Michael Gratton <mike@vee.net>
|
||||
|
||||
set(TEST_LIB_SRC
|
||||
mock-object.vala
|
||||
test-case.vala
|
||||
)
|
||||
|
||||
|
|
@ -23,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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
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 SearchQuery open_search(string query, SearchQuery.Strategy strategy) {
|
||||
return new MockSearchQuery();
|
||||
}
|
||||
|
||||
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 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(Geary.SearchQuery query,
|
||||
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
|
||||
throw new EngineError.UNSUPPORTED("Mock method");
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -71,16 +83,6 @@ public class Geary.MockFolder : Folder {
|
|||
throw new EngineError.UNSUPPORTED("Mock method");
|
||||
}
|
||||
|
||||
public override async void
|
||||
find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
out Geary.EmailIdentifier? low,
|
||||
out Geary.EmailIdentifier? high,
|
||||
Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
throw new EngineError.UNSUPPORTED("Mock method");
|
||||
}
|
||||
|
||||
|
||||
public override async Gee.List<Geary.Email>?
|
||||
list_email_by_id_async(Geary.EmailIdentifier? initial_id,
|
||||
int count,
|
||||
|
|
@ -88,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>?
|
||||
|
|
@ -97,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>?
|
||||
|
|
|
|||
456
test/engine/app/app-conversation-monitor-test.vala
Normal file
456
test/engine/app/app-conversation-monitor-test.vala
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
geary_test_lib_sources = [
|
||||
'mock-object.vala',
|
||||
'test-case.vala',
|
||||
]
|
||||
|
||||
|
|
@ -19,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',
|
||||
|
|
|
|||
243
test/mock-object.vala
Normal file
243
test/mock-object.vala
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
private interface Argument {
|
||||
|
||||
public abstract void assert(Object object) throws Error;
|
||||
|
||||
}
|
||||
|
||||
private class BoxArgument<T> : Object, Argument {
|
||||
|
||||
private T value;
|
||||
|
||||
internal BoxArgument(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public new void assert(Object object) throws Error {
|
||||
assert_true(
|
||||
object is BoxArgument,
|
||||
"Expected %s value".printf(this.get_type().name())
|
||||
);
|
||||
assert_true(this.value == ((BoxArgument<T>) object).value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class IntArgument : Object, Argument {
|
||||
|
||||
private int value;
|
||||
|
||||
internal IntArgument(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public new void assert(Object object) throws Error {
|
||||
assert_true(object is IntArgument, "Expected int value");
|
||||
assert_int(this.value, ((IntArgument) object).value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an expected method call on a mock object.
|
||||
*
|
||||
* An instance of this object is returned when calling {@link
|
||||
* Mock.Object.expect_call}, and may be used to further specify
|
||||
* expectations, such that the mock method should throw a specific
|
||||
* error or return a specific value or object.
|
||||
*/
|
||||
public class ExpectedCall : Object {
|
||||
|
||||
|
||||
public string name { get; private set; }
|
||||
internal Object[]? args;
|
||||
public Error? throw_error { get; private set; default = null; }
|
||||
public Object? return_object { get; private set; default = null; }
|
||||
public Variant? return_value { get; private set; default = null; }
|
||||
public bool was_called { get; private set; default = false; }
|
||||
|
||||
|
||||
internal ExpectedCall(string name, Object[]? args) {
|
||||
this.name = name;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public ExpectedCall returns_object(Object value) {
|
||||
this.return_object = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedCall returns_boolean(bool value) {
|
||||
this.return_value = new GLib.Variant.boolean(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExpectedCall @throws(Error err) {
|
||||
this.throw_error = err;
|
||||
return this;
|
||||
}
|
||||
|
||||
internal void called() {
|
||||
this.was_called = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Denotes a class that is injected into code being tested.
|
||||
*
|
||||
* Mock objects are unit testing fixtures that are used to provide
|
||||
* instances of specific classes or interfaces which are required by
|
||||
* the code being tested. For example, if an object being tested
|
||||
* requires certain objects to be passed in via its constructor or as
|
||||
* arguments of method calls and uses these to implement its
|
||||
* behaviour, mock objects that fulfil these requirements can be used.
|
||||
*
|
||||
* Mock objects provide a means of both ensuring code being tested
|
||||
* makes expected method calls with expected arguments on its
|
||||
* dependencies, and a means of orchestrating the return value and
|
||||
* exceptions raised when these methods are called, if any.
|
||||
*
|
||||
* To specify a specific method should be called on a mock object,
|
||||
* call {@link expect_call} with the name of the method and optionally
|
||||
* the arguments that are expected. The returned {@link ExpectedCall}
|
||||
* object can be used to specify any exception or return values for
|
||||
* the method. After executing the code being tested, call {@link
|
||||
* assert_expectations} to ensure that the actual calls made matched
|
||||
* those expected.
|
||||
*/
|
||||
public interface MockObject {
|
||||
|
||||
|
||||
public static Object box_arg<T>(T value) {
|
||||
return new BoxArgument<T>(value);
|
||||
}
|
||||
|
||||
public static Object int_arg(int value) {
|
||||
return new IntArgument(value);
|
||||
}
|
||||
|
||||
protected abstract Gee.Queue<ExpectedCall> expected { get; set; }
|
||||
|
||||
|
||||
public ExpectedCall expect_call(string name, Object[]? args = null) {
|
||||
ExpectedCall expected = new ExpectedCall(name, args);
|
||||
this.expected.offer(expected);
|
||||
return expected;
|
||||
}
|
||||
|
||||
public void assert_expectations() throws Error {
|
||||
assert_true(this.expected.is_empty,
|
||||
"%d expected calls not made".printf(this.expected.size));
|
||||
reset_expectations();
|
||||
}
|
||||
|
||||
public void reset_expectations() {
|
||||
this.expected.clear();
|
||||
}
|
||||
|
||||
protected bool boolean_call(string name, Object[] args, bool default_return)
|
||||
throws Error {
|
||||
ExpectedCall? expected = call_made(name, args);
|
||||
|
||||
bool return_value = default_return;
|
||||
if (expected.return_value != null) {
|
||||
return_value = expected.return_value.get_boolean();
|
||||
}
|
||||
return return_value;
|
||||
}
|
||||
|
||||
protected R object_call<R>(string name, Object[] args, R default_return)
|
||||
throws Error {
|
||||
ExpectedCall? expected = call_made(name, args);
|
||||
|
||||
R? return_object = default_return;
|
||||
if (expected.return_object != null) {
|
||||
return_object = (R) expected.return_object;
|
||||
}
|
||||
return return_object;
|
||||
}
|
||||
|
||||
protected R object_or_throw_call<R>(string name, Object[] args, Error default_error)
|
||||
throws Error {
|
||||
ExpectedCall? expected = call_made(name, args);
|
||||
|
||||
if (expected.return_object != null) {
|
||||
throw default_error;
|
||||
}
|
||||
return expected.return_object;
|
||||
}
|
||||
|
||||
protected void void_call(string name, Object[] args) throws Error {
|
||||
call_made(name, args);
|
||||
}
|
||||
|
||||
private ExpectedCall? call_made(string name, Object[] args) throws Error {
|
||||
assert_false(this.expected.is_empty, "Unexpected call: %s".printf(name));
|
||||
|
||||
ExpectedCall expected = this.expected.poll();
|
||||
assert_string(expected.name, name, "Unexpected call");
|
||||
if (expected.args != null) {
|
||||
assert_args(expected.args, args, "Call %s".printf(name));
|
||||
}
|
||||
|
||||
expected.called();
|
||||
|
||||
if (expected.throw_error != null) {
|
||||
throw expected.throw_error;
|
||||
}
|
||||
|
||||
return expected;
|
||||
}
|
||||
|
||||
private void assert_args(Object[]? expected_args, Object[]? actual_args, string context)
|
||||
throws Error {
|
||||
int args = 0;
|
||||
foreach (Object expected in expected_args) {
|
||||
if (args >= actual_args.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
Object actual = actual_args[args];
|
||||
string arg_context = "%s, argument #%d".printf(context, args++);
|
||||
|
||||
if (expected is Argument) {
|
||||
((Argument) expected).assert(actual);
|
||||
} else if (expected != null) {
|
||||
assert_true(
|
||||
actual != null,
|
||||
"%s: Expected %s, actual is null".printf(
|
||||
arg_context, expected.get_type().name()
|
||||
)
|
||||
);
|
||||
assert_true(
|
||||
expected.get_type() == actual.get_type(),
|
||||
"%s: Expected %s, actual: %s".printf(
|
||||
arg_context,
|
||||
expected.get_type().name(),
|
||||
actual.get_type().name()
|
||||
)
|
||||
);
|
||||
assert_equal(
|
||||
expected, actual,
|
||||
"%s: object value".printf(arg_context)
|
||||
);
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
assert_int(
|
||||
expected_args.length, actual_args.length,
|
||||
"%s: argument list length".printf(context)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Copyright (C) 2009 Julien Peeters
|
||||
* Copyright (C) 2017 Michael Gratton
|
||||
* Copyright (C) 2017-2018 Michael Gratton <mike@vee.net>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
|
|
@ -17,12 +17,142 @@
|
|||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* Author:
|
||||
* Julien Peeters <contact@julienpeeters.fr>
|
||||
* Michael Gratton <mike@vee.net>
|
||||
* Julien Peeters <contact@julienpeeters.fr>
|
||||
* Michael Gratton <mike@vee.net>
|
||||
*/
|
||||
|
||||
|
||||
public void assert_null(Object? actual, string? context = null)
|
||||
throws Error {
|
||||
if (actual != null) {
|
||||
print_assert(context ?? "Object is non-null", null);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_non_null(Object? actual, string? context = null)
|
||||
throws Error {
|
||||
if (actual == null) {
|
||||
print_assert(context ?? "Object is null", null);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_equal(Object expected, Object? actual, string? context = null)
|
||||
throws Error {
|
||||
if (expected != actual) {
|
||||
print_assert(context ?? "Objects are not equal", null);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_string(string expected, string? actual, string? context = null)
|
||||
throws Error {
|
||||
if (expected != actual) {
|
||||
string a = expected;
|
||||
if (a.length > 20) {
|
||||
a = a[0:15] + "…";
|
||||
}
|
||||
string b = actual;
|
||||
if (b.length > 20) {
|
||||
b = b[0:15] + "…";
|
||||
}
|
||||
print_assert("Expected: \"%s\", was: \"%s\"".printf(a, b), context);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_int(int expected, int actual, string? context = null)
|
||||
throws Error {
|
||||
if (expected != actual) {
|
||||
print_assert("Expected: %d, was: %d".printf(expected, actual), context);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_true(bool condition, string? context = null)
|
||||
throws Error {
|
||||
if (!condition) {
|
||||
print_assert(context ?? "Expected true", null);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_false(bool condition, string? context = null)
|
||||
throws Error {
|
||||
if (condition) {
|
||||
print_assert(context ?? "Expected false", null);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
public void assert_error(Error expected, Error? actual, string? context = null) {
|
||||
bool failed = false;
|
||||
if (actual == null) {
|
||||
print_assert(
|
||||
"Expected error: %s %i, was null".printf(
|
||||
expected.domain.to_string(), expected.code
|
||||
),
|
||||
context
|
||||
);
|
||||
failed = true;
|
||||
} else if (expected.domain != actual.domain ||
|
||||
expected.code != actual.code) {
|
||||
print_assert(
|
||||
"Expected error: %s %i, was actually %s %i: %s".printf(
|
||||
expected.domain.to_string(),
|
||||
expected.code,
|
||||
actual.domain.to_string(),
|
||||
actual.code,
|
||||
actual.message
|
||||
),
|
||||
context
|
||||
);
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
// XXX this shadows GLib.assert_no_error since that doesn't work
|
||||
public void assert_no_error(Error? err, string? context = null) {
|
||||
if (err != null) {
|
||||
print_assert(
|
||||
"Unexpected error: %s %i: %s".printf(
|
||||
err.domain.to_string(),
|
||||
err.code,
|
||||
err.message
|
||||
),
|
||||
context
|
||||
);
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
private inline void print_assert(string message, string? context) {
|
||||
string output = message;
|
||||
if (context != null) {
|
||||
output = "%s: %s".printf(context, output);
|
||||
}
|
||||
GLib.stderr.puts(output);
|
||||
GLib.stderr.putc('\n');
|
||||
}
|
||||
|
||||
public abstract class TestCase : Object {
|
||||
|
||||
|
||||
private class SignalWaiter : Object {
|
||||
|
||||
public bool was_fired = false;
|
||||
|
||||
public void @callback(Object source) {
|
||||
was_fired = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected MainContext main_loop = MainContext.default();
|
||||
|
||||
private GLib.TestSuite suite;
|
||||
|
|
@ -75,6 +205,51 @@ public abstract class TestCase : Object {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a mock object's call to be completed.
|
||||
*
|
||||
* This method busy waits on the test's main loop until either
|
||||
* until {@link ExpectedCall.was_called} is true, or until the
|
||||
* given timeout in seconds has occurred.
|
||||
*
|
||||
* Returns //true// if the call was made, or //false// if the
|
||||
* timeout was reached.
|
||||
*/
|
||||
protected bool wait_for_call(ExpectedCall call, double timeout = 1.0) {
|
||||
GLib.Timer timer = new GLib.Timer();
|
||||
timer.start();
|
||||
while (!call.was_called && timer.elapsed() < timeout) {
|
||||
this.main_loop.iteration(false);
|
||||
}
|
||||
return call.was_called;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an object's signal to be fired.
|
||||
*
|
||||
* This method busy waits on the test's main loop until either
|
||||
* until the object emits the named signal, or until the given
|
||||
* timeout in seconds has occurred.
|
||||
*
|
||||
* Returns //true// if the signal was fired, or //false// if the
|
||||
* timeout was reached.
|
||||
*/
|
||||
protected bool wait_for_signal(Object source, string name, double timeout = 0.5) {
|
||||
SignalWaiter handler = new SignalWaiter();
|
||||
ulong id = GLib.Signal.connect_swapped(
|
||||
source, name, (GLib.Callback) handler.callback, handler
|
||||
);
|
||||
|
||||
GLib.Timer timer = new GLib.Timer();
|
||||
timer.start();
|
||||
while (!handler.was_fired && timer.elapsed() < timeout) {
|
||||
this.main_loop.iteration(false);
|
||||
}
|
||||
|
||||
source.disconnect(id);
|
||||
return handler.was_fired;
|
||||
}
|
||||
|
||||
private class Adaptor {
|
||||
|
||||
public string name { get; private set; }
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue