Conversation
This repository exists for the C ABI and every test in it was C++. That is not a small gap. The wrapper is header-only, so a C++ test reaches the ABI through it, and everything the wrapper keeps for you is something the suite never once asked the ABI to keep. A destructor does not run twice. A zu::Connection has no null state to pass. A Result cannot outlive its Connection by accident, because the wrapper arranged that it cannot. Those are ABI promises, and they were all being tested by proxy or not at all. So: a C harness, and the misuse suite the scorecard item asks for, written in C because that is the only place most of it can be written. The harness is the C++ one with the C++ taken out. A hundred and fifty lines, no dependency, a case list written by hand at the bottom because C has no portable way to run code before main. Leaving a case out of that list is still caught rather than silent: it is then a static function nothing calls, which is -Wunused-function, which is -Werror in CI. A check that fails returns from its case, so a failing case does not run its own cleanup and leaks whatever it held, which is deliberate: the failure is the news and a goto ladder in every case would bury the thing the case is about. Fifteen cases. A statement used after its connection closed, a result outliving its connection on purpose because the header says it may, a null handle into every call that takes one, every free and close given nothing, a cell index off both ends of both axes, a thousand connections opened and closed, five hundred opens that failed, a transaction abandoned by closing rather than ending it, and the ones that look like misuse and are not. Two of them found things. The concurrency case calls back into the library from the progress watcher, which is the one place a second thread is certainly inside a call at a known moment, so the check is deterministic rather than a race run once. zu_query, zu_prepare and zu_begin all answer ZU_MISUSE_CONCURRENT, which is the promise kept. zu_conn_rows_read answers ZU_OK, and the header says it should not: it names zu_conn_interrupt as the only call meant to cross threads. The behaviour is right and the sentence is wrong, since a watcher reporting a rate needs the row count and has no other way to it. Filed as tamnd/zu#618, and all five statuses are asserted here so that whichever way it is resolved is a failing test rather than a surprise. The open case pins down what a bad path answers, which I had wrong. A file that is there and is not a database is ZU_CORRUPT, a path with nothing at it and a path that is a directory are ZU_IO, and none of them is ZU_ERROR. That distinction is the one a user needs, damaged against missing, and it was undocumented in the sense that nothing checked it. The correction went onto tamnd/zu#614 too, which had reported the wrong status. Then the CI step, which is what turns the item's "no leak" into something checked. The C suite runs once more under the sanitizers with leak detection on, which the rest of the suite cannot do. It can because it is the one file here that gives everything back by hand. LSan counts every malloc it sees including the ones inside libzu, which is uninstrumented but not invisible, so a handle this suite dropped is caught even though the allocation happened on the far side of the ABI. Verified against a deliberate leak first, because a leak checker that reports nothing and a leak checker that is switched off read the same. 38 of 38 green on server3 against libzu at engine HEAD, and the C suite clean under address and undefined behaviour with leaks on. Clang is covered by CI rather than here: there is no clang on the build host and installing one meant changing package configuration that belongs to somebody else.
tamnd
added a commit
to tamnd/zu
that referenced
this pull request
Aug 22, 2026
tamnd/zu-c#7 put a C test harness and a fifteen case misuse suite in the repository that had, until now, no C tests at all. The row goes 22 to 39 of the 90 it owes. Worth saying why that was the shape of the work rather than a file of new cases. The wrapper is header-only, so a C++ test reaches the ABI through it, and every promise the wrapper keeps for a caller is one the suite had never asked the ABI to keep. Most of the misuse item is in that gap: a destructor does not run twice, a zu::Connection has no null state to pass, a Result cannot outlive its Connection by accident. Two findings out of it, both filed: the progress watcher may call zu_conn_rows_read though the header says only zu_conn_interrupt crosses threads, which is #618, and a bad path answers ZU_CORRUPT or ZU_IO rather than the ZU_ERROR I had reported on #614.
22 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This repository exists for the C ABI and every test in it was C++.
That sounds like a tidiness complaint and it is not. The wrapper is header-only, so a C++ test reaches the ABI through the wrapper, and every promise the wrapper keeps on your behalf is a promise the suite never once asked the ABI to keep. A destructor does not run twice. A
zu::Connectionhas no null state to pass. AResultcannot outlive itsConnectionby accident, because the wrapper arranged that it cannot. All three of those are things the ABI guarantees, and all three were being tested by proxy or not at all.The
misusescorecard item is exactly the category that falls in the gap: deliberately wrong programs, and for each one no crash, no leak, and a clear message. Most of those programs cannot be written in C++.The harness
test/harness.h, which isharness.hppwith the C++ taken out. No dependency, same shape, same naming.Two decisions worth knowing. The case list is written out by hand at the bottom, because C has no way to run code before
mainthat works on every compiler this repository is willing to be built by. Forgetting to list a case is not silent: it is then a static function nothing calls, which is-Wunused-function, which is-Werrorin CI. And a check that fails returns from its case rather than unwinding, so a failing case does not run its own cleanup and leaks what it was holding. That is on purpose. A failing case has already found the news, and the alternative is a goto ladder in every case obscuring the thing the case is about.The suite
Fifteen cases in
test/misuse.c. A statement used after its connection closed. A result outliving its connection on purpose, because the header says it may and a pool depends on it. A null handle into every call that takes one, including the accessors, whose whole error channel is the status. Every free and close given nothing, which a C cleanup path leans on in every line it writes. A cell index off both ends of both axes. A thousand connections opened and closed. Five hundred opens that failed. A transaction abandoned by closing rather than ending it, checked by reopening rather than by asking the connection that did the writing. And a case for the programs that look like misuse and are not, which is the half that decides whether a strict library is usable.The descriptor cases measure the lowest free descriptor rather than counting anything, so they hold on both platforms. The leak they are about is the one that does not look like a leak: a host opening a connection per request falls over on the thousandth request, hours in, with a message about too many open files and nothing pointing here.
Two things it found
The watcher may call back in, partly. The concurrency case had to be deterministic, because two threads racing is not a test: whichever way it comes out, it came out that way once. The progress watcher solves it, since it runs on a thread of the library's and only while a statement is running. From in there:
The guard works. But the header names
zu_conn_interruptas the one call meant to cross threads, andzu_conn_rows_readis allowed too. The behaviour is right and the sentence is wrong: a watcher that wants to report a rate needs the row count and has no other way to reach it. Filed as tamnd/zu#618. All five statuses are asserted here, so whichever way that is resolved is a failing test rather than a surprise.What a bad path answers, which I had wrong. A file that is there and is not a database is
ZU_CORRUPT, withcorrupt file header: ... too short to be a zu1 database. A path with nothing at it, and a path that is a directory, areZU_IO. None of them isZU_ERROR. That is the distinction a user actually needs, damaged against missing, and the engine already draws it well; nothing was checking that it does. The correction went onto tamnd/zu#614, which had reported the wrong status.Leaks, checked rather than claimed
New CI step: the C suite runs once more under the sanitizers with leak detection on. The rest of the suite cannot do that, and this file can, because it is the one file here that gives everything back by hand rather than by destructor.
LSan counts every malloc it sees, including the ones inside libzu, which is uninstrumented but not invisible. So a connection or a result this suite dropped is caught even though the allocation happened on the far side of the ABI. That is what turns the item's "no leak" into a checked thing.
Verified against a deliberate leak before believing it, because a leak checker that reports nothing and a leak checker that is switched off read exactly the same.
Checked
38 of 38 green on server3 with gcc 13 against libzu at engine HEAD. The C suite clean under address and undefined behaviour with leaks on.
Clang is covered by CI rather than by me: there is no clang on the build host, and getting one there meant changing apt configuration that belongs to somebody else's software on a shared machine.