-
Notifications
You must be signed in to change notification settings - Fork 470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ALWAYS and NEVER macros (from SQLite) #720
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great, I find the macros very readable and clear. Just to confirm:
- Using the
NEVER
orALWAYS
macros 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?
@@ -45,7 +45,7 @@ else() | |||
set(ENABLE_REQUIRES_ALL_SYMBOLS OFF) | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why wasn't this previously formatted via clang?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is amazing 😮
#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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -364,8 +365,7 @@ H3Error h3NeighborRotations(H3Index origin, Direction dir, int *rotations, | |||
|
|||
int newRotations = 0; | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my info: This branch is now considered covered in our coverage metrics?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because for coverage it just sees 0 || oldBaseCell >= NUM_BASE_CELLS
(constant folds to oldBaseCell >= NUM_BASE_CELLS
, which is covered).
if (pentagonDirectionFaces[p].baseCell == baseCell) { | ||
dirFaces = pentagonDirectionFaces[p]; | ||
break; | ||
} | ||
} | ||
if (p == NUM_PENTAGONS) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be NEVER(p == NUM_PENTAGONS)
? This case is supposedly unreachable, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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]>
@@ -24,7 +24,7 @@ | |||
#ifndef ALLOC_H | |||
#define ALLOC_H | |||
|
|||
#include "h3api.h" // for TJOIN | |||
#include "h3api.h" // for TJOIN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file was incorrectly not formatted before
@@ -45,7 +45,7 @@ else() | |||
set(ENABLE_REQUIRES_ALL_SYMBOLS OFF) | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conceptually that is correct.
@@ -364,8 +365,7 @@ H3Error h3NeighborRotations(H3Index origin, Direction dir, int *rotations, | |||
|
|||
int newRotations = 0; | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
if (pentagonDirectionFaces[p].baseCell == baseCell) { | ||
dirFaces = pentagonDirectionFaces[p]; | ||
break; | ||
} | ||
} | ||
if (p == NUM_PENTAGONS) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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. |
@@ -56,7 +56,7 @@ Should be set to `Release` for production builds, and `Debug` in development. | |||
|
|||
## 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
return
ing an error code and possiblyfree
ing some resources) and to be visually inspectable. - debug builds: The defensive code is included and
assert
calls 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.