Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/src/Progmasoft/Catch3/Relations.hpp
blob: 423cb1205bf0718160b4f5dff00e7cdf7e2b266f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// Copyright (c) 2026 Progmasoft.
// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
#pragma once

#include <Progmasoft/Catch3/ForAll.hpp>
#include <Progmasoft/Catch3/Strategy/Context.hpp>
#include <functional>
#include <string>
#include <utility>

// Ready-made property bodies for the oracles that need no specification of
// the expected result: differential, metamorphic and round-trip testing.
// Each one is passed to ForAll in place of a hand-written body:
//
//     ForAll(Gen::Vectors(Gen::Integers<int>()),
//            Equivalent(ReferenceSort, FastSort));

namespace Progmasoft::Catch3
{

    /// Differential testing: the candidate must return what the reference
    /// returns for every input. A mismatch reports both results.
    template<typename Reference, typename Candidate>
    [[nodiscard]] auto
    Equivalent(Reference reference, Candidate candidate)
    {
        return [reference, candidate](const auto &...input) mutable {
            const auto expected = std::invoke(reference, input...);
            const auto actual = std::invoke(candidate, input...);
            if (!(expected == actual))
            {
                Note("reference: " + Detail::DescribeValue(expected));
                Note("candidate: " + Detail::DescribeValue(actual));
                Detail::RecordPropertyFailureMessage(
                    "the candidate disagrees with the reference");
            }
        };
    }

    /// Metamorphic testing: for every input x, relation(function(x),
    /// function(transform(x))) must hold, for example that sorting a
    /// shuffled list gives the same result, or that doubling a length
    /// doubles a cost.
    template<typename Function, typename Transform, typename Relation>
    [[nodiscard]] auto
    Metamorphic(Function function, Transform transform, Relation relation)
    {
        return [function, transform, relation](const auto &input) mutable {
            const auto source = std::invoke(function, input);
            const auto followUpInput = std::invoke(transform, input);
            const auto followUp = std::invoke(function, followUpInput);
            if (!static_cast<bool>(std::invoke(relation, source, followUp)))
            {
                Note("follow-up input: " + Detail::DescribeValue(followUpInput));
                Note("source output: " + Detail::DescribeValue(source));
                Note("follow-up output: " + Detail::DescribeValue(followUp));
                Detail::RecordPropertyFailureMessage(
                    "the metamorphic relation does not hold");
            }
        };
    }

    /// decode(encode(x)) == x for every input.
    template<typename Encode, typename Decode>
    [[nodiscard]] auto
    RoundTrip(Encode encode, Decode decode)
    {
        return [encode, decode](const auto &input) mutable {
            const auto encoded = std::invoke(encode, input);
            const auto decoded = std::invoke(decode, encoded);
            if (!(decoded == input))
            {
                Note("encoded: " + Detail::DescribeValue(encoded));
                Note("decoded: " + Detail::DescribeValue(decoded));
                Detail::RecordPropertyFailureMessage(
                    "decoding the encoded input did not give it back");
            }
        };
    }

    /// function(function(x)) == function(x) for every input.
    template<typename Function>
    [[nodiscard]] auto
    Idempotent(Function function)
    {
        return [function](const auto &input) mutable {
            const auto once = std::invoke(function, input);
            const auto twice = std::invoke(function, once);
            if (!(twice == once))
            {
                Note("once: " + Detail::DescribeValue(once));
                Note("twice: " + Detail::DescribeValue(twice));
                Detail::RecordPropertyFailureMessage(
                    "applying the function twice changed the result");
            }
        };
    }

} // namespace Progmasoft::Catch3