Skip to content
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

Merged
merged 21 commits into from
Nov 8, 2022

Conversation

isaacbrodsky
Copy link
Collaborator

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.

@coveralls
Copy link

coveralls commented Oct 25, 2022

Coverage Status

Coverage decreased (-0.4%) to 98.597% when pulling 98a747a on isaacbrodsky:always-macro into da5b21f on uber:master.

Copy link
Collaborator

@nrabinowitz nrabinowitz left a 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 or ALWAYS 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)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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);
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is amazing 😮

src/h3lib/include/h3Assert.h Outdated Show resolved Hide resolved
#define NEVER(X) (0)
#elif !defined(NDEBUG)
#define ALWAYS(X) ((X) ? 1 : (assert(0), 0))
#define NEVER(X) ((X) ? (assert(0), 1) : 0)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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) {
Copy link
Collaborator

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?

Copy link
Collaborator Author

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.

website/docs/core-library/testing.md Outdated Show resolved Hide resolved
isaacbrodsky and others added 2 commits October 25, 2022 16:16
Co-authored-by: Nick Rabinowitz <public@nickrabinowitz.com>
Co-authored-by: Nick Rabinowitz <public@nickrabinowitz.com>
@@ -24,7 +24,7 @@
#ifndef ALLOC_H
#define ALLOC_H

#include "h3api.h" // for TJOIN
#include "h3api.h" // for TJOIN
Copy link
Collaborator Author

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)
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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)
Copy link
Collaborator Author

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) {
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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) {
Copy link
Collaborator Author

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.

@isaacbrodsky
Copy link
Collaborator Author

This looks great, I find the macros very readable and clear. Just to confirm:

* Using the `NEVER` or `ALWAYS` macros on a branch condition will properly exclude that branch from coverage?

Yes, you can find an example here: https://coveralls.io/builds/53603547/source?filename=src%2Fh3lib%2Flib%2FdirectedEdge.c#L282

* The drop in coverage in this PR is due to the macros exposing reachable branches we had previously excluded from coverage?

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.
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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 returning an error code and possibly freeing 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.

@ajfriend
Copy link
Contributor

ajfriend commented Nov 7, 2022

These macros are awesome!

@ajfriend
Copy link
Contributor

ajfriend commented Nov 7, 2022

@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?

@isaacbrodsky
Copy link
Collaborator Author

@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.

@ajfriend
Copy link
Contributor

ajfriend commented Nov 8, 2022

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.

@isaacbrodsky isaacbrodsky merged commit fc5a3c8 into uber:master Nov 8, 2022
@isaacbrodsky isaacbrodsky deleted the always-macro branch November 8, 2022 21:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants