Merge branch 'wip/794174-conversation-monitor-max-cpu'. Fixes Bug 794174.

This commit is contained in:
Michael James Gratton 2018-04-07 10:04:20 +10:00
commit 2149f74c99
18 changed files with 1098 additions and 203 deletions

View file

@ -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.

View file

@ -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));
}
}

View file

@ -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();
}
}

View file

@ -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) {

View file

@ -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

View file

@ -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());

View file

@ -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;
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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

View file

@ -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
);
}
}

View file

@ -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;
}
}

View file

@ -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>?

View 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;
}
}

View file

@ -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
View 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)
);
}
}

View file

@ -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; }

View file

@ -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());