GCC Code Coverage Report


Directory: include/tts/
File: include/tts/test/ranges.hpp
Date: 2026-10-14 16:28:14
Exec Total Coverage
Lines: 99 99 100.0%
Functions: 22 22 100.0%
Branches: 56 62 90.3%

Line Branch Exec Source
1 //======================================================================================================================
2 /// @file
3 /*
4 TTS - Tiny Test System
5 Copyright : TTS Contributors & Maintainers
6 SPDX-License-Identifier: BSL-1.0
7 */
8 //======================================================================================================================
9 #pragma once
10
11 namespace tts
12 {
13 template<typename Base> struct adapter
14 {
15 20480 template<typename U, typename Func> static void run(Base const*& src, U*& dst, Func f) noexcept
16 {
17 20480 *dst++ = f(*src++);
18 20480 }
19 9 static auto retrieve(Base const* src) noexcept
20 {
21 9 return *src;
22 }
23 9 static void display(Base const& v) noexcept
24 {
25 9 output().write(as_text(v));
26 9 }
27 };
28
29 namespace _
30 {
31 template<typename Input, typename Output> struct histogram_entry
32 {
33 bool status;
34 Input input_value;
35 Output found_value, ref_value;
36 };
37
38 template<typename Type, typename In, typename Out, typename Func>
39 14 void compute(In const& inputs, Out& outputs, Func fn)
40 {
41 14 auto in = inputs.data();
42 14 auto end = inputs.data() + inputs.size();
43 14 auto out = outputs.data();
44
2/2
✓ Branch 0 taken 23552 times.
✓ Branch 1 taken 8 times.
38926 while(in != end)
45 38912 adapter<Type>::run(in, out, fn);
46 14 }
47
48 16384 inline std::size_t last_bucket_less(std::size_t nb_buckets, double ulp) noexcept
49 {
50 4844 auto next2 = [](double x)
51 {
52 4844 auto v = static_cast<std::size_t>(_::ceil(x));
53 4844 v--;
54 4844 v |= v >> 1;
55 4844 v |= v >> 2;
56 4844 v |= v >> 4;
57 4844 v |= v >> 8;
58 4844 v |= v >> 16;
59 4844 v++;
60 4844 return v;
61 };
62
63 std::size_t bucket;
64
2/2
✓ Branch 0 taken 11540 times.
✓ Branch 1 taken 4844 times.
16384 if(ulp <= 1.5) bucket = static_cast<std::size_t>(_::ceil(ulp * 2));
65
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4844 times.
4844 else if(_::is_inf(ulp)) bucket = nb_buckets - 1;
66 4844 else bucket = _::min(nb_buckets - 2, static_cast<std::size_t>(_::log2(next2(ulp)) + 4));
67 16384 return bucket;
68 }
69
70 4 template<typename... S> void header(S const&... s)
71 {
72
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
4 if(::tts::is_quiet()) return;
73
4/4
✓ Branch 2 taken 4 times.
✓ Branch 6 taken 4 times.
✓ Branch 10 taken 4 times.
✓ Branch 14 taken 4 times.
4 ((::tts::output().write("%-*s", 16, s)), ...);
74
1/1
✓ Branch 3 taken 4 times.
4 ::tts::output().writeln();
75 }
76
77 template<typename U, typename R, typename V>
78 171 void results(U ulp, unsigned int count, R ratio, auto desc, V const& v)
79 {
80
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 90 times.
171 assert(desc && "Description cannot be null");
81
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 90 times.
171 if(::tts::is_quiet()) return;
82
2/2
✓ Branch 0 taken 30 times.
✓ Branch 1 taken 60 times.
171 if(ulp != -1) ::tts::output().write("%-16.1f%-16u%-16g%s", ulp, count, ratio, desc);
83
1/1
✓ Branch 2 taken 60 times.
114 else ::tts::output().write("%*s", static_cast<int>(48 + strlen(desc)), desc); // NOSONAR
84 171 adapter<V>::display(v);
85
1/1
✓ Branch 3 taken 90 times.
171 ::tts::output().writeln();
86 }
87
88 7 template<typename P> void print_producer(P const& prod, auto alt)
89 {
90
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
7 if(::tts::is_quiet()) return;
91
2/2
✓ Branch 2 taken 1 times.
✓ Branch 5 taken 1 times.
2 if constexpr(::tts::_::described<P>) ::tts::output().writeln(::tts::as_text(prod));
92 5 else ::tts::output().writeln(alt);
93 }
94 }
95
96 template<typename RefType, typename NewType, typename Generator, typename RefFun, typename NewFun>
97 7 double ulp_histogram(Generator g, RefFun reference, NewFun challenger)
98 {
99 using out_type = std::decay_t<std::invoke_result_t<RefFun, RefType>>;
100 using nout_type = std::decay_t<std::invoke_result_t<NewFun, NewType>>;
101
102 //-- Find how many elements in a block
103
1/1
✓ Branch 2 taken 4 times.
7 std::size_t count = ::tts::arguments().value(std::size_t {4096}, "--block");
104
105 //-- Prepare blocks
106
2/2
✓ Branch 1 taken 4 times.
✓ Branch 4 taken 4 times.
7 buffer<out_type> ref_out(count), new_out(count);
107
1/1
✓ Branch 1 taken 4 times.
7 buffer<RefType> inputs(count);
108
109
2/2
✓ Branch 1 taken 16384 times.
✓ Branch 2 taken 4 times.
28679 for(std::size_t i = 0; i < inputs.size(); ++i)
110
1/1
✓ Branch 1 taken 12288 times.
28672 inputs[ i ] = ::tts::produce(type<RefType> {}, g, i, count);
111
112
1/1
✓ Branch 2 taken 4 times.
7 std::size_t repetition = ::tts::arguments().value(std::size_t {1}, "--loop");
113
114 7 double max_ulp = 0.;
115 7 std::size_t nb_buckets = 2 + 1 + 16;
116 7 std::size_t nb_ulps = 0;
117
1/1
✓ Branch 1 taken 4 times.
7 buffer<unsigned int> ulp_map(nb_buckets, 0);
118
119
1/1
✓ Branch 1 taken 4 times.
7 buffer<_::histogram_entry<NewType, nout_type>> samples(nb_buckets, {false, {}, {}, {}});
120
121
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
14 for(std::size_t r = 0; r < repetition; ++r)
122 {
123 //--- Compute reference & challenger outputs
124
1/1
✓ Branch 1 taken 4 times.
7 _::compute<RefType>(inputs, ref_out, reference);
125
1/1
✓ Branch 1 taken 4 times.
7 _::compute<NewType>(inputs, new_out, challenger);
126
127 //--- Compute ulp distances
128
1/1
✓ Branch 1 taken 4 times.
7 buffer<double> ulpdists(count);
129
130
2/2
✓ Branch 1 taken 16384 times.
✓ Branch 2 taken 4 times.
28679 for(std::size_t i = 0; i < ulpdists.size(); ++i)
131 {
132 28672 nb_ulps++;
133
1/1
✓ Branch 3 taken 16384 times.
28672 ulpdists[ i ] = ::tts::ulp_check(ref_out[ i ], new_out[ i ]);
134 28672 max_ulp = _::max(max_ulp, ulpdists[ i ]);
135
136 28672 auto idx = _::last_bucket_less(nb_buckets, ulpdists[ i ]);
137 28672 ulp_map[ idx ]++;
138
139
2/2
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 16354 times.
28672 if(!samples[ idx ].status)
140 {
141 114 samples[ idx ] = {true,
142 57 adapter<NewType>::retrieve(&inputs[ i ]),
143 57 adapter<nout_type>::retrieve(&new_out[ i ]),
144 57 adapter<nout_type>::retrieve(&ref_out[ i ])};
145 }
146 }
147 }
148
149
1/1
✓ Branch 1 taken 4 times.
7 _::header("Max ULP", "Count (#)", "Ratio Sum (%)", "Samples");
150
1/1
✓ Branch 2 taken 4 times.
7 _::separator(!::tts::is_quiet());
151
152 7 double ratio = 0.;
153
154
2/2
✓ Branch 1 taken 76 times.
✓ Branch 2 taken 4 times.
140 for(std::size_t i = 0; i < ulp_map.size(); ++i)
155 {
156
2/2
✓ Branch 1 taken 30 times.
✓ Branch 2 taken 46 times.
133 if(ulp_map[ i ] != 0)
157 {
158 57 double ulps = 0;
159 57 ratio += (100. * ulp_map[ i ]) / static_cast<double>(nb_ulps);
160
161
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 15 times.
57 if(i <= 3) ulps = static_cast<double>(i) / 2.0;
162
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
30 else if(i == nb_buckets - 1) ulps = std::numeric_limits<double>::infinity();
163 30 else ulps = 1 << (i - 4);
164
165 57 auto [ s, in, out, ref ] = samples[ i ];
166
167
1/1
✓ Branch 2 taken 30 times.
57 _::results(ulps, ulp_map[ i ], ratio, "Input: ", in);
168
1/1
✓ Branch 1 taken 30 times.
57 _::results(-1., 0, 0., "Found: ", out);
169
1/1
✓ Branch 1 taken 30 times.
57 _::results(-1., 0, 0., "instead of: ", ref);
170
1/1
✓ Branch 2 taken 30 times.
57 _::separator(!::tts::is_quiet());
171 }
172 }
173
174 7 return max_ulp;
175 7 }
176 }
177
178 //======================================================================================================================
179 /*!
180 @def TTS_ULP_RANGE_CHECK
181 @ingroup test-precision
182 @brief Generate a range based test between two functions
183
184 Evaluates the histogram of ULP difference between two functions run on the same data set and that
185 they lie in a given [ULP distance](rationale.html#ulp).
186
187 @param Producer Data set generator to use.
188 @param RefType Type to use as reference function input.
189 @param NewType Type to use as challenger function input.
190 @param RefFunc Reference function to compare to.
191 @param NewFunc Challenger function to be compared to the reference one.
192 @param Ulpmax Maximal ULPs acceptable for passing the test.
193
194 @groupheader{Example}
195 @snippet doc/ulp_ranges.cpp snippet
196 **/
197 //======================================================================================================================
198 #if defined(TTS_DOXYGEN_INVOKED)
199 #define TTS_ULP_RANGE_CHECK(Producer, RefType, NewType, RefFunc, NewFunc, Ulpmax)
200 #else
201 #define TTS_ULP_RANGE_CHECK(Producer, RefType, NewType, RefFunc, NewFunc, Ulpmax) \
202 [ & ]() \
203 { \
204 if(!::tts::is_quiet()) \
205 ::tts::output().write("Comparing: %s<%s> with %s<%s> using ", \
206 TTS_STRING(RefFunc), \
207 TTS_STRING(TTS_REMOVE_PARENS(RefType)), \
208 TTS_STRING(NewFunc), \
209 TTS_STRING(TTS_REMOVE_PARENS(NewType))); \
210 \
211 auto local_tts_generator = TTS_REMOVE_PARENS(Producer); \
212 ::tts::_::print_producer(local_tts_generator, TTS_STRING(TTS_REMOVE_PARENS(Producer))); \
213 \
214 double tts_ulp_max = Ulpmax; \
215 double local_tts_threshold = ::tts::arguments().value(tts_ulp_max, "--ulpmax"); \
216 double local_tts_max_ulp = \
217 ::tts::ulp_histogram<TTS_REMOVE_PARENS(RefType), TTS_REMOVE_PARENS(NewType)>( \
218 local_tts_generator, RefFunc, NewFunc); \
219 \
220 if(local_tts_max_ulp <= local_tts_threshold) \
221 { \
222 ::tts::global_runtime.pass(); \
223 } \
224 else \
225 { \
226 TTS_FAIL("Expecting: %s similar to %s within %s ULP but found: %s ULP instead", \
227 TTS_STRING(NewFunc), \
228 TTS_STRING(RefFunc), \
229 ::tts::as_text(local_tts_threshold).data(), \
230 ::tts::as_text(local_tts_max_ulp).data()); \
231 } \
232 }()
233 /**/
234 #endif
235
236 namespace tts
237 {
238 //====================================================================================================================
239 /*!
240 @brief Data generator using a realistic random distribution
241
242 @groupheader{Example}
243 @code
244 #define TTS_MAIN // No need for main()
245 #include <tts/tts.hpp>
246
247 float ok_x (float x) { return x; }
248 float bad_x(float x) { return x + x*1e-7f; }
249
250 TTS_CASE( "Test range check" )
251 {
252 TTS_ULP_RANGE_CHECK ( tts::realistic_generator<float>(-100.f,+100.f)
253 , float, float , ok_x, bad_x
254 , 2.
255 );
256 };
257 @endcode
258
259 @see realistic_distribution
260 @tparam T Type of data to generate
261 **/
262 //====================================================================================================================
263 template<typename T> struct realistic_generator
264 {
265 1 realistic_generator(T mn, T mx)
266 1 : mini(mn)
267 1 , maxi(mx)
268 {
269 1 }
270 4096 T operator()(auto, auto, auto) const
271 {
272 4096 return ::tts::random_value(mini, maxi);
273 }
274
275 private:
276 // The rendering used to be a hidden friend reading these directly. It is a tts::display
277 // specialization now, and needs the same access.
278 template<typename> friend struct display;
279
280 T mini, maxi;
281 };
282
283 template<typename T> struct display<realistic_generator<T>>
284 {
285 1 static text render(realistic_generator<T> const& s)
286 {
287 return text {"realistic_generator<%s>(%s,%s)",
288
1/1
✓ Branch 1 taken 1 times.
2 as_text(typename_<T>).data(),
289
1/1
✓ Branch 1 taken 1 times.
2 as_text(s.mini).data(),
290
1/1
✓ Branch 3 taken 1 times.
3 as_text(s.maxi).data()};
291 }
292 };
293 }
294