S2 C++ Style Guide
Purpose
This guide documents the C++ coding conventions used by the S2 Geometry Library. It does not attempt to justify why these conventions are used, and indeed in some cases they could be improved upon.
In general, the S2 C++ coding style is built on the Google C++ Style Guide. However it’s important to remember that the S2 library has a long history, and many parts of the library were developed before modern C++ features even existed. The S2 library has many clients both within and outside of Google (through the open source release), which can make it very difficult to adopt new conventions.
The overriding objective when adding new code should be to maintain consistency within the library. In cases where the S2 conventions conflict with the current Google style guide, the existing S2 conventions should take precedence. Changes in coding style should only be made when it is practical to adopt them across the entire library, including the important step of updating all affected clients so that any deprecated classes or methods can be removed.
The first step when adding new classes or methods should be to find similar classes or methods that already exist in the library, and then follow their conventions as closely as possible.
External Dependencies
The S2 library is built on many platforms, and has an open source version with many external clients. New dependencies should be avoided. In particular, note that S2 does not currently depend on the following:
-
GMock (except for a few files that not currently part of the open-source release). Instead please limit yourself to the more basic features of GUnit.
-
Protocol buffers.
-
Any Google maps or geo library.
-
Many features of Abseil, including
StatusOr
(see “Error Handling” below).
Filenames
The S2 library has a flat directory structure. Generally each header file contains either a single class (plus any associated helper classes) or a collection of functions in a namespace.
Filenames should be in lowercase with words separated by underscores. Note that “s2”, “s1”, etc. are not considered to be words. Examples:
s2builder.h
s2cell_id.h
encoded_s2shape_index.h
For historical reasons, latlng
is treated as one word even though the
corresponding classes are camel-cased. For example, S2LatLngRect
is defined
in s2latlng_rect.h
.
Files that define functions or classes within the s2shapeutil
and
s2builderutil
namespaces include that namespace as a prefix:
s2shapeutil_conversion.h
s2shapeutil_count_edges.h
s2builderutil_lax_polyon_layer.h
s2builderutil_snap_functions.h
Classes or functions that are needed in multiple places within the S2 library
but are not part of the public API can be put in header files with the
_internal.h
suffix. Examples:
s2coords_internal.h
s2predicates_internal.h
This can help remove clutter from the public API and also facilitate testing.
Note that the internal content can be implemented in the same .cc
file as the
public part of the API.
“Util” header files are strongly discouraged. Instead try to break your API
into meaningful components. The only remaining “util”-style header file in the
S2 library is s2pointutil.h
which contains about ten functions for
manipulating points.
Documentation
Comment Style
Please put effort into your comments; they are the most important part of your code.
Comments should be full sentences, starting with a capital letter and ending with a period. This rule applies even to partial-line comments when reasonable:
enum IndexStatus {
STALE, // There are pending updates.
UPDATING, // Updates are currently being applied.
FRESH, // There are no pending updates.
};
For bonus points, put two spaces after your periods: contrary to popular belief, this is not some archaic convention from the days of typewriters, but rather an effort to mimic professional typography (take a close look at a book sometime).
Header Files
Public classes and methods should be thoroughly documented in the header file. Important topics to cover include:
- Description of the problem being solved
- Code samples showing how to use the API
- Limitations and known problems
Requirements of classes and methods may be documented via a “REQUIRES” comment. For example:
// Encodes an unsigned integer in little-endian format using "length" bytes.
// (The client must ensure that the encoder's buffer is large enough.)
//
// REQUIRES: T is an unsigned integer type.
// REQUIRES: 2 <= sizeof(T) <= 8
// REQUIRES: 0 <= length <= sizeof(T)
// REQUIRES: value < 256 ** length
// REQUIRES: encoder->avail() >= length
template <class T>
void EncodeUintWithLength(T value, int length, Encoder* encoder);
The following callouts may also be useful:
ENSURES:
properties guaranteed by the class/method.DEFAULT:
documents the default value of an option.CAVEAT:
an unexpected property or potential pitfall.NOTE:
to draw special attention to a comment.
Implementations
Implementation details should be documented where the corresponding types or methods are defined, not where they are declared. This means that types and member variables declared in the header file should be documented there, whereas private non-inline methods should be documented in the .cc file. This convention makes it easier to find documentation when looking at code, and also makes it more likely that the documentation will be updated when implementations are changed.
Here is an example of documenting a private method in a .cc
file:
// Outputs the current buffered path (which is assumed to be a loop), and
// resets the state to prepare for buffering a new loop.
void S2BufferOperation::OutputPath() {
op_.AddLoop(path_);
path_.clear(); // Does not change capacity.
have_input_start_ = false;
have_offset_start_ = false;
}
External Documentation
In addition to header file comments, significant new classes should have
documentation added in the “g3doc” directory. Typically this will expand
significantly on what is in the header file, giving more background information
and examples. Documentation added here will be published on the s2geometry.io
website.
Ideally documentation should be added and/or updated in the same changelist that adds the relevant code.
Namespaces
The S2 library as a whole is not defined within a namespace. (Namespaces did
not even exist when it was first designed.) Instead most of the global symbols
include S2
in their names (except for a few classes that use S1
, R2
,
etc. instead.)
Nevertheless, some parts of the library do use namespaces. In general the names
of namespaces should be in lowercase with words separated by underscores
(e.g. s2polyline_alignment
). Note that the “util” suffix is not considered a
word, so we have:
namespace s2shapeutil { ... }
namespace s2builderutil { ... }
There is also a namespace called S2
that contains many of the low-level
functions defined by the library (e.g. S2::TurnAngle
, S2::RobustCrossProd
,
etc.) Note that while class names often include S2
as a prefix rather
than being placed in a namespace, this convention is not typically used for
global functions or constants and so these entities should be put in a namespace
instead.
In general S2 does not use nested namespaces, except for internal content which
may be placed in a nested namespace called internal
. This technique can be
used alone to protect internal content (e.g. the file s2testing.h
defines some
content in the S2::internal
namespace) and/or with an _internal.h
header
file.
Using Declarations
using
declarations are used extensively in .cc
files in order to reduce code
verbosity. This includes types and functions from the std
and absl
namespaces as well as aliases for nested types. For example,
s2buffer_operation_test.cc
has the following declarations:
using absl::make_unique;
using std::max;
using std::string;
using std::unique_ptr;
using std::vector;
using EndCapStyle = S2BufferOperation::EndCapStyle;
using PolylineSide = S2BufferOperation::PolylineSide;
Note that this is allowable only in .cc
files, not header files, and that
using namespace
declarations are prohibited.
Classes
Class names in S2 should be nouns with the first letter of each word capitalized. Examples:
S2Shape
S2LatLngRect
S1Angle
Short, descriptive names are highly preferred. It is reasonable to put as much thought into the names of your classes, methods, variables, etc. as you put into the code itself. Good names are essential to making APIs easier to understand.
The names of subtypes should be obviously related to their parent class. For
example, S2LaxPolygonShape
is a subtype of S2Shape
, and
s2builderutil::S2CellIdSnapFunction
is a subtype of S2Builder::SnapFunction
.
Index, Query, Operation
Classes in S2 should be designed so as to separate the data representation from
the actual operations as much as possible.A class that contains many geometric
objects for the purpose of performing operations on them should preferentially
be called an Index
. Such classes should define only the minimum number of
functions necessary to access the data in the index. Examples:
S2ShapeIndex
S2CellIndex
S2PointIndex
Rather than having a single class with a wide API, prefer to have several
classes or standalone functions with narrow APIs. For example, rather than
having a method in S2ShapeIndex
to count the total number of edges, instead
there is a separate function to do this defined in s2shapeutil_count_edges.h
.
A class that does something useful with an index should preferentially be called
a Query
, unless it can be viewed as function that maps an index to another
similar index in which case it may be called an Operation
. Examples:
S2ClosestPointQuery
S2ConvexHullQuery
S2CrossingEdgeQuery
S2BooleanOperation
S2BufferOperation
Index
classes should be thread-safe but Query
and Operation
classes
generally are not. Instead the expectation is that each thread should create
its own Query
objects (typically on the stack) as needed. The fact that
Query
classes are not thread-safe allows them to maintain internal state while
executing the query and/or to cache information between queries.
Query
and Operation
classes typically follow one of two design patterns.
One is the builder pattern, where data can be added incrementally using several
method calls, and then a different method is called to compute the result.
Examples following this pattern include S2WindingOperation
,
S2BufferOperation
, S2ConvexHullQuery
, etc.
The other common pattern is a pure function pattern where the arguments are
supplied and the result is returned during a single function call. Examples of
this pattern include S2ClosestEdgeQuery
, S2ClosestPointQuery
,
S2ContainsPointQuery
, etc. Note that even though this pattern in theory
allows the query method to be stateless and therefore thread-safe, S2 classes
avoid this in favor of rather allowing query objects to maintain whatever state
they may need in order to make queries as efficient as possible. This means
that query methods should not be const even if the current implementation of
the method would allow this, since this might prevent future optimizations.
When query methods need to return several values of different types, the query
method should return a result object. For example, S2ClosestEdgeQuery
returns
a Result
object with methods such as distance()
, shape_id()
, and
edge_id()
.
This technique is strongly preferred over the alternative technique of having the query object store the latest result in its own state and having methods to retrieve the result field-by-field. For example, a Result object makes it much easier to combine the results of several queries:
auto a_result = query.GetResult(a);
auto b_result = query.GetResult(b);
... do something with both results ...
Note that as long as the optimizer does its job correctly, returning a result by value does not involve any actual copying. Instead the caller passes a hidden pointer and the result is constructed directly on the caller’s stack.
Options
Classes with options should have a nested Options
class. For
example, S2Builder
has an S2Builder::Options
class. Options
classes generally consist of a set of fields with “get” and “set” methods. For
example:
class S2RegionCoverer {
public:
class Options {
public:
Options();
int max_cells() const;
void set_max_cells(int max_cells);
...
};
explicit S2RegionCoverer(const Options& options);
const Options& options() const;
...
};
The set methods generally return void
, but it is also acceptable to return a
reference to the Options object in order to allow chained initialization:
S2Foo foo{S2Foo::Options().set_bar(x).set_baz(y)};
Options are usually read-only once the relevant object has been constructed.
Classes that allow options to be modified after construction do so via a
mutable_options()
method:
Options* mutable_options();
Options classes are copyable and movable.
Methods
Accessor method names are nouns, written in lowercase with words separated by underscores. Examples:
S1Angle::degrees()
S2Builder::options()
Non-accessor method names should be verbs, written in uppercase with the first letter of each word capitalized. Examples:
S2Builder::StartLayer(...)
S2Builder::Build(...)
S2ShapeIndex::Minimize()
Methods that return something but that don’t have an obvious verb should
typically start with Get
. For example:
S2ShapeIndex::GetShape(int id)
S2::GetPointOnLine(...)
If the method may return an empty set of results, the verb Find
can also be
used. For example:
S2ClosestEdgeQuery::FindClosestEdges(...)
There are exceptions for well-known geometric operations that yield objects of
the same type. For example, S2Cap::Union
computes the union of two S2Cap
objects. This naming convention is generally reserved for low-level geometric
objects such as S1Interval
, S2LatLngRect
, S2Cap
, etc.
Methods that return boolean values (predicates) should be active verbs
(e.g. S2LatLngRect::Contains()
) or start with the word “is”
(e.g. S2Shape::is_empty()
, S2::IsUnitLength()
). (Note that this convention
differs from std::vector<T>::empty()
for example.)
Inline Methods
Except for accessor methods, inline methods should be declared and defined separately (just like non-inline methods). Note that only the definition needs to be declared inline, not the declaration. Inline definitions should be placed at the bottom of the corresponding header file after a comment saying “Implementation details follow”. For example:
class S2PaddedCell {
public:
// Construct an S2PaddedCell for the given cell id and padding.
S2PaddedCell(S2CellId id, double padding);
S2CellId id() const { return id_; }
double padding() const { return padding_; }
int level() const { return level_; }
// Return the bound for this cell (including padding).
const R2Rect& bound() const { return bound_; }
// Return the (i,j) coordinates for the child cell at the given traversal
// position. The traversal position corresponds to the order in which child
// cells are visited by the Hilbert curve.
void GetChildIJ(int pos, int* i, int* j) const;
...
};
////////////////// Implementation details follow ////////////////////
inline void S2PaddedCell::GetChildIJ(int pos, int* i, int* j) const {
int ij = S2::internal::kPosToIJ[orientation_][pos];
*i = ij >> 1;
*j = ij & 1;
}
Constructors, Init, Factory Methods
Virtually all classes should have constructors. If the constructor parameters
may be ambiguous, static factory methods should be used instead. For example,
S2LatLng
has the following factory methods:
static constexpr S2LatLng FromRadians(double lat_radians, double lng_radians);
static constexpr S2LatLng FromDegrees(double lat_degrees, double lng_degrees);
static constexpr S2LatLng FromE5(int32_t lat_e5, int32_t lng_e5);
static constexpr S2LatLng FromE6(int32_t lat_e6, int32_t lng_e6);
static constexpr S2LatLng FromE7(int32_t lat_e7, int32_t lnt_e7);
Factory method names should begin with “From”, except for simple and frequently
used classes where brevity takes precedence. For example, S1Angle
has the
factory methods S1Angle::Radians(double radians)
and S1Angle::Degrees(double
degrees)
.
Factory methods should also be used when the parameter order may be ambiguous. Examples:
static R2Rect FromCenterSize(const R2Point& center, const R2Point& size);
static S2CellId FromFacePosLevel(int face, uint64 pos, int level);
Complex classes that allocate memory should also have a default constructor and
an Init
method. This allows clients to declare instances of these classes as
member variables and defer their initialization as necessary. This allows
clients to avoid allocating objects on the heap and accessing them through
unique_ptr<>
just because the arguments needed to initialize them are not
readily available in their own constructor. This not only reduces memory
allocation but also increases locality of reference.
So for example, S2Builder
has the following:
class S2Builder {
...
// Default constructor; requires Init() to be called.
S2Builder();
// Convenience constructor that calls Init().
explicit S2Builder(const Options& options);
// Initializes an S2Builder with the given options.
void Init(const Options& options);
const Options& options() const { return options_; }
...
};
Factory methods that return unique_ptr<T>
should be avoided, since this
forces clients to allocate memory and also reduces locality of reference.
Types
Templates
Templated types and functions are discouraged in the public parts of the API. Templated code is necessarily harder to document and understand, and the generalization that it offers is frequently overrated and underused.
Templates may be used internally when this substantially reduces code
duplication or otherwise makes the library easier to maintain. For example, the
S2 classes S2ClosestEdgeQuery
and S2FurtherEdgeQuery
are each wrappers
around a templated internal class called S2ClosestEdgeQueryBase
.
Distances and Angles
Distances and angles should be represented using the classses S1Angle
or
S1ChordAngle
. S2 APIs should always use these classes in preference to
accepting distances in radians, degrees, meters, etc. since it avoids the need
to specify units. (Note that the S2 library operates on the unit sphere, not
the Earth’s surface, and therefore the natural unit of distance is the radian.)
Whenever possible, classes should support both S1Angle
and S1ChordAngle
.
S1ChordAngle
is frequently used internally, since (1) it is much faster to
compute and (2) it supports the exact distance predicates that are needed to
implement algorithms robustly (see s2predicates.h
). Methods that accept an
S1Angle
often convert it to S1ChordAngle
internally.
Note that S1ChordAngle
is only capable of representing distances up to 180
degrees, and that its accuracy declines for distances approaching 180 degrees.
Distances which may be greater than 180 degrees, such as polyline lengths or
polygon perimeters, should always be represented using S1Angle
.
Parameters
Small types (16 bytes or less) should generally be passed by value. This includes the following commonly used S2 types:
S2CellId
S2LatLng
S1Angle
S1ChordAngle
Types larger than 16 bytes should be passed as follows:
-
If ownership is being transferred, use
unique_ptr<T>
. -
If the callee keeps a pointer to the object that outlives the function call, use
const T*
. For example,S2ClosestEdgeQuery
keeps a pointer to itsS2ShapeIndex
constructor argument, so it accepts that argument by const pointer:explicit S2ClosestEdgeQuery(const S2ShapeIndex* index, const Options& options = Options());
-
Otherwise, the parameter should be passed by const reference. Note in particular that
S2Point
is passed by const reference. Const pointers should not be used unless the callee keeps a pointer as outlined above. -
The only exception to the rule above is when the callee needs to make a copy of the argument and* the given type has an efficient move operator (i.e., one that transfers ownership of data rather than simply copying it). In this case the parameter may be passed by value rather than by const reference. For example:
class S2CellUnion final : public S2Region { ... // Constructs a cell union with the given S2CellIds, then calls Normalize() // to sort them, remove duplicates, and merge cells when possible. // // The argument is passed by value, so if you are passing a named variable // and have no further use for it, consider using std::move(). // // A cell union containing a single S2CellId may be constructed like this: // // S2CellUnion example({cell_id}); explicit S2CellUnion(std::vector<S2CellId> cell_ids); ... } inline S2CellUnion::S2CellUnion(std::vector<S2CellId> cell_ids) : cell_ids_(std::move(cell_ids)) { Normalize(); }
Note that this guidance is considerably more strict than advocated by the current Google style guide. Do not pass parameters larger than 16 bytes unless (1) the type has an efficient
std::move
operator and (2) the callee makes a copy of the argument usingstd::move
.
Const Value Parameters
Parameters passed by value should never be “const” in method declarations. They should only be declared const (if desired) in method definitions. For example:
class S2CellId {
...
// Return true if the given cell is contained within this one.
bool contains(S2CellId other) const;
...
};
////////////////// Implementation details follow ////////////////////
inline bool S2CellId::contains(const S2CellId other) const {
DCHECK(is_valid());
DCHECK(other.is_valid());
return other >= range_min() && other <= range_max();
}
Note that most S2 code does not bother declaring local variables and parameters passed by value as “const” even when those values are not modified, since the additional documentation benefit often does not outweigh the additional code clutter.
Geometry Output
Classes that construct geometry and return it to the client should do so via an
S2Builder::Layer
parameter. For example, the S2BufferOperation
constructor
looks like this:
explicit S2BufferOperation(std::unique_ptr<S2Builder::Layer> result_layer,
const Options& options = Options{});
When the client eventually calls the S2BufferOperation::Build()
method, the
output geometry is sent to the given result_layer
. This allows the client to
have complete control over how the output geometry is represented. There are
various possible layer types that build different representations such as
S2Polyon
or S2LaxPolygonShape
.
Obsolete Types
Clients are discouraged from using certain S2 classes for new code. Classes
that have officially been deprecated are marked as such using the
ABSL_DEPRECATED
macro. Currently the only such class is S2PolygonBuilder
which has been superseded by the far more robust and flexible S2Builder
.
However it is important to note that the classes S2Polygon
and S2Polyline
are also obsolete and should ideally be deprecated at some point in the future.
New code should prefer to use their S2Shape
-based replacements,
S2LaxPolygonShape
and S2LaxPolylineShape
. These new classes can be
constructed and decoded far more efficiently than S2Polygon
and S2Polyline
,
and are also capable of representing degenerate geometry if desired (which is
what the Lax
in their names refers to).
Error Handling
In general S2 code should try to avoid generating runtime errors.
Parameter Range Checking
If a parameter has a limited valid range, the preferred technique in S2 is to
DCHECK
that the parameter is valid and then clamp it to the
allowed range. For example:
class S2BufferOperation::Options {
...
// Specifies the allowable error when buffering, expressed as a fraction
// of buffer_radius().
//
// REQUIRES: error_fraction() >= kMinErrorFraction
// REQUIRES: error_fraction() <= 1.0
//
// DEFAULT: 0.01 (i.e., maximum error of 1%)
static constexpr double kMinErrorFraction = 1e-6;
double error_fraction() const;
void set_error_fraction(double error_fraction);
...
};
void S2BufferOperation::Options::set_error_fraction(double error_fraction) {
DCHECK_GE(error_fraction, kMinErrorFraction);
DCHECK_LE(error_fraction, 1.0);
error_fraction_ = max(kMinErrorFraction, min(1.0, error_fraction));
}
Invariants
Internal invariants should be enforced via DCHECK
. This is an important
aspect of both documentation and testing. For example, the following is a
private method that should only be called when certain conditions are true:
void S2BufferOperation::BufferEdgeAndVertex(const S2Point& a, const S2Point& b,
const S2Point& c) {
DCHECK_NE(a, b);
DCHECK_NE(b, c);
DCHECK_NE(buffer_sign_, 0);
if (!tracker_.ok()) return;
...
}
Programming errors within the S2 library should not be converted to runtime errors if at all possible.
S2Error
For historical reasons, S2 defines its own error class S2Error
consisting of
an error code and a status message. This class is used exclusively within the
library in preference to other error representations such as absl::Status
.
While it may be desirable to eventually convert the library to use a different
error representation, the overriding principle is to maintain consistency within
the library so that users (including external users of the open source version)
do not need to deal with two different error representations at once.
Here is a partial definition of the S2Error
class:
class S2Error {
public:
enum Code {
OK = 0, // No error.
////////////////////////////////////////////////////////////////////
// Generic errors, not specific to geometric objects:
UNKNOWN = 1000, // Unknown error.
UNIMPLEMENTED = 1001, // Operation is not implemented.
OUT_OF_RANGE = 1002, // Argument is out of range.
INVALID_ARGUMENT = 1003, // Invalid argument (other than a range error).
FAILED_PRECONDITION = 1004, // Object is not in the required state.
INTERNAL = 1005, // An internal invariant has failed.
DATA_LOSS = 1006, // Data loss or corruption.
RESOURCE_EXHAUSTED = 1007, // A resource has been exhausted.
CANCELLED = 1008, // Operation was cancelled.
...
};
S2Error() : code_(OK), text_() {}
// Convenience constructor that calls Init().
template <typename... Args>
S2Error(Code code, const absl::FormatSpec<Args...>& format,
const Args&... args);
}
// Set the error to the given code and printf-style message. Note that you
// can prepend text to an existing error by calling Init() more than once:
//
// error->Init(error->code(), "Loop %d: %s", j, error->text().c_str());
template <typename... Args>
void Init(Code code, const absl::FormatSpec<Args...>& format,
const Args&... args);
bool ok() const { return code_ == OK; }
Code code() const { return code_; }
std::string text() const { return text_; }
// Clear the error to contain the OK code and no error message.
void Clear();
private:
Code code_;
std::string text_;
};
Errors are typically initialized using S2Error::Init()
, although they can also
be constructed and copied. For example:
error->Init(S2Error::RESOURCE_EXHAUSTED,
"Memory limit exceeded (tracked usage %d bytes, limit %d bytes)",
usage_bytes_, limit_bytes_);
S2Error
is typically passed as a pointer parameter and is required to be
non-nullptr
. (Note that the current Google style recommendation is to pass such
parameters by reference, which precludes nullptr
arguments at the expense of
giving up the visible documentation hint that the argument may be modified.)
Public methods typically call error->Clear()
to remove any existing error,
whereas private methods either set the error or leave its state unchanged.
Methods that would otherwise return void
often return error->ok()
instead in
order to simplify client code. For example:
class S2BooleanOperation {
...
// Executes the given operation. Returns true on success, and otherwise
// sets "error" appropriately. (This class does not generate any errors
// itself, but the S2Builder::Layer might.)
bool Build(const S2ShapeIndex& a, const S2ShapeIndex& b, S2Error* error);
}
Client code might then look like this:
bool DoSomething(..., S2Error* error) {
...
if (!op.Build(a, b, error)) return false;
...
}
If a function has a void or other non-boolean result, the caller must check
error->ok()
explicitly:
void DoSomething(S2Builder::Layer* layer, const S2Builder::Graph& graph,
S2Error* error) {
layer->Build(graph, error);
if (!error->ok()) return;
...
}
Testing
Tests should attempt to achieve excellent coverage with a minimum of code. This will usually mean writing helper functions and/or test fixtures in order to avoid code duplication. Do not be afraid to write test code with loops, multiple levels of helpers, etc., if this will result in better testing with less code duplication.
S2 tests should always be run in a debug build (not the default “fast
build”) since this will give full line number information on all your stack
traces. In other words, build S2 with -c dbg
when you are testing new code.
Non-optimized builds also enable various internal consistency checks. This
includes not only the DCHECK
macros but also the --s2debug
flag which
enables certain internal validity checking. (This flag can also be enabled by
hand if you are suspect a problem with geometry that is too large or slow to
process using a debug build.)
Tests should focus on the functionality provided by your class, not the
functionality of the classes that your class depends on. For example, if your
class uses S2Builder
then you should not write tests whose purpose is simply
to verify the guarantees that S2Builder
makes (which are thoroughly tested
elsewhere).
Allow For Errors
Tests should generally allow for small amounts of error in their results rather than expecting an exact match. This helps to make tests less fragile, avoiding the need to update them whenever small changes to the underlying algorithms are made.
Useful Gunit helper functions include EXPECT_DOUBLE_EQ
and EXPECT_NEAR
.
It can also be useful to snap results to a fixed resolution using
s2builderutil::IntLatLngSnapFunction()
.
“Golden” files (which compare the results of a series of operations to a “golden” result deemed to be correct) are strictly prohibited.` Such tests are inherently fragile and will break whenever the smallest details of S2 algorithms are changed.
S2Testing
The header file s2testing.h
provides many utility functions and classes that
are useful when writing tests. In addition the distance-related functions
mentioned above, there is extensive support for generating random geometry of
various kinds.
Support code for writing tests can also be found in the following files:
thread_testing.h
s2shapeutil_testing.h
s2closest_edge_query_testing.h
S2Earth
S2 tests are not allowed to depend on s2earth.h
(which provides basic
functionality for converting distances on the Earth’s surface to S2-compatible
types). Instead tests should use the even more basic functionality provided in
s2testing.h
. For example:
S1Angle tolerance = S2Testing::MetersToAngle(1);
Randomization and Robustness
S2 makes heavy use of randomized tests. This is particularly useful as a way to test robustness guarantees. Typically, a specialized function is written to generate test cases that are very likely to trigger problems, and then a large number of cases are checked for correctness. Here is an example:
// Chooses a random S2Point that is often near the intersection of one of the
// coodinates planes or coordinate axes with the unit sphere. (It is possible
// to represent very small perturbations near such points.)
S2Point ChoosePoint() {
S2Point x = S2Testing::RandomPoint();
for (int i = 0; i < 3; ++i) {
if (S2Testing::rnd.OneIn(3)) {
x[i] *= pow(1e-50, S2Testing::rnd.RandDouble());
}
}
return x.Normalize();
}
TEST(S2, ProjectError) {
for (int iter = 0; iter < 1000; ++iter) {
SCOPED_TRACE(StrCat("Iteration ", iter));
S2Testing::rnd.Reset(iter); // Easier to reproduce a specific case.
S2Point a = ChoosePoint();
S2Point b = ChoosePoint();
S2Point n = S2::RobustCrossProd(a, b).Normalize();
S2Point x = S2Testing::SamplePoint(S2Cap(n, S1Angle::Radians(1e-15)));
S2Point p = S2::Project(x, a, b);
EXPECT_LT(s2pred::CompareEdgeDistance(
p, a, b, S1ChordAngle(S2::kProjectPerpendicularError)), 0);
}
}
Note the use of SCOPED_TRACE
so that any failing iterations can be identified.
This is obviously only useful if the random number seed is deterministic, which
is one reason that S2Testing
defines its own random number generator available
as S2Testing::rnd
. Tests should not use random number generators with
non-deterministic seeds, since this makes problems impossible to debug.
Also note the use of S2Testing::rnd.Reset(iter)
which allows a specific
failing test case to be reproduced more easily by simply replacing ‘iter’ by the
failing iteration number. (Note that most S2 code actually calls
rnd.Reset(iter + 1)
because it turns out that rnd.Reset(0)
and
rnd.Reset(1)
have the same effect.)
Observe the use of S2Testing
helper methods such as RandomPoint
and
SamplePoint
. Also note the use of pow(10**x, S2Testing::rnd.RandDouble())
,
which produces a value whose exponent is uniformly distributed between “x”
and 0. This is a useful technique for generating numbers that are
well-distributed over a wide range of magnitudes.