From 272d5ddf2fc9f8a5d201d4d07ce641793dec4df1 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 29 Aug 2026 19:26:59 -0500 Subject: [PATCH] add compile-check test forcing analysis of all wrapper bodies Zig never analyzes the body of a function that is never called, so refAllDecls alone lets broken wrappers pass the test suite. The new test gives every public wrapper a real call site behind a never-taken branch. It immediately caught three more latent bugs: getMtime wrapped a time_t return value as a status code, Query.getDatabase assigned null to a non-optional field instead of returning null, and countMessages matched a nonexistent .xapian_error status. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017i7R7ReKJAHGQpitxGXqrb --- src/Directory.zig | 4 +- src/Query.zig | 4 +- src/compile_check.zig | 156 ++++++++++++++++++++++++++++++++++++++++++ src/notmuch.zig | 1 + 4 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 src/compile_check.zig diff --git a/src/Directory.zig b/src/Directory.zig index 7c91359..1eb987e 100644 --- a/src/Directory.zig +++ b/src/Directory.zig @@ -43,8 +43,8 @@ pub fn setMtime(self: *const Directory, mtime: i64) Error!void { /// Get the mtime of a directory, (as previously stored with `setMtime`). /// /// Returns `null` if no mtime has previously been stored for this directory. -pub fn getMtime(self: *const Directory) Error!?i64 { - const mtime = try wrap(c.notmuch_directory_get_mtime(self.directory)); +pub fn getMtime(self: *const Directory) ?i64 { + const mtime = c.notmuch_directory_get_mtime(self.directory); if (mtime == 0) return null; return @intCast(mtime); } diff --git a/src/Query.zig b/src/Query.zig index e3fac6c..6e6270b 100644 --- a/src/Query.zig +++ b/src/Query.zig @@ -31,7 +31,7 @@ pub fn getQueryString(self: *const Query) [:0]const u8 { /// Return the notmuch database of this query. See `Database.queryCreate`. pub fn getDatabase(self: *const Query) ?Database { return .{ - .database = c.notmuch_query_get_database(self.query) orelse null, + .database = c.notmuch_query_get_database(self.query) orelse return null, }; } @@ -169,7 +169,7 @@ pub fn countMessages(self: *const Query) CountMessagesError!u32 { var count: c_uint = undefined; return switch (status(c.notmuch_query_count_messages(self.query, &count))) { .success => @intCast(count), - .xapian_error => error.XapianError, + .xapian_exception => error.XapianError, else => unreachable, }; } diff --git a/src/compile_check.zig b/src/compile_check.zig new file mode 100644 index 0000000..5706203 --- /dev/null +++ b/src/compile_check.zig @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: © 2024 Jeffrey C. Ollie +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Forces semantic analysis of every public wrapper function body. +//! +//! `refAllDecls` only references declarations; Zig never analyzes the body of +//! a function that is never called, so a wrapper that calls the wrong C +//! function — or would not compile at all — can slip through the test suite. +//! The never-taken branch below gives each wrapper a real call site without +//! executing any of them. + +const notmuch = @import("notmuch.zig"); + +test "every wrapper body compiles" { + var never = false; + _ = &never; + if (!never) return; + + try notmuch.compact("path", "backup", null, null); + _ = notmuch.builtWith("compact"); + + const db: notmuch.Database = undefined; + _ = notmuch.Database.open(.read_only, .{}); + _ = notmuch.Database.create(.{}); + try db.close(); + try db.compact("backup", null, null); + try db.deinit(); + _ = db.getPath(); + _ = try db.getVersion(); + _ = db.needsUpgrade(); + try db.upgrade(null, null); + try db.beginAtomic(); + try db.endAtomic(); + _ = db.getRevision(); + _ = try db.getDirectory("path"); + try db.indexFile("file", null); + _ = try db.indexFileGetMessage("file", null); + try db.removeMessage("file"); + _ = try db.findMessage("id"); + _ = try db.findMessageByFilename("file"); + _ = db.getAllTags(); + var db_mut: notmuch.Database = undefined; + try db_mut.reopen(.read_only); + _ = try db.queryCreate("tag:inbox"); + _ = try db.queryCreateWithSyntax("tag:inbox", .xapian); + _ = try db.configGet(.user_name); + try db.configSet(.user_name, "value"); + _ = db.configGetValues(.new_tags); + _ = db.configGetValuesString("key"); + _ = db.configGetPairs("prefix"); + _ = try db.configGetBool(.autocommit); + _ = db.configPath(); + _ = db.getDefaultIndexOpts(); + + const query: notmuch.Query = undefined; + _ = notmuch.Query.init(undefined); + _ = query.getQueryString(); + _ = query.getDatabase(); + query.setOmitExcluded(.all); + query.setSort(.newest_first); + _ = query.getSort(); + try query.addTagExclude("tag"); + _ = try query.searchThreads(); + _ = try query.searchMessages(); + _ = try query.countMessages(); + _ = try query.countThreads(); + query.deinit(); + + const message: notmuch.Message = undefined; + _ = message.getDatabase(); + _ = message.getMessageID(); + _ = message.getThreadID(); + _ = message.getReplies(); + _ = try message.countFiles(); + _ = try message.getFilename(); + _ = try message.getFilenames(); + try message.reindex(null); + _ = try message.getFlag(.match); + message.setFlag(.match, true); + _ = message.getDate(); + _ = message.getHeader("Subject"); + _ = message.getTags(); + try message.addTag("tag"); + try message.removeTag("tag"); + try message.removeAllTags(); + try message.maildirFlagsToTags(); + _ = try message.hasMaildirFlag(.F); + try message.tagsToMaildirFlags(); + try message.freeze(); + try message.thaw(); + message.deinit(); + _ = message.getProperty("key"); + try message.addProperty("key", "value"); + try message.removeProperty("key", "value"); + try message.removeAllProperties(null); + try message.removeAllPropertiesWithPrefix(null); + _ = message.getProperties("key", true); + _ = try message.countProperties("key"); + + const thread: notmuch.Thread = undefined; + _ = thread.getThreadID(); + _ = thread.getTotalMessages(); + _ = thread.getTotalFiles(); + _ = thread.getToplevelMessages(); + _ = thread.getMessages(); + _ = thread.getMatchedMessages(); + _ = thread.getAuthors(); + _ = thread.getSubject(); + _ = thread.getOldestDate(); + _ = thread.getNewestDate(); + _ = thread.getTags(); + thread.deinit(); + + const directory: notmuch.Directory = undefined; + try directory.setMtime(0); + _ = directory.getMtime(); + _ = try directory.getChildFiles(); + _ = try directory.getChildDirectories(); + try directory.delete(); + directory.deinit(); + + const indexopts: notmuch.IndexOpts = undefined; + try indexopts.setDecryptPolicy(.auto); + _ = indexopts.getDecryptPolicy(); + indexopts.deinit(); + + const messages: notmuch.MessagesIterator = undefined; + _ = messages.collectTags(); + _ = try messages.next(); + messages.deinit(); + + var threads: notmuch.ThreadsIterator = undefined; + _ = try threads.next(); + threads.deinit(); + + var filenames: notmuch.FilenamesIterator = undefined; + _ = filenames.next(); + filenames.deinit(); + + var tags: notmuch.TagsIterator = undefined; + _ = tags.next(); + tags.deinit(); + + var values: notmuch.ValuesIterator = undefined; + _ = values.next(); + values.start(); + values.deinit(); + + var pairs: notmuch.PairsIterator = undefined; + _ = pairs.next(); + pairs.deinit(); + + const properties: notmuch.PropertiesIterator = undefined; + _ = properties.next(); + properties.deinit(); +} diff --git a/src/notmuch.zig b/src/notmuch.zig index b4576a3..2f32011 100644 --- a/src/notmuch.zig +++ b/src/notmuch.zig @@ -47,4 +47,5 @@ pub fn builtWith(name: [:0]const u8) bool { test { std.testing.refAllDecls(@This()); + _ = @import("compile_check.zig"); } -- 2.51.2