From b49caa3ec9d4c95c74a1fe5669a9928416492c7e Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 20 Oct 2023 02:59:36 -0700 Subject: [PATCH] Cleaning up and documenting scmp code. --- .circleci/config.yml | 18 ++-- CMakeLists.txt | 2 + include/scmp/Math.h | 68 +++++++++------ include/scmp/filter/Madgwick.h | 6 -- include/scmp/geom.h | 43 ++++++++++ include/scmp/geom/Coord2D.h | 115 +++++++++++++------------ include/scmp/scmp.h | 7 +- include/sctest/Checks.h | 10 ++- src/scmp/Coord2D.cc | 5 ++ src/test/SimpleSuite.cc | 2 +- test/coord2d.cc | 23 ++++- test/math.cc | 152 +++++++++++++++++++++++++++++++++ 12 files changed, 343 insertions(+), 108 deletions(-) create mode 100644 include/scmp/geom.h create mode 100644 test/math.cc diff --git a/.circleci/config.yml b/.circleci/config.yml index cceed2e..9224a25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,35 +1,27 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/configuration-reference version: 2.1 -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/configuration-reference/#jobs jobs: ctest: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/configuration-reference/#executor-job docker: - image: git.wntrmute.dev/sc/dev:alpine - # Add steps to the job - # See: https://circleci.com/docs/configuration-reference/#steps steps: - checkout - run: name: Setup cmake build command: setup-cmake.sh - static_analysis: docker: - image: git.wntrmute.dev/sc/dev:alpine steps: - checkout - run: - - name: trunk check - command: ./trunk check + name: Trunk check + command: ./trunk check -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/configuration-reference/#workflows workflows: ctest: jobs: - ctest + static_analysis: + jobs: + - static_analysis diff --git a/CMakeLists.txt b/CMakeLists.txt index 4eaf121..70d7f20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ set(HEADER_FILES include/scsl/StringUtil.h include/scsl/TLV.h + include/scmp/geom.h include/scmp/scmp.h include/scmp/Math.h include/scmp/Motion2D.h @@ -116,6 +117,7 @@ generate_test(stringutil) # math and physics generate_test(coord2d) generate_test(madgwick) +generate_test(math) generate_test(orientation) generate_test(quaternion) generate_test(vector) diff --git a/include/scmp/Math.h b/include/scmp/Math.h index 83bc936..1ac3a7f 100644 --- a/include/scmp/Math.h +++ b/include/scmp/Math.h @@ -31,11 +31,10 @@ namespace scmp { -/// MAX_RADIAN is a precomputed 2 * M_PI. and MIN_RADIAN is -2 * M_PI. +/// MAX_RADIAN is a precomputed 2 * M_PI. constexpr double MAX_RADIAN = 2 * M_PI; constexpr double MIN_RADIAN = -2 * M_PI; -constexpr double POS_HALF_RADIAN = M_PI / 2; -constexpr double NEG_HALF_RADIAN = -(M_PI / 2); +constexpr double PI_D = 3.141592653589793; /// Roll m die of n sides, returning a vector of the dice. @@ -48,50 +47,65 @@ int DieTotal(int m, int n); int BestDie(int k, int m, int n); -/// Convert radians to degrees. -/// @param rads the angle in radians -/// @return the angle in degrees. +/// \brief Convert radians to degrees. +/// +/// \param rads the angle in radians +/// \return the angle in degrees. float RadiansToDegreesF(float rads); -/// Convert radians to degrees. -/// @param rads the angle in radians -/// @return the angle in degrees. +/// \brief Convert radians to degrees. +/// +/// \param rads the angle in radians +/// \return the angle in degrees. double RadiansToDegreesD(double rads); -/// Convert degrees to radians. -/// @param degrees the angle in degrees -/// @return the angle in radians. +/// \brief Convert degrees to radians. +/// +/// \param degrees the angle in degrees +/// \return the angle in radians. float DegreesToRadiansF(float degrees); -/// Convert degrees to radians. -/// @param degrees the angle in degrees -/// @return the angle in radians. +/// \brief Convert degrees to radians. +/// +/// \param degrees the angle in degrees +/// \return the angle in radians. double DegreesToRadiansD(double degrees); -/// RotateRadians rotates theta0 by theta1 radians, wrapping the result to -/// MIN_RADIAN <= result <= MAX_RADIAN. +/// \brief RotateRadians rotates theta0 by theta1 radians, wrapping +/// the result to MIN_RADIAN <= result <= MAX_RADIAN. +/// +/// \param theta0 +/// \param theta1 +/// \return double RotateRadians(double theta0, double theta1); -/// Get the default epsilon value. -/// @param epsilon The variable to store the epsilon value in. +/// \brief Get the default epsilon value. +/// +/// \param epsilon The variable to store the epsilon value in. void DefaultEpsilon(double &epsilon); /// Get the default epsilon value. -/// @param epsilon The variable to store the epsilon value in. +/// +/// \param epsilon The variable to store the epsilon value in. void DefaultEpsilon(float &epsilon); -/// Return whether the two values of type T are equal to within some tolerance. -/// @tparam T The type of value -/// @param a A value of type T used as the left-hand side of an equality check. -/// @param b A value of type T used as the right-hand side of an equality check. -/// @param epsilon The tolerance value. -/// @return Whether the two values are "close enough" to be considered equal. +/// \brief Return whether the two values of type T are equal to within +/// some tolerance. +/// +/// \tparam T The type of value +/// \param a A value of type T used as the left-hand side of an +/// equality check. +/// \param b A value of type T used as the right-hand side of an +/// equality check. +/// \param epsilon The tolerance value. +/// \return Whether the two values are "close enough" to be considered +/// equal. template static T WithinTolerance(T a, T b, T epsilon) { - return std::abs(a - b) < epsilon; + return std::abs(a - b) <= epsilon; } diff --git a/include/scmp/filter/Madgwick.h b/include/scmp/filter/Madgwick.h index c924a04..b10208b 100644 --- a/include/scmp/filter/Madgwick.h +++ b/include/scmp/filter/Madgwick.h @@ -55,7 +55,6 @@ public: Madgwick() : deltaT(0.0), previousSensorFrame(), sensorFrame() {}; - /// \brief The Madgwick filter is initialised with a sensor frame. /// /// \param sf A sensor frame; if zero, the sensor frame will be @@ -67,7 +66,6 @@ public: } } - /// \brief Initialise the filter with a sensor frame quaternion. /// /// \param sf A quaternion representing the current Orientation. @@ -75,7 +73,6 @@ public: deltaT(0.0), previousSensorFrame(), sensorFrame(sf) {}; - /// \brief Return the current orientation as measured by the /// filter. /// @@ -86,7 +83,6 @@ public: return this->sensorFrame; } - /// \brief Return the filter's rate of angular change from a /// sensor frame. /// @@ -152,7 +148,6 @@ public: this->UpdateFrame(this->sensorFrame + q, delta); } - /// \brief Update the sensor frame with a gyroscope reading. /// /// If no Δt is provided, the filter's default is used. @@ -168,7 +163,6 @@ public: this->UpdateAngularOrientation(gyro, this->deltaT); } - /// \brief Retrieve a vector of the Euler angles in ZYX Orientation. /// /// \return A vector of Euler angles as <ψ, θ, ϕ>. diff --git a/include/scmp/geom.h b/include/scmp/geom.h new file mode 100644 index 0000000..91bf580 --- /dev/null +++ b/include/scmp/geom.h @@ -0,0 +1,43 @@ +/// +/// \file include/scmp/geom.h +/// \author K. Isom +/// \date 2023-10-20 +/// \brief 2D point and polar coordinate systems. +/// +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + +#ifndef SCSL_GEOM_H +#define SCSL_GEOM_H + + +#include +#include +#include +#include + + +namespace scmp { + + +/// \brief Geometry-related code. +namespace geom {} + + +} // namespace scmp + + +#endif // SCSL_GEOM_H diff --git a/include/scmp/geom/Coord2D.h b/include/scmp/geom/Coord2D.h index f0cf251..7916b0a 100755 --- a/include/scmp/geom/Coord2D.h +++ b/include/scmp/geom/Coord2D.h @@ -1,29 +1,29 @@ -/// coord2d.h defines 2D point and polar coordinate systems. -// -// Project: scccl -// File: include/math/coord2d.h -// Author: Kyle Isom -// Date: 2017-06-05 -// Namespace: math::geom -// -// coord2d.h defines 2D coordinate classes and functions. -// -// Copyright 2017 Kyle Isom -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/// +/// \file include/scmp/geom/Coord2D.h +/// \author K. Isom +/// \date 2017-06-05 +/// \brief 2D point and polar coordinate systems. +/// +/// Copyright 2017 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + #ifndef SCMATH_GEOM_COORD2D_H #define SCMATH_GEOM_COORD2D_H + #include #include #include @@ -34,72 +34,81 @@ namespace geom { class Point2D; + class Polar2D; -// Point2D is a logical grouping of a set of 2D cartesian coordinates. +/// \brief Point2D is a logical grouping of a set of 2D cartesian +/// coordinates. class Point2D { - public: - int x, y; +public: + int x, y; - // A Point2D can be initialised by setting its members to 0, by providing the - // x and y coordiantes, or through translation from a polar coordinate. - Point2D() : x(0), y(0) {} - Point2D(int _x, int _y) : x(_x), y(_y) {} - Point2D(const Polar2D&); + // A Point2D can be initialised by setting its members to 0, by providing the + // x and y coordiantes, or through translation from a polar coordinate. + Point2D(); + Point2D(int _x, int _y); + Point2D(const Polar2D &); - std::string ToString(void); - void ToPolar(Polar2D&); + std::string ToString(); + void ToPolar(Polar2D &); // Rotate rotates the point by theta radians. Alternatively, a rotation // can use this point as the centre, with a polar coordinate and a rotation // amount (in radians). The latter is used to specify a central point // of rotation with vertices specified as polar coordinates from the centre. // Both forms take a reference to a Point2D to store the rotated point. - void Rotate(Point2D& rotated, double theta); + void Rotate(Point2D &rotated, double theta); std::vector Rotate(std::vector, double); // Translate adds this point to the first argument, storing the result in the // second argument. - void Translate(const Point2D& other, Point2D& translated); + void Translate(const Point2D &other, Point2D &translated); // Distance returns the distance from this point to another. - int Distance(const Point2D& other); + int Distance(const Point2D &other); - Point2D operator+(const Point2D &rhs) const { return Point2D(x + rhs.x, y + rhs.y); } - Point2D operator-(const Point2D &rhs) const { return Point2D(x - rhs.x, y - rhs.y); } - Point2D operator*(const int k) const { return Point2D(x * k, y * k); } - bool operator==(const Point2D& rhs) const; - bool operator!=(const Point2D& rhs) const { return !(*this == rhs); } - friend std::ostream& operator<<(std::ostream& outs, const Point2D& pt); + Point2D operator+(const Point2D &rhs) const + { return Point2D(x + rhs.x, y + rhs.y); } + Point2D operator-(const Point2D &rhs) const + { return Point2D(x - rhs.x, y - rhs.y); } + Point2D operator*(const int k) const + { return Point2D(x * k, y * k); } + bool operator==(const Point2D &rhs) const; + bool operator!=(const Point2D &rhs) const + { return !(*this == rhs); } + friend std::ostream &operator<<(std::ostream &outs, const Point2D &pt); }; // A Polar2D is a 2D polar coordinate, specified in terms of the radius from // some origin and the angle from the positive X axis of a cartesian coordinate // system. class Polar2D { - public: - double r, theta; +public: + double r, theta; // A Polar2D can be initialised as a zeroised polar coordinate, by specifying // the radius and angle directly, or via conversion from a Point2D. - Polar2D() : r(0.0), theta(0.0) {} - Polar2D(double _r, double _theta) : r(_r), theta(_theta) {} - Polar2D(const Point2D&); + Polar2D() : r(0.0), theta(0.0) + {} + Polar2D(double _r, double _theta) : r(_r), theta(_theta) + {} + Polar2D(const Point2D &); std::string ToString(); - void ToPoint(Point2D&); + void ToPoint(Point2D &); // Rotate rotates the polar coordinate by the number of radians, storing the result // in the Polar2D argument. - void Rotate(Polar2D&, double); + void Rotate(Polar2D &, double); // RotateAround rotates this point about by theta radians, storing the rotated point // in result. - void RotateAround(const Point2D& other, Point2D& result, double tjeta); + void RotateAround(const Point2D &other, Point2D &result, double tjeta); - bool operator==(const Polar2D&) const; - bool operator!=(const Polar2D& rhs) const { return !(*this == rhs); } - friend std::ostream& operator<<(std::ostream&, const Polar2D&); + bool operator==(const Polar2D &) const; + bool operator!=(const Polar2D &rhs) const + { return !(*this == rhs); } + friend std::ostream &operator<<(std::ostream &, const Polar2D &); }; diff --git a/include/scmp/scmp.h b/include/scmp/scmp.h index 68a2ee7..906425a 100644 --- a/include/scmp/scmp.h +++ b/include/scmp/scmp.h @@ -25,9 +25,10 @@ /// \brief Shimmering Clarity Math & Physics toolkit. -namespace scmp { - -} // namespace scmp +/// +/// The Shimmering Clarity contains code related to math and physics, +/// particularly as relevant to game programming and robotics. +namespace scmp {} #endif //SCSL_SCMP_H diff --git a/include/sctest/Checks.h b/include/sctest/Checks.h index 1d76c2d..c8d998d 100755 --- a/include/sctest/Checks.h +++ b/include/sctest/Checks.h @@ -35,11 +35,17 @@ namespace sctest { #define SCTEST_CHECK_FALSE(x) if ((x)) { return false; } #define SCTEST_CHECK_EQ(x, y) if ((x) != (y)) { return false; } #define SCTEST_CHECK_NE(x, y) if ((x) == (y)) { return false; } +#define SCTEST_CHECK_GEQ(x, y) if ((x) < (y)) { return false; } +#define SCTEST_CHECK_LEQ(x, y) if ((x) > (y)) { return false; } #define SCTEST_CHECK_FEQ(x, y) { float eps; scmp::DefaultEpsilon(eps); if (!scmp::WithinTolerance((x), (y), eps)) { return false; }} #define SCTEST_CHECK_DEQ(x, y) { double eps; scmp::DefaultEpsilon(eps); if (!scmp::WithinTolerance((x), (y), eps)) { return false; }} -#define SCTEST_CHECK_FEQ_EPS(x, y, eps) { if (!scmp::WithinTolerance((x), (y), eps)) { return false; }} -#define SCTEST_CHECK_DEQ_EPS(x, y, eps) { if (!scmp::WithinTolerance((x), (y), eps)) { return false; }} +#define SCTEST_CHECK_FEQ_EPS(x, y, eps) { if (!scmp::WithinTolerance((x), (y), (eps))) { return false; }} +#define SCTEST_CHECK_FNE_EPS(x, y, eps) { if (scmp::WithinTolerance((x), (y), (eps))) { return false; }} + +#define SCTEST_CHECK_DEQ_EPS(x, y, eps) { if (!scmp::WithinTolerance((x), (y), (eps))) { return false; }} +#define SCTEST_CHECK_DNE_EPS(x, y, eps) { if (scmp::WithinTolerance((x), (y), (eps))) { return false; }} + } // namespace sctest diff --git a/src/scmp/Coord2D.cc b/src/scmp/Coord2D.cc index cbe2f41..f859d16 100755 --- a/src/scmp/Coord2D.cc +++ b/src/scmp/Coord2D.cc @@ -42,6 +42,11 @@ namespace geom { // // Point2D + +Point2D::Point2D() : x(0), y(0) {} + +Point2D::Point2D(int _x, int _y) : x(_x), y(_y) {} + Point2D::Point2D(const Polar2D &pol) : x(std::rint(std::cos(pol.theta) * pol.r)), y(std::rint(std::sin(pol.theta) * pol.r)) {} diff --git a/src/test/SimpleSuite.cc b/src/test/SimpleSuite.cc index 5924a20..6e0e6fd 100755 --- a/src/test/SimpleSuite.cc +++ b/src/test/SimpleSuite.cc @@ -61,7 +61,7 @@ SimpleSuite::AddTest(std::string name, std::function test) void SimpleSuite::AddFailingTest(std::string name, std::function test) { - const UnitTest test_case = {std::move(name), test, false}; + const UnitTest test_case = {std::move(name), std::move(test), false}; tests.push_back(test_case); } diff --git a/test/coord2d.cc b/test/coord2d.cc index 40a2638..59de98b 100755 --- a/test/coord2d.cc +++ b/test/coord2d.cc @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -222,9 +223,25 @@ geomRotatePointsAboutOrigin() int -main() +main(int argc, char *argv[]) { + auto quiet = false; + auto flags = new scsl::Flags("test_orientation", + "This test validates various orientation-related components in scmp."); + flags->Register("-q", false, "suppress test output"); + + auto parsed = flags->Parse(argc, argv); + if (parsed != scsl::Flags::ParseStatus::OK) { + std::cerr << "Failed to parse flags: " + << scsl::Flags::ParseStatusToString(parsed) << "\n"; + } + SimpleSuite suite; + flags->GetBool("-q", quiet); + if (quiet) { + suite.Silence(); + } + suite.AddTest("geomValidateAngularRotation", geomValidateAngularRotation); suite.AddTest("geomConversionIdentities", geomConversionIdentities); suite.AddTest("geomVerifyBasicProperties", geomVerifyBasicProperties); @@ -232,8 +249,8 @@ main() suite.AddTest("geomRotatePoint2D", geomRotatePoint2D); suite.AddTest("geomRotatePointsAboutOrigin", geomRotatePointsAboutOrigin); + delete flags; auto result = suite.Run(); - std::cout << suite << "\n"; - + std::cout << suite.GetReport() << "\n"; return result ? 0 : 1; } diff --git a/test/math.cc b/test/math.cc new file mode 100644 index 0000000..96a1c1d --- /dev/null +++ b/test/math.cc @@ -0,0 +1,152 @@ +/// +/// \file test/math.cc +/// \author K. Isom +/// \date 2023-10-20 +/// \brief Unit tests for math functions. +/// +/// Arena defines a memory management backend for pre-allocating memory. +/// +/// Copyright 2023 K. Isom +/// +/// Permission to use, copy, modify, and/or distribute this software for +/// any purpose with or without fee is hereby granted, provided that +/// the above copyright notice and this permission notice appear in all /// copies. +/// +/// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +/// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +/// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +/// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +/// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +/// OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +/// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +/// PERFORMANCE OF THIS SOFTWARE. +/// + +#include + +#include +#include +#include +#include + + +namespace { + + +bool +BestDie() +{ + // Theoretically, this could fail. The odds of that happening + // with 100 die and a proper RNG is 0.99999998792533, + // practically 100%. At 1000 die, it's virtually guaranteed. + auto n = scmp::BestDie(1000, 6, 1); + + SCTEST_CHECK_EQ(n, 6); + + return true; +} + + +bool +DieTotal() +{ + auto n = scmp::DieTotal(100, 6); + + SCTEST_CHECK_GEQ(n, 100); + SCTEST_CHECK_LEQ(n, 600); + + return true; +} + + +bool +WithinToleranceFloat() +{ + float x = 0.1235; + float y = 0.1236; + float eps = 0.0; + float expected = 0.1234; + + scmp::DefaultEpsilon(eps); + + SCTEST_CHECK_FEQ_EPS(x, expected, eps); + SCTEST_CHECK_FNE_EPS(y, expected, eps); + + return true; +} + + +bool +WithinToleranceDouble() +{ + double x = 0.12348; + double y = 0.1236; + double eps = 0.0; + double expected = 0.12345; + + scmp::DefaultEpsilon(eps); + + SCTEST_CHECK_DEQ_EPS(x, expected, eps); + SCTEST_CHECK_DNE_EPS(y, expected, eps); + + return true; +} + + +bool +RotateRadians() +{ + double theta0 = 0.0; + double theta1 = scmp::PI_D; + + auto rotated = scmp::RotateRadians(theta0, theta1); + SCTEST_CHECK_DEQ(rotated, theta1); + + rotated = scmp::RotateRadians(rotated, theta1); + SCTEST_CHECK_DEQ(rotated, theta0); + + theta1 = scmp::PI_D * 3 / 2; + rotated = scmp::RotateRadians(theta0, theta1); + SCTEST_CHECK_DEQ(rotated, -scmp::PI_D / 2); + + rotated = scmp::RotateRadians(rotated, theta1); + SCTEST_CHECK_DEQ(rotated, scmp::PI_D); + + return true; +} + + +} // anonymous namespace + + +int +main(int argc, char *argv[]) +{ + auto quiet = false; + auto flags = new scsl::Flags("test_orientation", + "This test validates various orientation-related components in scmp."); + flags->Register("-q", false, "suppress test output"); + + auto parsed = flags->Parse(argc, argv); + if (parsed != scsl::Flags::ParseStatus::OK) { + std::cerr << "Failed to parse flags: " + << scsl::Flags::ParseStatusToString(parsed) << "\n"; + } + + sctest::SimpleSuite suite; + flags->GetBool("-q", quiet); + if (quiet) { + suite.Silence(); + } + + suite.AddTest("BestDie", BestDie); + suite.AddTest("DieTotal", DieTotal); + suite.AddTest("WithinToleranceFloat", WithinToleranceFloat); + suite.AddTest("WithinToleranceDouble", WithinToleranceDouble); + suite.AddTest("RotateRadians", RotateRadians); + + delete flags; + auto result = suite.Run(); + std::cout << suite.GetReport() << "\n"; + return result ? 0 : 1; +}