diff --git a/src/FilenamesIterator.zig b/src/FilenamesIterator.zig index 506b111..812cedf 100644 --- a/src/FilenamesIterator.zig +++ b/src/FilenamesIterator.zig @@ -27,6 +27,24 @@ pub fn next(self: *FilenamesIterator) ?[:0]const u8 { return std.mem.span(c.notmuch_filenames_get(filenames) orelse unreachable); } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; + defer message.deinit(); + + // The corpus message is backed by a single file; after exhaustion, + // next keeps returning null. + var filenames = try message.getFilenames(); + defer filenames.deinit(); + const filename = filenames.next() orelse return error.TestUnexpectedResult; + try std.testing.expect(std.mem.endsWith(u8, filename, "one.eml")); + try std.testing.expect(filenames.next() == null); +} + /// Deinitialize a `FilenamesIterator` object. /// /// It's not strictly necessary to call this function. All memory from the diff --git a/src/MessagesIterator.zig b/src/MessagesIterator.zig index 02ce105..955be9b 100644 --- a/src/MessagesIterator.zig +++ b/src/MessagesIterator.zig @@ -10,6 +10,8 @@ //! sooner. const MessagesIterator = @This(); +const std = @import("std"); + const c = @import("c"); const Message = @import("Message.zig"); @@ -47,6 +49,40 @@ pub fn collectTags(self: *const MessagesIterator) ?TagsIterator { }; } +test collectTags { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + const db = test_db.database; + + const one = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; + defer one.deinit(); + try one.addTag("red"); + const two = try db.findMessage("two@example.org") orelse return error.TestUnexpectedResult; + defer two.deinit(); + try two.addTag("blue"); + + const query = try db.queryCreate("subject:lunch"); + defer query.deinit(); + var messages = try query.searchMessages(); + defer messages.deinit(); + + // The tags of all remaining messages, gathered in one pass. This + // exhausts the message iterator, per the warning above. + var collected = messages.collectTags() orelse return error.TestUnexpectedResult; + defer collected.deinit(); + var saw_red = false; + var saw_blue = false; + while (collected.next()) |tag| { + if (std.mem.eql(u8, tag, "red")) saw_red = true; + if (std.mem.eql(u8, tag, "blue")) saw_blue = true; + } + try std.testing.expect(saw_red); + try std.testing.expect(saw_blue); + try std.testing.expect(try messages.next() == null); +} + pub fn next(self: *const MessagesIterator) NextError!?Message { const messages = self.messages orelse return null; return switch (status(c.notmuch_messages_status(messages))) { @@ -64,6 +100,24 @@ pub fn next(self: *const MessagesIterator) NextError!?Message { }; } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const query = try test_db.database.queryCreate("*"); + defer query.deinit(); + var messages = try query.searchMessages(); + defer messages.deinit(); + + // Iterate all matches; after exhaustion, next keeps returning null. + var count: usize = 0; + while (try messages.next()) |message| : (count += 1) message.deinit(); + try std.testing.expectEqual(fixture.corpus.len, count); + try std.testing.expect(try messages.next() == null); +} + /// Deinitialize a `MessagesIterator` object. /// /// It's not strictly necessary to call this function. All memory from diff --git a/src/PairsIterator.zig b/src/PairsIterator.zig index 0738454..d99f063 100644 --- a/src/PairsIterator.zig +++ b/src/PairsIterator.zig @@ -34,6 +34,29 @@ pub fn next(self: *PairsIterator) ?Pair { }; } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + const db = test_db.database; + + try db.configSet(.user_name, "Test User"); + + // Each pair carries the full dotted key and its value; a key with no + // value yields a null value. + var pairs = db.configGetPairs("user.") orelse return error.TestUnexpectedResult; + defer pairs.deinit(); + var saw_name = false; + while (pairs.next()) |pair| { + if (std.mem.eql(u8, pair.key, "user.name")) { + saw_name = true; + try std.testing.expectEqualStrings("Test User", pair.value orelse ""); + } + } + try std.testing.expect(saw_name); +} + /// Deinitialize a config pairs iterator, along with any associated resources. pub fn deinit(self: *PairsIterator) void { c.notmuch_config_pairs_destroy(self.pairs); diff --git a/src/PropertiesIterator.zig b/src/PropertiesIterator.zig index c4ce17f..f7da5c6 100644 --- a/src/PropertiesIterator.zig +++ b/src/PropertiesIterator.zig @@ -29,6 +29,28 @@ pub fn next(self: PropertiesIterator) ?KV { }; } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; + defer message.deinit(); + try message.addProperty("zig.key", "first"); + try message.addProperty("zig.key", "second"); + + // Each entry is a key/value pair; multiple values for one key yield + // one entry per value. After exhaustion, next keeps returning null. + var properties = message.getProperties("zig.key", true); + defer properties.deinit(); + var count: usize = 0; + while (properties.next()) |kv| : (count += 1) + try std.testing.expectEqualStrings("zig.key", kv.key); + try std.testing.expectEqual(@as(usize, 2), count); + try std.testing.expect(properties.next() == null); +} + pub fn deinit(self: PropertiesIterator) void { const properties = self.properties orelse return; c.notmuch_message_properties_destroy(properties); diff --git a/src/TagsIterator.zig b/src/TagsIterator.zig index f70390d..0198f20 100644 --- a/src/TagsIterator.zig +++ b/src/TagsIterator.zig @@ -24,6 +24,26 @@ pub fn next(self: *TagsIterator) ?[:0]const u8 { return std.mem.span(c.notmuch_tags_get(tags) orelse unreachable); } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const message = try test_db.database.findMessage("one@example.org") orelse return error.TestUnexpectedResult; + defer message.deinit(); + try message.addTag("beta"); + try message.addTag("alpha"); + + // Tags are iterated in sorted order, regardless of insertion order; + // after exhaustion, next keeps returning null. + var tags = message.getTags(); + defer tags.deinit(); + try std.testing.expectEqualStrings("alpha", tags.next() orelse return error.TestUnexpectedResult); + try std.testing.expectEqualStrings("beta", tags.next() orelse return error.TestUnexpectedResult); + try std.testing.expect(tags.next() == null); +} + pub fn deinit(self: *TagsIterator) void { c.notmuch_tags_destroy(self.tags); } diff --git a/src/ThreadsIterator.zig b/src/ThreadsIterator.zig index 0e28afb..011efc9 100644 --- a/src/ThreadsIterator.zig +++ b/src/ThreadsIterator.zig @@ -5,6 +5,8 @@ //! lists of threads. pub const ThreadsIterator = @This(); +const std = @import("std"); + const c = @import("c"); const Thread = @import("Thread.zig"); @@ -41,6 +43,25 @@ pub fn next(self: *ThreadsIterator) NextError!?Thread { }; } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + + const query = try test_db.database.queryCreate("*"); + defer query.deinit(); + var threads = try query.searchThreads(); + defer threads.deinit(); + + // The corpus's four messages form three threads; after exhaustion, + // next keeps returning null. + var count: usize = 0; + while (try threads.next()) |thread| : (count += 1) thread.deinit(); + try std.testing.expectEqual(@as(usize, 3), count); + try std.testing.expect(try threads.next() == null); +} + /// Deinitialize a `ThreadsIterator` object. /// /// It's not strictly necessary to call this function. All memory from diff --git a/src/ValuesIterator.zig b/src/ValuesIterator.zig index c26ae3e..a0d0ddd 100644 --- a/src/ValuesIterator.zig +++ b/src/ValuesIterator.zig @@ -25,11 +25,47 @@ pub fn next(self: *ValuesIterator) ?[:0]const u8 { return std.mem.span(c.notmuch_config_values_get(self.values) orelse unreachable); } +test next { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + const db = test_db.database; + + try db.configSet(.exclude_tags, "spam;deleted"); + + // Values are yielded in configuration order; after exhaustion, next + // keeps returning null. + var values = db.configGetValues(.exclude_tags) orelse return error.TestUnexpectedResult; + defer values.deinit(); + try std.testing.expectEqualStrings("spam", values.next() orelse return error.TestUnexpectedResult); + try std.testing.expectEqualStrings("deleted", values.next() orelse return error.TestUnexpectedResult); + try std.testing.expect(values.next() == null); +} + /// Reset the `ValuesIterator` to the first element. pub fn start(self: *ValuesIterator) void { c.notmuch_config_values_start(self.values); } +test start { + const fixture = @import("testing.zig"); + + var test_db = try fixture.corpusDatabase(); + defer test_db.deinit(); + const db = test_db.database; + + try db.configSet(.exclude_tags, "spam;deleted"); + + var values = db.configGetValues(.exclude_tags) orelse return error.TestUnexpectedResult; + defer values.deinit(); + while (values.next()) |_| {} + + // start rewinds the exhausted iterator to the first value. + values.start(); + try std.testing.expectEqualStrings("spam", values.next() orelse return error.TestUnexpectedResult); +} + /// Deinitialize a config values iterator, along with any associated /// resources. pub fn deinit(self: *ValuesIterator) void { diff --git a/src/tests.zig b/src/tests.zig index 7f803fe..f33df89 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -205,37 +205,6 @@ test "Message maildir flags" { try message.maildirFlagsToTags(); } -test "MessagesIterator tag collection" { - var test_db = try fixture.corpusDatabase(); - defer test_db.deinit(); - const db = test_db.database; - - const one = try db.findMessage("one@example.org") orelse return error.TestUnexpectedResult; - defer one.deinit(); - try one.addTag("red"); - const two = try db.findMessage("two@example.org") orelse return error.TestUnexpectedResult; - defer two.deinit(); - try two.addTag("blue"); - - const query = try db.queryCreate("subject:lunch"); - defer query.deinit(); - - var messages = try query.searchMessages(); - defer messages.deinit(); - // collectTags gathers the tags of all remaining messages in one pass - // (and exhausts the iterator, per its warning). - var collected = messages.collectTags() orelse return error.TestUnexpectedResult; - defer collected.deinit(); - var saw_red = false; - var saw_blue = false; - while (collected.next()) |tag| { - if (std.mem.eql(u8, tag, "red")) saw_red = true; - if (std.mem.eql(u8, tag, "blue")) saw_blue = true; - } - try std.testing.expect(saw_red); - try std.testing.expect(saw_blue); -} - test "Database.open error reporting" { const missing = notmuch.Database.open(.read_only, .{ .database_path = "/nonexistent/notmuch-zig-test",