-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathexample_tests.zig
167 lines (141 loc) · 6.29 KB
/
example_tests.zig
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
const std = @import("std");
const marble = @import("marble");
const SinusTest = struct {
const tolerance = std.math.floatEps(f64) * 20;
/// This test has a single value, but you could also design the test to take an
/// array as input. The transformations, check and execute functions would then
/// loop through them all. Alternatively, the test can be run multiple times
/// with different inputs.
value: f64,
/// The mathematical property "sin(x) = sin(π − x)" must hold
pub fn transformPi(self: *SinusTest) void {
// If you flip this to + instead of - you'll observe how metamorphic tests fail.
self.value = std.math.pi - self.value;
}
/// Adding half the epsilon must still cause the relation to hold given the tolerance
pub fn transformEpsilon(self: *SinusTest) void {
self.value = self.value + std.math.floatEps(f64) / 2.0;
}
/// A metamorphic relation is a relation between outputs in different executions.
/// This relation must hold after every execution of transformation combinations.
pub fn check(_: *SinusTest, original_output: f64, transformed_output: f64) bool {
return std.math.approxEqAbs(f64, original_output, transformed_output, tolerance);
}
/// Called initially to compute the baseline output, and after every transformation combination
pub fn execute(self: *SinusTest) f64 {
return std.math.sin(self.value);
}
};
test "sinus" {
var i: f64 = 1;
while (i < 100) : (i += 1) {
var t = SinusTest{ .value = i };
try std.testing.expect(try marble.run(SinusTest, &t, .{}));
}
}
/// Input to a query
const Query = struct {
term: []const u8 = "test",
ascending: bool = false,
page_size: usize = 50,
};
// This is an example of a "conditional relations" test where the metamorphic relationship
// depends on which transformations are applied in the current combination.
const QueryTest = struct {
value: Query,
additional_term: bool = false,
/// Reset the additional_term flag before each combination. It will be
/// flipped on for combinations including `transformAdditionalTerm`.
pub fn before(self: *QueryTest, phase: marble.Phase) void {
if (phase == .Combination) self.additional_term = false;
}
/// Sorting shouldn't affect total count
pub fn transformSort(self: *QueryTest) void {
self.value.ascending = true;
}
/// Page count shouldn't affect total count
pub fn transformPageCount(self: *QueryTest) void {
self.value.page_size = 25;
}
/// Our search engine is case insensitive
pub fn transformCase(self: *QueryTest) void {
self.value.term = "TEST";
}
/// Another term reduces the number of hits
pub fn transformAdditionalTerm(self: *QueryTest) void {
self.additional_term = true;
self.value.term = "test another";
}
/// Number of total hits shouldn't change when changing sort order, page count and casing.
/// However, we do expect multiple search terms to reduce the number of hits.
/// These are two metamorphic relations, one of which is checked conditionally.
pub fn check(self: *QueryTest, untransformed_hits: usize, hits_after_transformations: usize) bool {
if (self.additional_term) return untransformed_hits >= hits_after_transformations;
return untransformed_hits == hits_after_transformations;
}
/// Execute the query, returning the total number of hits. A real-world test could do a mocked REST call.
pub fn execute(self: *QueryTest) usize {
// Emulate fewer hits when additional search terms are added
return if (self.additional_term) 50 else 100;
}
};
test "query" {
var query_test = QueryTest{ .value = .{} };
try std.testing.expect(try marble.run(QueryTest, &query_test, .{ .skip_combinations = false, .verbose = false }));
}
/// Test some metamorphic relations of binary search
/// MT relations courtesy of @jacobdweightman
const BinarySearchTest = struct {
const S = struct {
fn order(context: void, lhs: usize, rhs: usize) std.math.Order {
_ = context;
return std.math.order(lhs, rhs);
}
};
/// The value is the binary search result qindex
value: ?usize = undefined,
arr: []const usize = undefined,
testing_accidental_insert: bool = undefined,
pub fn before(self: *BinarySearchTest, phase: marble.Phase) void {
if (phase == .Combination) {
self.testing_accidental_insert = false;
}
}
/// Test that basic relations hold:
/// if x = A[k], then binarySearch(x, A) = k
pub fn transformSimple(self: *BinarySearchTest) void {
const x = self.arr[self.value.?];
self.value = std.sort.binarySearch(usize, x, self.arr, {}, S.order);
}
// This transform will catch an error where the value being searched for is
// accidentally being inserted into the array:
// if A[k-1] < x < A[k+1] and x != A[k], then binarySearch(x, A) = -1
pub fn transformAccidentalInsert(self: *BinarySearchTest) void {
self.testing_accidental_insert = true;
if (self.value.? == 0 or self.value.? + 1 >= self.arr.len) return;
var x = self.arr[self.value.? - 1] + 1;
if (x == self.arr[self.value.?]) x += 1;
if (x >= self.arr[self.value.? + 1]) return;
self.value = std.sort.binarySearch(usize, x, self.arr, {}, S.order);
}
/// Test binary search array splitting correctness:
// if x = A[k], then binarySearch(A[k-1], A) = k-1 and binarySearch(A[k+1], A) = k + 1
pub fn transformSplitting(self: *BinarySearchTest) void {
const x = self.arr[self.value.?];
self.value = std.sort.binarySearch(usize, x, self.arr, {}, S.order);
}
pub fn check(self: *BinarySearchTest, org: ?usize, new: ?usize) bool {
return (new == null and self.testing_accidental_insert) or org.? == new.?;
}
pub fn execute(self: *BinarySearchTest) ?usize {
return self.value;
}
};
test "std.sort.binarySearch" {
const array: []const usize = &.{ 4, 6, 10, 15, 18, 25, 40 };
var i: usize = 0;
while (i < array.len) : (i += 1) {
var bs_test = BinarySearchTest{ .value = i, .arr = array };
try std.testing.expect(try marble.run(BinarySearchTest, &bs_test, .{ .skip_combinations = true }));
}
}