Add ALWAYS and NEVER macros (from SQLite)#720
Conversation
nrabinowitz
left a comment
There was a problem hiding this comment.
This looks great, I find the macros very readable and clear. Just to confirm:
- Using the
NEVERorALWAYSmacros on a branch condition will properly exclude that branch from coverage? - The drop in coverage in this PR is due to the macros exposing reachable branches we had previously excluded from coverage?
| endif() | ||
|
|
||
| option(ENABLE_COVERAGE "Enable compiling tests with coverage." ON) | ||
| option(ENABLE_COVERAGE "Enable compiling tests with coverage." OFF) |
There was a problem hiding this comment.
For my info, why toggle this?
Oh, I see - this now changes the production build as well? So locally, we should make sure to toggle this ON when developing, right?
There was a problem hiding this comment.
I believe this should be OFF while developing in order to get the benefit of the assertions.
| void *H3_MEMORY(malloc)(size_t size); | ||
| void *H3_MEMORY(calloc)(size_t num, size_t size); | ||
| void *H3_MEMORY(realloc)(void *ptr, size_t size); | ||
| void H3_MEMORY(free)(void *ptr); |
There was a problem hiding this comment.
Why wasn't this previously formatted via clang?
There was a problem hiding this comment.
It was not included in the list of source files, presumably an oversight.
| * | ||
| * May you do good and not evil. | ||
| * May you find forgiveness for yourself and forgive others. | ||
| * May you share freely, never taking more than you give. |
| #define NEVER(X) (0) | ||
| #elif !defined(NDEBUG) | ||
| #define ALWAYS(X) ((X) ? 1 : (assert(0), 0)) | ||
| #define NEVER(X) ((X) ? (assert(0), 1) : 0) |
There was a problem hiding this comment.
So I understand, the expression (assert(0), 1) means: first assert 0 (i.e. fail), then return 1, which is unused in practice but necessary for the compiler?
There was a problem hiding this comment.
Conceptually that is correct.
| // Should not be possible because `origin` would have to be a | ||
| // pentagon | ||
| return neighborResult; // LCOV_EXCL_LINE | ||
| // TODO: Reachable via fuzzer |
There was a problem hiding this comment.
For my info: Were these TODOs uncovered by trying to use the new macros here and triggering assertion errors?
There was a problem hiding this comment.
Yes, I generally tried to replace LCOV exclusions first with the macros and had to replace them with TODOs, since this PR makes it clear they are reachable.
| int oldBaseCell = H3_GET_BASE_CELL(current); | ||
| if (oldBaseCell < 0 || // LCOV_EXCL_BR_LINE | ||
| oldBaseCell >= NUM_BASE_CELLS) { | ||
| if (NEVER(oldBaseCell < 0) || oldBaseCell >= NUM_BASE_CELLS) { |
There was a problem hiding this comment.
For my info: This branch is now considered covered in our coverage metrics?
There was a problem hiding this comment.
Yes, because for coverage it just sees 0 || oldBaseCell >= NUM_BASE_CELLS (constant folds to oldBaseCell >= NUM_BASE_CELLS, which is covered).
| break; | ||
| } | ||
| } | ||
| if (p == NUM_PENTAGONS) { |
There was a problem hiding this comment.
Should this be NEVER(p == NUM_PENTAGONS)? This case is supposedly unreachable, right?
There was a problem hiding this comment.
I'm not sure that would satisfy the compile error about dirFaces being uninitialized. We could check.
Co-authored-by: Nick Rabinowitz <[email protected]>
Co-authored-by: Nick Rabinowitz <[email protected]>
| #define ALLOC_H | ||
|
|
||
| #include "h3api.h" // for TJOIN | ||
| #include "h3api.h" // for TJOIN |
There was a problem hiding this comment.
This file was incorrectly not formatted before
| endif() | ||
|
|
||
| option(ENABLE_COVERAGE "Enable compiling tests with coverage." ON) | ||
| option(ENABLE_COVERAGE "Enable compiling tests with coverage." OFF) |
There was a problem hiding this comment.
I believe this should be OFF while developing in order to get the benefit of the assertions.
| void *H3_MEMORY(malloc)(size_t size); | ||
| void *H3_MEMORY(calloc)(size_t num, size_t size); | ||
| void *H3_MEMORY(realloc)(void *ptr, size_t size); | ||
| void H3_MEMORY(free)(void *ptr); |
There was a problem hiding this comment.
It was not included in the list of source files, presumably an oversight.
| #define NEVER(X) (0) | ||
| #elif !defined(NDEBUG) | ||
| #define ALWAYS(X) ((X) ? 1 : (assert(0), 0)) | ||
| #define NEVER(X) ((X) ? (assert(0), 1) : 0) |
There was a problem hiding this comment.
Conceptually that is correct.
| int oldBaseCell = H3_GET_BASE_CELL(current); | ||
| if (oldBaseCell < 0 || // LCOV_EXCL_BR_LINE | ||
| oldBaseCell >= NUM_BASE_CELLS) { | ||
| if (NEVER(oldBaseCell < 0) || oldBaseCell >= NUM_BASE_CELLS) { |
There was a problem hiding this comment.
Yes, because for coverage it just sees 0 || oldBaseCell >= NUM_BASE_CELLS (constant folds to oldBaseCell >= NUM_BASE_CELLS, which is covered).
| // Should not be possible because `origin` would have to be a | ||
| // pentagon | ||
| return neighborResult; // LCOV_EXCL_LINE | ||
| // TODO: Reachable via fuzzer |
There was a problem hiding this comment.
Yes, I generally tried to replace LCOV exclusions first with the macros and had to replace them with TODOs, since this PR makes it clear they are reachable.
| break; | ||
| } | ||
| } | ||
| if (p == NUM_PENTAGONS) { |
There was a problem hiding this comment.
I'm not sure that would satisfy the compile error about dirFaces being uninitialized. We could check.
Yes, you can find an example here: https://coveralls.io/builds/53603547/source?filename=src%2Fh3lib%2Flib%2FdirectedEdge.c#L282
Yes, I had hoped this PR would increase coverage by being able to exclude some more branches we had not marked with exclusions, but rather it exposed they are reachable, and we should cover them via unit testing. |
| ## ENABLE_COVERAGE | ||
|
|
||
| Whether to compile `Debug` builds with coverage instrumentation (compatible with GCC, Clang, and lcov). | ||
| Whether to compile `Debug` builds with coverage instrumentation (compatible with GCC, Clang, and lcov). This also elides defensive code in the library. |
There was a problem hiding this comment.
I had to look up "elides". Maybe "bypasses"? 😅
And maybe even a quick note on why:
"This also bypasses defensive code in the library, since it should typically not be reachable by coverage."
| ## Defensive code | ||
|
|
||
| H3 uses preprocessor macros borrowed from [SQLite's testing methodology](https://www.sqlite.org/testing.html) to include defensive code in the library. Defensive code is code that handles error conditions for which there are no known test cases to demonstrate it. The lack of known test cases means that without the macros, the defensive cases could inappropriately reduce coverage metrics, disincentivizing including them. | ||
|
|
There was a problem hiding this comment.
nit/suggestion: What about presenting the next three paragraphs as an unordered list, introduced by something like
The macros will behave differently, depending on the build type being "release", "debug", or "coverage":
It is probably a matter of personal choice, but I find it can make documentation a little more scannable.
There was a problem hiding this comment.
One styling idea:
The macros will behave differently, depending on the library build type being "release", "debug", or "coverage":
- release builds: The defensive code is included without modification. These branches are intended to be very simple (usually only
returning an error code and possiblyfreeing some resources) and to be visually inspectable. - debug builds: The defensive code is included and
assertcalls are included if the defensive code is invoked. Any unit test or fuzzer which can demonstrate the defensive code is actually reached will trigger a test failure and the developers can be alerted to cover the defensive code in unit tests. - coverage builds: The defensive code is not included by replacing its condition with a constant value. The compiler removes the defensive code and it is not counted in coverage metrics.
|
These macros are awesome! |
|
@isaacbrodsky, this is maybe more of a conceptual question, but what if there were a user who was risk-tolerant but very interested in performance. Would it make sense for them to want to build a release version of the library that bypasses the defensive code (like we do in the coverage build)? Is that configuration possible, or should we allow for that? Would that provide any potential performance benefit by skipping the defensive checks? |
I don't think that configuration is supported here. It would be possible to eliminate some conditional checks, but I would imagine the performance gain to be modest. We could add another CMake option that controls whether those are included or not that's separate from (probably defaulted from) the coverage option. |
Makes sense. And I don't think we need to make any changes here. That was just a conceptual question. |
77cad8f to
98a747a
Compare
Adds ALWAYS, NEVER, and related macros from SQLite (as linked in the code comments) to help ensure fuzzers or unit tests that reach expected-unreachable code appropriately assert. This also excludes defensive code blocks from coverage metrics.