cauldron
helps to write property-based test cases
and abstract from specific examples.
The core definition is strategy (which is related to strategy pattern, but not the same): an object which encapsulates an algorithm for generating specific type of values.
Strategies can be modified
- by filtering out values which do not satisfy custom predicates,
- by transforming values with custom operators,
and combined
- united into one,
- passed as an argument to another strategy.
Let's build strategy which always generates true
value
#include <iostream>
#include <cauldron/just.h>
int main() {
cauldron::Just<bool> true_values(true);
bool true_value = true_values();
if (true_value) {
std::cout << "Hello, World!" << std::endl;
return 0;
} else {
std::cout << "Something went wrong!" << std::endl;
return 1;
};
}
Let's assume we're writing a program that works with vectors of numbers and we need to calculate Euclidean norm (aka magnitude) of a vector. Formula is
where is the vector space dimension.
Magnitude satisfies next ratios
- Magnitude of zero vector equals to zero
- Magnitude of non-zero vector is always positive
- Magnitude of vector with same coordinate equals to product of dimension's square root and coordinate modulus
Let's write test case using Catch
framework
which checks if our implementation of Euclidean norm computation satisfies this ratios.
- Consider that numbers have floating point type (e.g.
double
). - For checking triangle inequality we will overload vectors'
operator+
.
Our test case will look like
#define CATCH_CONFIG_MAIN
#include <algorithm>
#include <functional>
#include <vector>
#include <catch.hpp>
#include <cauldron/just.h>
#include <cauldron/integers.h>
#include <cauldron/floats.h>
#include <cauldron/vectors.h>
double magnitude(const std::vector<double> &vector);
std::vector<double> operator+(const std::vector<double> &vector,
const std::vector<double> &other_vector) {
assert(vector.size() == other_vector.size());
std::vector<double> result;
result.reserve(vector.size());
std::transform(vector.begin(), vector.end(),
other_vector.begin(),
std::back_inserter(result),
std::plus<double>());
return result;
}
TEST_CASE("Magnitude computation", "[magnitude]") {
/*
* we use `1` to make vectors non-empty,
* `100` is just an upper bound to start with
*/
cauldron::Integers<size_t> dimensions(1, 100);
size_t dimension = dimensions();
// considering all vectors have the same size
cauldron::Just<size_t> sizes(dimension);
// for now it generates values from `0.` to `1.`
cauldron::Floats<double> elements;
cauldron::Vectors<double> vectors(sizes,
elements);
SECTION("zero vector") {
std::vector<double> zero_vector(dimension, 0);
REQUIRE(magnitude(zero_vector) == 0.);
}
SECTION("non-zero vector") {
auto is_non_zero_number = [](double number) -> bool {
return number != 0;
};
cauldron::Requirement<std::vector<double>> is_non_zero_vector(
[&](const std::vector<double> &vector) -> bool {
return std::any_of(vector.begin(), vector.end(),
is_non_zero_number);
});
auto non_zero_vectors = vectors.filter(is_non_zero_vector);
std::vector<double> non_zero_vector = non_zero_vectors();
REQUIRE(magnitude(non_zero_vector) > 0.);
}
SECTION("triangle inequality") {
std::vector<double> vector = vectors();
std::vector<double> other_vector = vectors();
REQUIRE(magnitude(vector + other_vector)
<= magnitude(vector) + magnitude(other_vector));
}
SECTION("same value vector") {
double element = elements();
std::vector<double> same_value_vector(dimension, element);
REQUIRE(magnitude(same_value_vector) == sqrt(dimension) * fabs(element));
}
}
As we can see there is only declaration of magnitude
.
Straightforward definition would be
double magnitude(const std::vector<double> &vector) {
double result = 0.;
for (const double coordinate: vector) {
result += pow(coordinate, 2);
}
return sqrt(result);
}
But if we change elements
strategy to
cauldron::Just<double> elements(std::numeric_limits<double>::max()
/ sqrt(dimension));
last ratio will not be satisfied
since each coordinate squared will be greater than max possible double
value.
If we rewrite magnitude formula like
where
there will be no overflow.
So finally we can write
double magnitude(const std::vector<double> &vector) {
std::vector<double> coordinates_moduli;
coordinates_moduli.reserve(vector.size());
std::transform(vector.begin(), vector.end(),
std::back_inserter(coordinates_moduli),
[](double number) -> double { return fabs(number); });
double max_coordinate_modulus = *std::max_element(coordinates_moduli.begin(),
coordinates_moduli.end());
if (max_coordinate_modulus == 0.) {
return 0.;
}
double result = 0.;
for (const double coordinate: vector) {
result += pow(coordinate / max_coordinate_modulus, 2);
}
return max_coordinate_modulus * sqrt(result);
}
and make sure that all tests pass.
There are no releases yet.
Requirements:
git
.
Download the latest version from GitHub
repository
git clone https://github.com/lycantropos/cauldron.git
cd cauldron
Next instructions are executed from project's directory.
Requirements:
CMake 3.9+
,Clang 3.4+
orGCC 5.0+
(onLinux
systemsClang
will requirelibstdc++5
or newer).
Create build directory
mkdir build
cd build
If build directory already exists re-create it after removing
rm -r build
mkdir build
cd build
Build and install
cmake ..
make
make install
Requirements:
curl
,Catch 2.0.1+
single header,CMake 3.9+
,Clang 3.4+
orGCC 5.0+
(onLinux
systemsClang
will requirelibstdc++5
or newer).
Download Catch <https://github.com/catchorg/Catch2>
__ framework header
(may require sudo
)
cd /usr/local/include
curl -LJO https://github.com/catchorg/Catch2/releases/download/v${CATCH_VERSION}/catch.hpp
cd -
where CATCH_VERSION
is the Catch
framework version (e.g. 2.0.1
).
Create build directory
mkdir build
cd build
If build directory already exists re-create it after removing
rm -r build
mkdir build
cd build
Build with tests
mkdir -p build
cd build
cmake -DTESTS=ON ..
make
Run tests
./main
Requirements:
Run tests in Docker
containers
-
directly with
Docker Compose
docker-compose up
-
with
Bash
script (e.g. can be used inGit
hooks)./run-tests.sh
Requirements:
Choose which version number category to bump following semver specification
.
Test bumping version
bumpversion --dry-run --verbose $VERSION
where $VERSION
is the target version number category name,
possible values are patch
/minor
/major
.
Bump version
bumpversion --verbose $VERSION
To avoid inconsistency between branches and pull requests,
bumping version should be merged into master
branch as separate pull request.