Skip to content
This repository has been archived by the owner on Aug 19, 2020. It is now read-only.

lycantropos/cauldron

Repository files navigation

cauldron

Travis CI Codecov ReadTheDocs Licence

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.

Usage

Simple example

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;
  };
}

Complex example

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

magnitude

where n is the vector space dimension.

Magnitude satisfies next ratios

  1. Magnitude of zero vector equals to zero

zero vector

  1. Magnitude of non-zero vector is always positive

non-zero vector

  1. Triangle inequality

triangle inequality

  1. Magnitude of vector with same coordinate equals to product of dimension's square root and coordinate modulus

same value vector

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

magnitude

where

alpha

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.

Downloading

Release

There are no releases yet.

Developer

Requirements:

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.


Installation

Requirements:

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

Development

Running tests

Plain

Requirements:

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

Docker

Requirements:

Run tests in Docker containers

  • directly with Docker Compose

    docker-compose up
  • with Bash script (e.g. can be used in Git hooks)

    ./run-tests.sh

Bumping version

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.


About

Property-based testing inspired by hypothesis

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages