1.3.64
 
Loading...
Searching...
No Matches
Test_functions.h
1#pragma once
2// =================================================================================
3// Suite 2: Global Helper Functions (global.h)
4//
5// Tests for standalone utility functions, typically found in a global header,
6// that provide common functionality like math, string parsing, and file handling.
7// =================================================================================
8TEST_CASE("Mathematical and Geometric Helpers") {
9 SUBCASE("global.h utilities") {
10 SUBCASE("deg2rad and rad2deg") {
11 DOCTEST_CHECK(deg2rad(180.f) == doctest::Approx(PI_F));
12 DOCTEST_CHECK(rad2deg(PI_F) == doctest::Approx(180.f));
13 }
14
15 SUBCASE("clamp") {
16 DOCTEST_CHECK(clamp(5, 0, 10) == 5);
17 DOCTEST_CHECK(clamp(-5, 0, 10) == 0);
18 DOCTEST_CHECK(clamp(15, 0, 10) == 10);
19 DOCTEST_CHECK(clamp(5.5f, 0.f, 10.f) == doctest::Approx(5.5f));
20 }
21
22 SUBCASE("Safe trigonometry") {
23 DOCTEST_CHECK(acos_safe(1.000001f) == doctest::Approx(0.f));
24 DOCTEST_CHECK(acos_safe(-1.000001f) == doctest::Approx(PI_F));
25 DOCTEST_CHECK(asin_safe(1.000001f) == doctest::Approx(PI_F / 2.f));
26 DOCTEST_CHECK(asin_safe(-1.000001f) == doctest::Approx(-PI_F / 2.f));
27 }
28
29 SUBCASE("Integer power") {
30 DOCTEST_CHECK(powi(2, 10) == 1024);
31 DOCTEST_CHECK(powi(3.f, 3) == doctest::Approx(27.f));
32 }
33
34 SUBCASE("calculateTriangleArea") {
35 vec3 v0(0, 0, 0), v1(1, 0, 0), v2(0, 1, 0);
36 DOCTEST_CHECK(calculateTriangleArea(v0, v1, v2) == doctest::Approx(0.5f));
37 }
38
39 SUBCASE("calculateTriangleArea with degenerate triangle") {
40 vec3 v0(0, 0, 0), v1(1, 1, 1), v2(2, 2, 2); // Collinear points
41 DOCTEST_CHECK(calculateTriangleArea(v0, v1, v2) == doctest::Approx(0.f));
42 }
43
44 SUBCASE("blend colors") {
45 RGBcolor c1(1, 0, 0), c2(0, 1, 0);
46 RGBcolor blended = blend(c1, c2, 0.5f);
47 DOCTEST_CHECK(blended.r == doctest::Approx(0.5f));
48 DOCTEST_CHECK(blended.g == doctest::Approx(0.5f));
49 DOCTEST_CHECK(blended.b == doctest::Approx(0.f));
50
51 RGBAcolor ca1(1, 0, 0, 0), ca2(0, 1, 0, 1);
52 RGBAcolor blended_a = blend(ca1, ca2, 0.5f);
53 DOCTEST_CHECK(blended_a.r == doctest::Approx(0.5f));
54 DOCTEST_CHECK(blended_a.g == doctest::Approx(0.5f));
55 DOCTEST_CHECK(blended_a.b == doctest::Approx(0.f));
56 DOCTEST_CHECK(blended_a.a == doctest::Approx(0.5f));
57 }
58 }
59 SUBCASE("global.h geometric functions") {
60 SUBCASE("atan2_2pi") {
61 DOCTEST_CHECK(atan2_2pi(0, 1) == doctest::Approx(0));
62 DOCTEST_CHECK(atan2_2pi(1, 0) == doctest::Approx(PI_F / 2.0));
63 DOCTEST_CHECK(atan2_2pi(0, -1) == doctest::Approx(PI_F));
64 DOCTEST_CHECK(atan2_2pi(-1, 0) == doctest::Approx(3.0 * PI_F / 2.0));
65 DOCTEST_CHECK(atan2_2pi(-1, -1) > PI_F);
66 }
67
68 SUBCASE("atan2_2pi edge cases") {
69 float res = atan2_2pi(0.f, 0.f);
70 DOCTEST_CHECK(res == doctest::Approx(0.f));
71 }
72
73 SUBCASE("rotatePoint") {
74 vec3 p(1, 0, 0);
75 vec3 rotated = rotatePoint(p, PI_F / 2.f, PI_F / 2.f); // elevation, azimuth
76 DOCTEST_CHECK(rotated.x == doctest::Approx(0.f));
77 DOCTEST_CHECK(rotated.y == doctest::Approx(0.f));
78 DOCTEST_CHECK(rotated.z == doctest::Approx(-1.f));
79 }
80
81 SUBCASE("rotatePointAboutLine") {
82 vec3 point(1, 0, 0);
83 vec3 line_base(0, 0, 0);
84 vec3 line_dir(0, 0, 1);
85 vec3 rotated = rotatePointAboutLine(point, line_base, line_dir, PI_F / 2.f);
86 DOCTEST_CHECK(rotated.x == doctest::Approx(0.f));
87 DOCTEST_CHECK(rotated.y == doctest::Approx(1.f));
88 DOCTEST_CHECK(rotated.z == doctest::Approx(0.f));
89 }
90
91 SUBCASE("lineIntersection") {
92 vec2 p1(0, 0), q1(2, 2);
93 vec2 p2(0, 2), q2(2, 0);
94 DOCTEST_CHECK(lineIntersection(p1, q1, p2, q2));
95
96 vec2 p3(0, 0), q3(1, 1);
97 vec2 p4(2, 2), q4(3, 3);
98 DOCTEST_CHECK(!lineIntersection(p3, q3, p4, q4));
99 }
100 SUBCASE("lineIntersection with collinear and overlapping segments") {
101 // Collinear, non-overlapping
102 vec2 p1(0, 0), q1(1, 1);
103 vec2 p2(2, 2), q2(3, 3);
104 DOCTEST_CHECK(!lineIntersection(p1, q1, p2, q2));
105
106 // Collinear, overlapping
107 vec2 p3(0, 0), q3(2, 2);
108 vec2 p4(1, 1), q4(3, 3);
109 DOCTEST_CHECK(lineIntersection(p3, q3, p4, q4));
110
111 // Collinear, one contains another
112 vec2 p5(0, 0), q5(3, 3);
113 vec2 p6(1, 1), q6(2, 2);
114 DOCTEST_CHECK(lineIntersection(p5, q5, p6, q6));
115
116 // Collinear, share an endpoint
117 vec2 p7(0, 0), q7(1, 1);
118 vec2 p8(1, 1), q8(2, 2);
119 DOCTEST_CHECK(lineIntersection(p7, q7, p8, q8));
120 }
121 SUBCASE("lineIntersection parallel") {
122 vec2 p1(0, 0), q1(1, 0);
123 vec2 p2(0, 1), q2(1, 1);
124 DOCTEST_CHECK(!lineIntersection(p1, q1, p2, q2));
125 }
126 SUBCASE("pointInPolygon") {
127 std::vector<vec2> square = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
128 DOCTEST_CHECK(pointInPolygon(vec2(0.5, 0.5), square));
129 DOCTEST_CHECK(!pointInPolygon(vec2(1.5, 0.5), square));
130 DOCTEST_CHECK(pointInPolygon(vec2(0, 0), square)); // On edge
131 }
132 SUBCASE("pointInPolygon with concave polygon") {
133 // Concave polygon (U-shaped)
134 std::vector<vec2> concave_poly = {{0, 0}, {5, 0}, {5, 5}, {3, 3}, {2, 5}, {0, 5}};
135 DOCTEST_CHECK(pointInPolygon(vec2(1, 1), concave_poly)); // Inside
136 DOCTEST_CHECK(!pointInPolygon(vec2(4, 4), concave_poly)); // In concave part, but outside
137 DOCTEST_CHECK(pointInPolygon(vec2(2.5, 4), concave_poly)); // Inside
138 }
139 }
140 SUBCASE("Spline Interpolation") {
141 vec3 p0 = make_vec3(0, 0, 0);
142 vec3 t0 = make_vec3(3, 0, 0);
143 vec3 p1 = make_vec3(1, 1.5, 0.4);
144 vec3 t1 = make_vec3(0, 1, 0);
145 vec3 xi = spline_interp3(0.6f, p0, t0, p1, t1);
146 vec3 ref = make_vec3(0.9360f, 0.8280f, 0.2592f);
147 DOCTEST_CHECK(xi.x == doctest::Approx(ref.x).epsilon(errtol));
148 DOCTEST_CHECK(xi.y == doctest::Approx(ref.y).epsilon(errtol));
149 DOCTEST_CHECK(xi.z == doctest::Approx(ref.z).epsilon(errtol));
150 }
151 SUBCASE("spline_interp3 edge cases") {
152 vec3 p0 = make_vec3(0, 0, 0);
153 vec3 t0 = make_vec3(1, 0, 0);
154 vec3 p1 = make_vec3(1, 1, 1);
155 vec3 t1 = make_vec3(0, 1, 0);
156
157 vec3 res_start = spline_interp3(0.0f, p0, t0, p1, t1);
158 DOCTEST_CHECK(res_start.x == doctest::Approx(p0.x));
159 DOCTEST_CHECK(res_start.y == doctest::Approx(p0.y));
160 DOCTEST_CHECK(res_start.z == doctest::Approx(p0.z));
161
162 vec3 res_end = spline_interp3(1.0f, p0, t0, p1, t1);
163 DOCTEST_CHECK(res_end.x == doctest::Approx(p1.x));
164 DOCTEST_CHECK(res_end.y == doctest::Approx(p1.y));
165 DOCTEST_CHECK(res_end.z == doctest::Approx(p1.z));
166
167 // Test clamping
168 vec3 res_low, res_high;
169 {
170 capture_cerr cerr_buffer;
171 res_low = spline_interp3(-0.5f, p0, t0, p1, t1);
172 res_high = spline_interp3(1.5f, p0, t0, p1, t1);
173 }
174 DOCTEST_CHECK(res_low.x == doctest::Approx(p0.x));
175 DOCTEST_CHECK(res_high.x == doctest::Approx(p1.x));
176 }
177 SUBCASE("Angle conversion helpers") {
178 CHECK(deg2rad(180.f) == doctest::Approx(PI_F).epsilon(errtol));
179 CHECK(rad2deg(PI_F) == doctest::Approx(180.f).epsilon(errtol));
180
181 CHECK(atan2_2pi(0.f, 1.f) == doctest::Approx(0.f).epsilon(errtol));
182 CHECK(atan2_2pi(1.f, 0.f) == doctest::Approx(0.5f * PI_F * 2.f / 2.f).epsilon(errtol));
183 CHECK(atan2_2pi(0.f, -1.f) == doctest::Approx(PI_F).epsilon(errtol));
184 CHECK(atan2_2pi(-1.f, 0.f) == doctest::Approx(1.5f * PI_F).epsilon(errtol));
185 }
186
187 SUBCASE("clamp template for common types") {
188 CHECK(clamp(5, 1, 4) == 4);
189 CHECK(clamp(-1, 0, 10) == 0);
190 CHECK(clamp(3.5f, 0.f, 10.f) == doctest::Approx(3.5f).epsilon(errtol));
191 CHECK(clamp(12.0, -5.0, 11.0) == doctest::Approx(11.0).epsilon(errtol));
192 }
193}
194
195TEST_CASE("Matrix and Transformation Helpers") {
196 float M[16], T[16], R[16];
197
198 SUBCASE("makeIdentityMatrix") {
200 DOCTEST_CHECK(T[0] == 1.f);
201 DOCTEST_CHECK(T[1] == 0.f);
202 DOCTEST_CHECK(T[2] == 0.f);
203 DOCTEST_CHECK(T[3] == 0.f);
204 DOCTEST_CHECK(T[4] == 0.f);
205 DOCTEST_CHECK(T[5] == 1.f);
206 DOCTEST_CHECK(T[6] == 0.f);
207 DOCTEST_CHECK(T[7] == 0.f);
208 DOCTEST_CHECK(T[8] == 0.f);
209 DOCTEST_CHECK(T[9] == 0.f);
210 DOCTEST_CHECK(T[10] == 1.f);
211 DOCTEST_CHECK(T[11] == 0.f);
212 DOCTEST_CHECK(T[12] == 0.f);
213 DOCTEST_CHECK(T[13] == 0.f);
214 DOCTEST_CHECK(T[14] == 0.f);
215 DOCTEST_CHECK(T[15] == 1.f);
216 }
217
218 SUBCASE("makeRotationMatrix") {
219 // About x-axis
220 makeRotationMatrix(PI_F / 2.f, "x", T);
221 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
222 DOCTEST_CHECK(T[6] == doctest::Approx(-1.f));
223 DOCTEST_CHECK(T[9] == doctest::Approx(1.f));
224 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
225
226 // About y-axis
227 makeRotationMatrix(PI_F / 2.f, "y", T);
228 DOCTEST_CHECK(T[0] == doctest::Approx(0.f));
229 DOCTEST_CHECK(T[2] == doctest::Approx(1.f));
230 DOCTEST_CHECK(T[8] == doctest::Approx(-1.f));
231 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
232
233 // About z-axis
234 makeRotationMatrix(PI_F / 2.f, "z", T);
235 DOCTEST_CHECK(T[0] == doctest::Approx(0.f));
236 DOCTEST_CHECK(T[1] == doctest::Approx(-1.f));
237 DOCTEST_CHECK(T[4] == doctest::Approx(1.f));
238 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
239
240 // About arbitrary axis
241 vec3 axis(1, 0, 0);
242 makeRotationMatrix(PI_F / 2.f, axis, T);
243 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
244 DOCTEST_CHECK(T[6] == doctest::Approx(-1.f));
245 DOCTEST_CHECK(T[9] == doctest::Approx(1.f));
246 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
247 }
248
249 SUBCASE("makeRotationMatrix invalid axis") {
250 float T[16];
251 DOCTEST_CHECK_THROWS(makeRotationMatrix(0.5f, "w", T));
252 }
253
254 SUBCASE("makeTranslationMatrix") {
255 makeTranslationMatrix(vec3(1, 2, 3), T);
256 DOCTEST_CHECK(T[3] == 1.f);
257 DOCTEST_CHECK(T[7] == 2.f);
258 DOCTEST_CHECK(T[11] == 3.f);
259 }
260
261 SUBCASE("makeScaleMatrix") {
262 // About origin
263 makeScaleMatrix(vec3(2, 3, 4), T);
264 DOCTEST_CHECK(T[0] == 2.f);
265 DOCTEST_CHECK(T[5] == 3.f);
266 DOCTEST_CHECK(T[10] == 4.f);
267
268 // About point
269 makeScaleMatrix(vec3(2, 2, 2), vec3(1, 1, 1), T);
270 vec3 v(2, 2, 2);
271 vec3 res;
272 vecmult(T, v, res);
273 DOCTEST_CHECK(res.x == doctest::Approx(3.f));
274 DOCTEST_CHECK(res.y == doctest::Approx(3.f));
275 DOCTEST_CHECK(res.z == doctest::Approx(3.f));
276 }
277
278 SUBCASE("matmult") {
279 makeRotationMatrix(PI_F / 2.f, "x", M);
280 makeRotationMatrix(-PI_F / 2.f, "x", T);
281 matmult(M, T, R); // Should be identity
282 DOCTEST_CHECK(R[0] == doctest::Approx(1.f));
283 DOCTEST_CHECK(R[5] == doctest::Approx(1.f));
284 DOCTEST_CHECK(R[10] == doctest::Approx(1.f));
285 DOCTEST_CHECK(R[15] == doctest::Approx(1.f));
286 }
287
288 SUBCASE("vecmult") {
289 makeRotationMatrix(PI_F / 2.f, "z", M);
290 vec3 v(1, 0, 0);
291 vec3 res;
292 vecmult(M, v, res);
293 DOCTEST_CHECK(res.x == doctest::Approx(0.f));
294 DOCTEST_CHECK(res.y == doctest::Approx(1.f));
295 DOCTEST_CHECK(res.z == doctest::Approx(0.f));
296
297 float v_arr[3] = {1, 0, 0};
298 float res_arr[3];
299 vecmult(M, v_arr, res_arr);
300 DOCTEST_CHECK(res_arr[0] == doctest::Approx(0.f));
301 DOCTEST_CHECK(res_arr[1] == doctest::Approx(1.f));
302 DOCTEST_CHECK(res_arr[2] == doctest::Approx(0.f));
303 }
304
305 SUBCASE("makeIdentityMatrix / matmult / vecmult") {
306 float I[16] = {};
308 for (int r = 0; r < 4; ++r) {
309 for (int c = 0; c < 4; ++c) {
310 CHECK(I[4 * r + c] == doctest::Approx(r == c ? 1.f : 0.f).epsilon(errtol));
311 }
312 }
313
314 float T[16];
315 makeTranslationMatrix(make_vec3(1.f, 2.f, 3.f), T);
316 float R[16];
317 matmult(I, T, R); // I * T == T
318 for (int i = 0; i < 16; ++i) {
319 CHECK(R[i] == doctest::Approx(T[i]).epsilon(errtol));
320 }
321
322 float v[3] = {4.f, 5.f, 6.f}, out[3] = {};
323 vecmult(I, v, out);
324 CHECK(out[0] == doctest::Approx(4.f).epsilon(errtol));
325 CHECK(out[1] == doctest::Approx(5.f).epsilon(errtol));
326 CHECK(out[2] == doctest::Approx(6.f).epsilon(errtol));
327 }
328
329 SUBCASE("rotatePoint & rotatePointAboutLine") {
330 vec3 p = make_vec3(1, 0, 0);
331 // Rotate 90° about z -> y axis
332 vec3 r = rotatePoint(p, 0.f, 0.5f * PI_F);
333 CHECK(r.x == doctest::Approx(0.f).epsilon(errtol));
334 CHECK(r.y == doctest::Approx(1.f).epsilon(errtol));
335
336 // Rotate about arbitrary line (x-axis) back to original
337 vec3 q = rotatePointAboutLine(r, make_vec3(0, 0, 0), make_vec3(1, 0, 0), -0.5f * PI_F);
338 CHECK(q.x == doctest::Approx(0.f).epsilon(errtol));
339 CHECK(q.y == doctest::Approx(0.f).epsilon(errtol));
340 }
341}
342
343TEST_CASE("String, File Path, and Parsing Utilities") {
344 SUBCASE("String Manipulation") {
345 DOCTEST_CHECK(deblank(" hello world ") == "helloworld");
346 DOCTEST_CHECK(trim_whitespace(" hello world ") == "hello world");
347 }
348 SUBCASE("String Delimiting") {
349 std::vector<std::string> result = separate_string_by_delimiter("a,b,c", ",");
350 DOCTEST_CHECK(result.size() == 3);
351 if (result.size() == 3) {
352 DOCTEST_CHECK(result[0] == "a");
353 DOCTEST_CHECK(result[1] == "b");
354 DOCTEST_CHECK(result[2] == "c");
355 }
356 }
357 SUBCASE("separate_string_by_delimiter edge cases") {
358 std::vector<std::string> result;
359 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("a,b,c", ";"));
360 DOCTEST_CHECK(result.size() == 1);
361 DOCTEST_CHECK(result[0] == "a,b,c");
362
363 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("a|b|c", "|"));
364 DOCTEST_CHECK(result.size() == 3);
365 DOCTEST_CHECK(result[0] == "a");
366 DOCTEST_CHECK(result[1] == "b");
367 DOCTEST_CHECK(result[2] == "c");
368
369 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("", ","));
370 DOCTEST_CHECK(result.size() == 1);
371
372 {
373 capture_cerr cerr_buffer;
374 DOCTEST_CHECK_THROWS(result = separate_string_by_delimiter("a,b,c", ""));
375 }
376
377 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter(",", ","));
378 DOCTEST_CHECK(result.size() == 2);
379 if (result.size() == 2) {
380 DOCTEST_CHECK(result[0] == "");
381 DOCTEST_CHECK(result[1] == "");
382 }
383 }
384 SUBCASE("String to Vector Conversions") {
385 DOCTEST_CHECK(string2vec2("1.5 2.5") == vec2(1.5f, 2.5f));
386 DOCTEST_CHECK(string2vec3("1.5 2.5 3.5") == vec3(1.5f, 2.5f, 3.5f));
387 DOCTEST_CHECK(string2vec4("1.5 2.5 3.5 4.5") == vec4(1.5f, 2.5f, 3.5f, 4.5f));
388 DOCTEST_CHECK(string2int2("1 2") == int2(1, 2));
389 DOCTEST_CHECK(string2int3("1 2 3") == int3(1, 2, 3));
390 DOCTEST_CHECK(string2int4("1 2 3 4") == int4(1, 2, 3, 4));
391 }
392 SUBCASE("string to vector conversions with invalid input") {
393 vec2 result_vec2;
394 vec3 result_vec3;
395 vec4 result_vec4;
396 int2 result_int2;
397 int3 result_int3;
398 int4 result_int4;
399 // Error messages from invalid input are expected - don't suppress them
400 DOCTEST_CHECK_THROWS(result_vec2 = string2vec2("1.5"));
401 DOCTEST_CHECK_THROWS(result_vec3 = string2vec3("1.5 2.5"));
402 DOCTEST_CHECK_THROWS(result_vec4 = string2vec4("1.5 2.5 3.5"));
403 DOCTEST_CHECK_THROWS(result_int2 = string2int2("1"));
404 DOCTEST_CHECK_THROWS(result_int3 = string2int3("1 2"));
405 DOCTEST_CHECK_THROWS(result_int4 = string2int4("1 2 3"));
406 DOCTEST_CHECK_THROWS(result_vec2 = string2vec2("1.5 abc"));
407 }
408
409 SUBCASE("String to Color Conversion") {
410 RGBAcolor color = string2RGBcolor("0.1 0.2 0.3 0.4");
411 DOCTEST_CHECK(color.r == doctest::Approx(0.1f));
412 DOCTEST_CHECK(color.g == doctest::Approx(0.2f));
413 DOCTEST_CHECK(color.b == doctest::Approx(0.3f));
414 DOCTEST_CHECK(color.a == doctest::Approx(0.4f));
415
416 color = string2RGBcolor("0.5 0.6 0.7");
417 DOCTEST_CHECK(color.r == doctest::Approx(0.5f));
418 DOCTEST_CHECK(color.g == doctest::Approx(0.6f));
419 DOCTEST_CHECK(color.b == doctest::Approx(0.7f));
420 DOCTEST_CHECK(color.a == doctest::Approx(1.0f));
421 }
422 SUBCASE("string2RGBcolor with invalid input") {
423 RGBAcolor result;
424 // Error messages from invalid input are expected - don't suppress them
425 DOCTEST_CHECK_THROWS(result = string2RGBcolor("0.1 0.2"));
426 DOCTEST_CHECK_THROWS(result = string2RGBcolor("0.1 0.2 0.3 0.4 0.5"));
427 DOCTEST_CHECK_THROWS(result = string2RGBcolor("a b c"));
428 }
429 SUBCASE("File Path Parsing") {
430 std::string filepath = "/path/to/file/filename.ext";
431 DOCTEST_CHECK(getFileExtension(filepath) == ".ext");
432 DOCTEST_CHECK(getFileName(filepath) == "filename.ext");
433 DOCTEST_CHECK(getFileStem(filepath) == "filename");
434#ifndef _WIN32
435 DOCTEST_CHECK(getFilePath(filepath, true) == "/path/to/file/");
436 DOCTEST_CHECK(getFilePath(filepath, false) == "/path/to/file");
437#endif
438
439 std::string filepath_noext = "/path/to/file/filename";
440 DOCTEST_CHECK(getFileExtension(filepath_noext).empty());
441 DOCTEST_CHECK(getFileName(filepath_noext) == "filename");
442 DOCTEST_CHECK(getFileStem(filepath_noext) == "filename");
443
444 std::string filepath_nodir = "filename.ext";
445 DOCTEST_CHECK(getFileExtension(filepath_nodir) == ".ext");
446 DOCTEST_CHECK(getFileName(filepath_nodir) == "filename.ext");
447 DOCTEST_CHECK(getFileStem(filepath_nodir) == "filename");
448 DOCTEST_CHECK(getFilePath(filepath_nodir, true).empty());
449 }
450 SUBCASE("File path parsing edge cases") {
451 DOCTEST_CHECK(getFileExtension(".bashrc").empty());
452 DOCTEST_CHECK(getFileName(".bashrc") == ".bashrc");
453 DOCTEST_CHECK(getFileStem(".bashrc") == ".bashrc");
454#ifndef _WIN32
455 DOCTEST_CHECK(getFilePath("/path/to/file/", true) == "/path/to/file/");
456 DOCTEST_CHECK(getFilePath("/path/to/file/", false) == "/path/to/file");
457#endif
458 DOCTEST_CHECK(getFilePath("..", true).empty());
459 DOCTEST_CHECK(getFileName("..") == "..");
460 DOCTEST_CHECK(getFileStem("..") == "..");
461 }
462 SUBCASE("Directory path detection") {
463 // Test cases that should be detected as directories
464 DOCTEST_CHECK(isDirectoryPath("./annotations") == true); // The bug case
465 DOCTEST_CHECK(isDirectoryPath("./output") == true);
466 DOCTEST_CHECK(isDirectoryPath("results") == true);
467 DOCTEST_CHECK(isDirectoryPath("data/") == true);
468 DOCTEST_CHECK(isDirectoryPath("/path/to/dir/") == true);
469 DOCTEST_CHECK(isDirectoryPath("temp") == true);
470 DOCTEST_CHECK(isDirectoryPath("images") == true);
471 DOCTEST_CHECK(isDirectoryPath("..") == true);
472 DOCTEST_CHECK(isDirectoryPath(".") == true);
473
474 // Test cases that should be detected as files
475 DOCTEST_CHECK(isDirectoryPath("file.txt") == false);
476 DOCTEST_CHECK(isDirectoryPath("./test.cpp") == false);
477 DOCTEST_CHECK(isDirectoryPath("output.json") == false);
478 DOCTEST_CHECK(isDirectoryPath("/path/to/file.xml") == false);
479 DOCTEST_CHECK(isDirectoryPath("image.jpg") == false);
480 DOCTEST_CHECK(isDirectoryPath("data.csv") == false);
481 DOCTEST_CHECK(isDirectoryPath(".bashrc") == false);
482
483 // Edge cases
484 DOCTEST_CHECK(isDirectoryPath("") == false); // empty path
485
486 // Test with existing directories if they exist
487 if (std::filesystem::exists("core/tests")) {
488 DOCTEST_CHECK(isDirectoryPath("core/tests") == true);
489 }
490 if (std::filesystem::exists("core/include/global.h")) {
491 DOCTEST_CHECK(isDirectoryPath("core/include/global.h") == false);
492 }
493 }
494 SUBCASE("validateOutputPath adds trailing slash to directories") {
495 // Test that validateOutputPath adds trailing slash to directory paths without one
496 // This is a regression test for the radiation plugin image_path bug
497
498 // Create a temporary directory for testing
499 std::filesystem::path temp_dir = std::filesystem::temp_directory_path() / "helios_test_validatepath";
500 std::filesystem::create_directories(temp_dir);
501
502 // Test 1: Directory path without trailing slash (existing directory)
503 std::string path1 = temp_dir.string();
504 // Remove trailing slash if present
505 if (!path1.empty() && (path1.back() == '/' || path1.back() == '\\')) {
506 path1.pop_back();
507 }
508 DOCTEST_CHECK(validateOutputPath(path1) == true);
509 DOCTEST_CHECK(path1.back() == '/'); // Should now have trailing slash
510
511 // Test 2: Directory path with trailing slash (should remain unchanged)
512 std::string path2 = temp_dir.string() + "/";
513 DOCTEST_CHECK(validateOutputPath(path2) == true);
514 DOCTEST_CHECK(path2.back() == '/');
515
516 // Test 3: Non-existent directory without trailing slash that is detected as directory
517 // Note: "../somedir" pattern from the bug report
518 std::string path3 = temp_dir.string() + "_nonexistent";
519 if (!path3.empty() && (path3.back() == '/' || path3.back() == '\\')) {
520 path3.pop_back();
521 }
522 // This path doesn't exist but should be detected as a directory and get trailing slash
523 bool result3 = validateOutputPath(path3);
524 DOCTEST_CHECK(result3 == true); // Should succeed (creates directory)
525 DOCTEST_CHECK(path3.back() == '/'); // Should have trailing slash
526
527 // Test 4: File path should not get trailing slash
528 std::string path4 = temp_dir.string() + "/test.txt";
529 DOCTEST_CHECK(validateOutputPath(path4, {".txt"}) == true);
530 DOCTEST_CHECK(path4.back() != '/'); // Should NOT have trailing slash (it's a file)
531
532 // Clean up
533 std::filesystem::remove_all(temp_dir);
534 std::filesystem::remove_all(temp_dir.string() + "_nonexistent");
535 }
536 SUBCASE("Primitive Type Parsing") {
537 float f;
538 DOCTEST_CHECK(parse_float("1.23", f));
539 DOCTEST_CHECK(f == doctest::Approx(1.23f));
540 DOCTEST_CHECK(!parse_float("abc", f));
541
542 double d;
543 DOCTEST_CHECK(parse_double("1.23", d));
544 DOCTEST_CHECK(d == doctest::Approx(1.23));
545 DOCTEST_CHECK(!parse_double("abc", d));
546
547 int i;
548 DOCTEST_CHECK(parse_int("123", i));
549 DOCTEST_CHECK(i == 123);
550 DOCTEST_CHECK(!parse_int("abc", i));
551 DOCTEST_CHECK(!parse_int("1.23", i));
552
553 unsigned int u;
554 DOCTEST_CHECK(parse_uint("123", u));
555 DOCTEST_CHECK(u == 123u);
556 DOCTEST_CHECK(!parse_uint("-123", u));
557 }
558 SUBCASE("Compound Type Parsing") {
559 int2 i2;
560 DOCTEST_CHECK(parse_int2("1 2", i2));
561 DOCTEST_CHECK(i2 == int2(1, 2));
562 DOCTEST_CHECK(!parse_int2("1", i2));
563
564 int3 i3;
565 DOCTEST_CHECK(parse_int3("1 2 3", i3));
566 DOCTEST_CHECK(i3 == int3(1, 2, 3));
567 DOCTEST_CHECK(!parse_int3("1 2", i3));
568
569 vec2 v2;
570 DOCTEST_CHECK(parse_vec2("1.1 2.2", v2));
571 DOCTEST_CHECK(v2.x == doctest::Approx(1.1f));
572 DOCTEST_CHECK(v2.y == doctest::Approx(2.2f));
573 DOCTEST_CHECK(!parse_vec2("1.1", v2));
574
575 vec3 v3;
576 DOCTEST_CHECK(parse_vec3("1.1 2.2 3.3", v3));
577 DOCTEST_CHECK(v3.x == doctest::Approx(1.1f));
578 DOCTEST_CHECK(!parse_vec3("1.1 2.2", v3));
579
580 RGBcolor rgb;
581 DOCTEST_CHECK(parse_RGBcolor("0.1 0.2 0.3", rgb));
582 DOCTEST_CHECK(rgb.r == doctest::Approx(0.1f));
583 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2", rgb));
584 }
585 SUBCASE("parse functions with whitespace") {
586 int i;
587 DOCTEST_CHECK(parse_int(" 123 ", i));
588 DOCTEST_CHECK(i == 123);
589 float f;
590 DOCTEST_CHECK(parse_float(" 1.23 ", f));
591 DOCTEST_CHECK(f == doctest::Approx(1.23f));
592 }
593 SUBCASE("parse_* invalid input") {
594 // Error messages from invalid input are expected - don't suppress them
595 int i;
596 DOCTEST_CHECK(!parse_int("1.5", i));
597 float f;
598 DOCTEST_CHECK(!parse_float("abc", f));
599 double d;
600 DOCTEST_CHECK(!parse_double("abc", d));
601 unsigned int u;
602 DOCTEST_CHECK(!parse_uint("-1", u));
603 int2 i2;
604 DOCTEST_CHECK(!parse_int2("1 abc", i2));
605 int3 i3;
606 DOCTEST_CHECK(!parse_int3("1 2 abc", i3));
607 vec2 v2;
608 DOCTEST_CHECK(!parse_vec2("1.1 abc", v2));
609 vec3 v3;
610 DOCTEST_CHECK(!parse_vec3("1.1 2.2 abc", v3));
611 RGBcolor c;
612 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2 abc", c));
613 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2 1.1", c)); // out of range
614 }
615}
616
617TEST_CASE("Vector Statistics and Manipulation") {
618 SUBCASE("Vector Statistics") {
619 std::vector<float> v = {1.f, 2.f, 3.f, 4.f, 5.f};
620 DOCTEST_CHECK(sum(v) == doctest::Approx(15.f));
621 DOCTEST_CHECK(mean(v) == doctest::Approx(3.f));
622 DOCTEST_CHECK(min(v) == doctest::Approx(1.f));
623 DOCTEST_CHECK(max(v) == doctest::Approx(5.f));
624 DOCTEST_CHECK(stdev(v) == doctest::Approx(sqrtf(2.f)));
625 DOCTEST_CHECK(median(v) == doctest::Approx(3.f));
626 std::vector<float> v2 = {1.f, 2.f, 3.f, 4.f};
627 DOCTEST_CHECK(median(v2) == doctest::Approx(2.5f));
628 }
629 SUBCASE("sum / mean / min / max / stdev / median") {
630 std::vector<float> vf{1.f, 2.f, 3.f, 4.f, 5.f};
631 CHECK(sum(vf) == doctest::Approx(15.f).epsilon(errtol));
632 CHECK(mean(vf) == doctest::Approx(3.f).epsilon(errtol));
633 CHECK(min(vf) == doctest::Approx(1.f).epsilon(errtol));
634 CHECK(max(vf) == doctest::Approx(5.f).epsilon(errtol));
635 CHECK(median(vf) == doctest::Approx(3.f).epsilon(errtol));
636 CHECK(stdev(vf) == doctest::Approx(std::sqrt(2.f)).epsilon(1e-4));
637
638 std::vector<int> vi{9, 4, -3, 10, 2};
639 CHECK(min(vi) == -3);
640 CHECK(max(vi) == 10);
641
642 std::vector<vec3> vv{make_vec3(2, 3, 4), make_vec3(-1, 7, 0)};
643 CHECK(min(vv) == make_vec3(-1, 3, 0));
644 CHECK(max(vv) == make_vec3(2, 7, 4));
645 }
646 SUBCASE("Vector statistics with single element") {
647 std::vector<float> v = {5.f};
648 DOCTEST_CHECK(stdev(v) == doctest::Approx(0.f));
649 }
650 SUBCASE("Vector Manipulation") {
651 SUBCASE("resize_vector") {
652 std::vector<std::vector<int>> vec2d;
653 resize_vector(vec2d, 2, 3);
654 DOCTEST_CHECK(vec2d.size() == 3);
655 DOCTEST_CHECK(vec2d[0].size() == 2);
656
657 std::vector<std::vector<std::vector<int>>> vec3d;
658 resize_vector(vec3d, 2, 3, 4);
659 DOCTEST_CHECK(vec3d.size() == 4);
660 DOCTEST_CHECK(vec3d[0].size() == 3);
661 DOCTEST_CHECK(vec3d[0][0].size() == 2);
662
663 std::vector<std::vector<std::vector<std::vector<int>>>> vec4d;
664 resize_vector(vec4d, 2, 3, 4, 5);
665 DOCTEST_CHECK(vec4d.size() == 5);
666 DOCTEST_CHECK(vec4d[0].size() == 4);
667 DOCTEST_CHECK(vec4d[0][0].size() == 3);
668 DOCTEST_CHECK(vec4d[0][0][0].size() == 2);
669 }
670
671 SUBCASE("flatten") {
672 std::vector<std::vector<int>> vec2d = {{1, 2}, {3, 4}};
673 std::vector<int> flat = flatten(vec2d);
674 DOCTEST_CHECK(flat.size() == 4);
675 DOCTEST_CHECK(flat[3] == 4);
676
677 std::vector<std::vector<std::vector<int>>> vec3d = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
678 flat = flatten(vec3d);
679 DOCTEST_CHECK(flat.size() == 8);
680 DOCTEST_CHECK(flat[7] == 8);
681
682 std::vector<std::vector<std::vector<std::vector<int>>>> vec4d = {{{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}, {{{9, 10}, {11, 12}}, {{13, 14}, {15, 16}}}};
683 flat = flatten(vec4d);
684 DOCTEST_CHECK(flat.size() == 16);
685 DOCTEST_CHECK(flat[15] == 16);
686 }
687
688 SUBCASE("vector operators") {
689 std::vector<float> v1 = {1, 2, 3};
690 std::vector<float> v2 = {4, 5, 6};
691 std::vector<float> v_sum = v1 + v2;
692 DOCTEST_CHECK(v_sum[0] == 5.f);
693 DOCTEST_CHECK(v_sum[2] == 9.f);
694
695 v1 += v2;
696 DOCTEST_CHECK(v1[0] == 5.f);
697 DOCTEST_CHECK(v1[2] == 9.f);
698
699 std::vector<float> v3 = {1};
700 DOCTEST_CHECK_THROWS(v1 + v3);
701 }
702
703 SUBCASE("vector-scalar operators") {
704 std::vector<float> v = {1.f, 2.f, 3.f};
705 float s = 2.f;
706
707 // Addition: vector + scalar
708 std::vector<float> v_add = v + s;
709 DOCTEST_CHECK(v_add.size() == 3);
710 DOCTEST_CHECK(v_add[0] == doctest::Approx(3.f));
711 DOCTEST_CHECK(v_add[1] == doctest::Approx(4.f));
712 DOCTEST_CHECK(v_add[2] == doctest::Approx(5.f));
713
714 // Addition: scalar + vector
715 std::vector<float> v_add2 = s + v;
716 DOCTEST_CHECK(v_add2[0] == doctest::Approx(3.f));
717 DOCTEST_CHECK(v_add2[1] == doctest::Approx(4.f));
718 DOCTEST_CHECK(v_add2[2] == doctest::Approx(5.f));
719
720 // Subtraction: vector - scalar
721 std::vector<float> v_sub = v - s;
722 DOCTEST_CHECK(v_sub[0] == doctest::Approx(-1.f));
723 DOCTEST_CHECK(v_sub[1] == doctest::Approx(0.f));
724 DOCTEST_CHECK(v_sub[2] == doctest::Approx(1.f));
725
726 // Subtraction: scalar - vector
727 std::vector<float> v_sub2 = s - v;
728 DOCTEST_CHECK(v_sub2[0] == doctest::Approx(1.f));
729 DOCTEST_CHECK(v_sub2[1] == doctest::Approx(0.f));
730 DOCTEST_CHECK(v_sub2[2] == doctest::Approx(-1.f));
731
732 // Multiplication: vector * scalar
733 std::vector<float> v_mul = v * s;
734 DOCTEST_CHECK(v_mul[0] == doctest::Approx(2.f));
735 DOCTEST_CHECK(v_mul[1] == doctest::Approx(4.f));
736 DOCTEST_CHECK(v_mul[2] == doctest::Approx(6.f));
737
738 // Multiplication: scalar * vector
739 std::vector<float> v_mul2 = s * v;
740 DOCTEST_CHECK(v_mul2[0] == doctest::Approx(2.f));
741 DOCTEST_CHECK(v_mul2[1] == doctest::Approx(4.f));
742 DOCTEST_CHECK(v_mul2[2] == doctest::Approx(6.f));
743
744 // Division: vector / scalar
745 std::vector<float> v_div = v / s;
746 DOCTEST_CHECK(v_div[0] == doctest::Approx(0.5f));
747 DOCTEST_CHECK(v_div[1] == doctest::Approx(1.f));
748 DOCTEST_CHECK(v_div[2] == doctest::Approx(1.5f));
749
750 // Division: scalar / vector
751 std::vector<float> v_div2 = 6.f / v;
752 DOCTEST_CHECK(v_div2[0] == doctest::Approx(6.f));
753 DOCTEST_CHECK(v_div2[1] == doctest::Approx(3.f));
754 DOCTEST_CHECK(v_div2[2] == doctest::Approx(2.f));
755
756 // Empty vector
757 std::vector<float> empty;
758 std::vector<float> empty_result = empty + 1.f;
759 DOCTEST_CHECK(empty_result.empty());
760 }
761 }
762}
763
764TEST_CASE("Miscellaneous Utilities") {
765 SUBCASE("1D Interpolation") {
766 std::vector<vec2> points = {{0, 0}, {1, 1}, {2, 0}};
767 float res = interp1(points, 0.5f);
768 DOCTEST_CHECK(res == doctest::Approx(0.5f));
769 res = interp1(points, 1.5f);
770 DOCTEST_CHECK(res == doctest::Approx(0.5f));
771 res = interp1(points, -1.f);
772 DOCTEST_CHECK(res == doctest::Approx(0.f));
773 res = interp1(points, 3.f);
774 DOCTEST_CHECK(res == doctest::Approx(0.f));
775 }
776 SUBCASE("interp1 edge cases") {
777 float result;
778 std::vector<vec2> empty_points;
779 // Error from empty input is expected - don't suppress it
780 DOCTEST_CHECK_THROWS(result = interp1(empty_points, 0.5f));
781
782 std::vector<vec2> single_point = {{1, 5}};
783 result = interp1(single_point, 0.5f);
784 DOCTEST_CHECK(result == doctest::Approx(5.f));
785 result = interp1(single_point, 2.f);
786 DOCTEST_CHECK(result == doctest::Approx(5.f));
787 }
788 SUBCASE("Timer") {
789 Timer t;
790 t.tic();
791 // Not a real test of time, just that it runs.
792 double elapsed = t.toc("mute");
793 DOCTEST_CHECK(elapsed >= 0);
794 }
795 SUBCASE("Custom Error") {
796 capture_cerr cerr_buffer;
797 DOCTEST_CHECK_THROWS_AS(helios_runtime_error("test error"), std::runtime_error);
798 }
799 SUBCASE("Random Number Generation") {
800 float r = randu();
801 DOCTEST_CHECK(r >= 0.f);
802 DOCTEST_CHECK(r <= 1.f);
803
804 int ri = randu(1, 10);
805 DOCTEST_CHECK(ri >= 1);
806 DOCTEST_CHECK(ri <= 10);
807 }
808 SUBCASE("randu(int, int)") {
809 int r = randu(5, 5);
810 DOCTEST_CHECK(r == 5);
811 for (int i = 0; i < 100; ++i) {
812 r = randu(1, 100);
813 DOCTEST_CHECK(r >= 1);
814 DOCTEST_CHECK(r <= 100);
815 }
816 }
817 SUBCASE("acos_safe & asin_safe clamp inputs") {
818 CHECK(acos_safe(1.5f) == doctest::Approx(0.f).epsilon(errtol));
819 CHECK(asin_safe(-2.f) == doctest::Approx(-0.5f * PI_F).epsilon(errtol));
820 }
821
822 SUBCASE("randu range checks") {
823 for (int i = 0; i < 100; ++i) {
824 float r = randu();
825 CHECK(r >= 0.f);
826 CHECK(r <= 1.f);
827
828 int ri = randu(5, 10);
829 CHECK(ri >= 5);
830 CHECK(ri <= 10);
831 CHECK(randu(3, 3) == 3);
832 }
833 }
834}
835
836static float quadratic(float x, std::vector<float> &, const void *) {
837 return x * x - 4.0f;
838}
839static float linear(float x, std::vector<float> &, const void *) {
840 return 3.5f - x;
841}
842static float flat(float, std::vector<float> &, const void *) {
843 return 2.0f;
844}
845static float cubic(float x, std::vector<float> &, const void *) {
846 return (x - 1.0f) * (x + 2.0f) * (x - 4.0f);
847}
848static float near_singular(float x, std::vector<float> &, const void *) {
849 return (x - 1e-3f) * (x - 1e-3f);
850}
851TEST_CASE("fzero") {
852 SUBCASE("fzero finds positive quadratic root") {
853 std::vector<float> v;
854 float root = helios::fzero(quadratic, v, nullptr, 1.0f, 1e-5f, 50, nullptr);
855 DOCTEST_CHECK(root == doctest::Approx(2.0f).epsilon(errtol));
856 }
857
858 SUBCASE("fzero finds root far from initial guess") {
859 std::vector<float> v;
860 float root = helios::fzero(linear, v, nullptr, -10.0f, 1e-6f, 50, nullptr);
861 DOCTEST_CHECK(root == doctest::Approx(3.5f).epsilon(errtol));
862 }
863
864 SUBCASE("fzero handles function without zero") {
865 std::vector<float> v;
866 WarningAggregator warnings;
867 float root = helios::fzero(flat, v, nullptr, 0.0f, 1e-6f, 10, &warnings);
868 DOCTEST_CHECK(std::isfinite(root));
869 // Should have a stagnation or convergence warning
870 bool has_stagnation = warnings.getCount("fzero_stagnation") > 0;
871 bool has_convergence = warnings.getCount("fzero_convergence_failure") > 0;
872 DOCTEST_CHECK((has_stagnation || has_convergence));
873 }
874
875 SUBCASE("fzero returns exact root at initial guess") {
876 std::vector<float> v;
877 float root = helios::fzero(quadratic, v, nullptr, 2.0f, 1e-6f, 5, nullptr);
878 DOCTEST_CHECK(root == doctest::Approx(2.0f).epsilon(errtol));
879 }
880
881 SUBCASE("fzero finds a cubic root") {
882 std::vector<float> v;
883 float root = helios::fzero(cubic, v, nullptr, 3.5f, 1e-5f, 80, nullptr);
884 DOCTEST_CHECK(root == doctest::Approx(4.0f).epsilon(errtol));
885 }
886
887 SUBCASE("fzero copes with near-singular derivative") {
888 std::vector<float> v;
889 float root = helios::fzero(near_singular, v, nullptr, 0.01f, 1e-4f, 50, nullptr);
890 DOCTEST_CHECK(std::fabs(near_singular(root, v, nullptr)) < 1e-4f);
891 }
892}
893
894TEST_CASE("linspace - Linearly Spaced Values") {
895 SUBCASE("linspace float basic functionality") {
896 std::vector<float> result = linspace(0.f, 10.f, 11);
897 DOCTEST_CHECK(result.size() == 11);
898 DOCTEST_CHECK(result[0] == doctest::Approx(0.f));
899 DOCTEST_CHECK(result[5] == doctest::Approx(5.f));
900 DOCTEST_CHECK(result[10] == doctest::Approx(10.f));
901
902 // Check uniformly spaced
903 for (size_t i = 1; i < result.size(); ++i) {
904 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(1.f));
905 }
906 }
907
908 SUBCASE("linspace float with negative range") {
909 std::vector<float> result = linspace(-5.f, 5.f, 6);
910 DOCTEST_CHECK(result.size() == 6);
911 DOCTEST_CHECK(result[0] == doctest::Approx(-5.f));
912 DOCTEST_CHECK(result[2] == doctest::Approx(-1.f));
913 DOCTEST_CHECK(result[5] == doctest::Approx(5.f));
914
915 // Check spacing
916 for (size_t i = 1; i < result.size(); ++i) {
917 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(2.f));
918 }
919 }
920
921 SUBCASE("linspace float single point") {
922 std::vector<float> result = linspace(5.f, 10.f, 1);
923 DOCTEST_CHECK(result.size() == 1);
924 DOCTEST_CHECK(result[0] == doctest::Approx(5.f));
925 }
926
927 SUBCASE("linspace float two points") {
928 std::vector<float> result = linspace(1.f, 3.f, 2);
929 DOCTEST_CHECK(result.size() == 2);
930 DOCTEST_CHECK(result[0] == doctest::Approx(1.f));
931 DOCTEST_CHECK(result[1] == doctest::Approx(3.f));
932 }
933
934 SUBCASE("linspace float reversed range") {
935 std::vector<float> result = linspace(10.f, 0.f, 6);
936 DOCTEST_CHECK(result.size() == 6);
937 DOCTEST_CHECK(result[0] == doctest::Approx(10.f));
938 DOCTEST_CHECK(result[2] == doctest::Approx(6.f));
939 DOCTEST_CHECK(result[5] == doctest::Approx(0.f));
940
941 // Check negative spacing
942 for (size_t i = 1; i < result.size(); ++i) {
943 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(-2.f));
944 }
945 }
946
947 SUBCASE("linspace float error handling") {
948 std::vector<float> result;
949 // Error messages from invalid input are expected - don't suppress them
950 DOCTEST_CHECK_THROWS(result = linspace(0.f, 1.f, 0));
951 DOCTEST_CHECK_THROWS(result = linspace(0.f, 1.f, -1));
952 }
953
954 SUBCASE("linspace vec2 basic functionality") {
955 vec2 start(0.f, 1.f);
956 vec2 end(4.f, 5.f);
957 std::vector<vec2> result = linspace(start, end, 5);
958
959 DOCTEST_CHECK(result.size() == 5);
960 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
961 DOCTEST_CHECK(result[0].y == doctest::Approx(1.f));
962 DOCTEST_CHECK(result[2].x == doctest::Approx(2.f));
963 DOCTEST_CHECK(result[2].y == doctest::Approx(3.f));
964 DOCTEST_CHECK(result[4].x == doctest::Approx(4.f));
965 DOCTEST_CHECK(result[4].y == doctest::Approx(5.f));
966 }
967
968 SUBCASE("linspace vec2 single point") {
969 vec2 start(1.f, 2.f);
970 vec2 end(3.f, 4.f);
971 std::vector<vec2> result = linspace(start, end, 1);
972
973 DOCTEST_CHECK(result.size() == 1);
974 DOCTEST_CHECK(result[0].x == doctest::Approx(start.x));
975 DOCTEST_CHECK(result[0].y == doctest::Approx(start.y));
976 }
977
978 SUBCASE("linspace vec2 error handling") {
979 std::vector<vec2> result;
980 vec2 start(0.f, 0.f);
981 vec2 end(1.f, 1.f);
982 // Error messages from invalid input are expected - don't suppress them
983 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
984 DOCTEST_CHECK_THROWS(result = linspace(start, end, -5));
985 }
986
987 SUBCASE("linspace vec3 basic functionality") {
988 vec3 start(0.f, 0.f, 0.f);
989 vec3 end(3.f, 6.f, 9.f);
990 std::vector<vec3> result = linspace(start, end, 4);
991
992 DOCTEST_CHECK(result.size() == 4);
993 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
994 DOCTEST_CHECK(result[0].y == doctest::Approx(0.f));
995 DOCTEST_CHECK(result[0].z == doctest::Approx(0.f));
996 DOCTEST_CHECK(result[1].x == doctest::Approx(1.f));
997 DOCTEST_CHECK(result[1].y == doctest::Approx(2.f));
998 DOCTEST_CHECK(result[1].z == doctest::Approx(3.f));
999 DOCTEST_CHECK(result[3].x == doctest::Approx(3.f));
1000 DOCTEST_CHECK(result[3].y == doctest::Approx(6.f));
1001 DOCTEST_CHECK(result[3].z == doctest::Approx(9.f));
1002 }
1003
1004 SUBCASE("linspace vec3 with mixed positive/negative components") {
1005 vec3 start(-1.f, 2.f, -3.f);
1006 vec3 end(1.f, -2.f, 3.f);
1007 std::vector<vec3> result = linspace(start, end, 3);
1008
1009 DOCTEST_CHECK(result.size() == 3);
1010 DOCTEST_CHECK(result[0].x == doctest::Approx(-1.f));
1011 DOCTEST_CHECK(result[0].y == doctest::Approx(2.f));
1012 DOCTEST_CHECK(result[0].z == doctest::Approx(-3.f));
1013 DOCTEST_CHECK(result[1].x == doctest::Approx(0.f));
1014 DOCTEST_CHECK(result[1].y == doctest::Approx(0.f));
1015 DOCTEST_CHECK(result[1].z == doctest::Approx(0.f));
1016 DOCTEST_CHECK(result[2].x == doctest::Approx(1.f));
1017 DOCTEST_CHECK(result[2].y == doctest::Approx(-2.f));
1018 DOCTEST_CHECK(result[2].z == doctest::Approx(3.f));
1019 }
1020
1021 SUBCASE("linspace vec3 error handling") {
1022 std::vector<vec3> result;
1023 vec3 start(0.f, 0.f, 0.f);
1024 vec3 end(1.f, 1.f, 1.f);
1025 // Error messages from invalid input are expected - don't suppress them
1026 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
1027 DOCTEST_CHECK_THROWS(result = linspace(start, end, -10));
1028 }
1029
1030 SUBCASE("linspace vec4 basic functionality") {
1031 vec4 start(0.f, 1.f, 2.f, 3.f);
1032 vec4 end(4.f, 9.f, 14.f, 19.f);
1033 std::vector<vec4> result = linspace(start, end, 5);
1034
1035 DOCTEST_CHECK(result.size() == 5);
1036 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
1037 DOCTEST_CHECK(result[0].y == doctest::Approx(1.f));
1038 DOCTEST_CHECK(result[0].z == doctest::Approx(2.f));
1039 DOCTEST_CHECK(result[0].w == doctest::Approx(3.f));
1040 DOCTEST_CHECK(result[2].x == doctest::Approx(2.f));
1041 DOCTEST_CHECK(result[2].y == doctest::Approx(5.f));
1042 DOCTEST_CHECK(result[2].z == doctest::Approx(8.f));
1043 DOCTEST_CHECK(result[2].w == doctest::Approx(11.f));
1044 DOCTEST_CHECK(result[4].x == doctest::Approx(4.f));
1045 DOCTEST_CHECK(result[4].y == doctest::Approx(9.f));
1046 DOCTEST_CHECK(result[4].z == doctest::Approx(14.f));
1047 DOCTEST_CHECK(result[4].w == doctest::Approx(19.f));
1048 }
1049
1050 SUBCASE("linspace vec4 two points") {
1051 vec4 start(1.f, 2.f, 3.f, 4.f);
1052 vec4 end(5.f, 6.f, 7.f, 8.f);
1053 std::vector<vec4> result = linspace(start, end, 2);
1054
1055 DOCTEST_CHECK(result.size() == 2);
1056 DOCTEST_CHECK(result[0] == start);
1057 DOCTEST_CHECK(result[1] == end);
1058 }
1059
1060 SUBCASE("linspace vec4 error handling") {
1061 std::vector<vec4> result;
1062 vec4 start(0.f, 0.f, 0.f, 0.f);
1063 vec4 end(1.f, 1.f, 1.f, 1.f);
1064 // Error messages from invalid input are expected - don't suppress them
1065 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
1066 DOCTEST_CHECK_THROWS(result = linspace(start, end, -1));
1067 }
1068
1069 SUBCASE("linspace precision and endpoint accuracy") {
1070 // Test that endpoints are exactly preserved despite floating point arithmetic
1071 std::vector<float> result = linspace(0.1f, 0.9f, 9);
1072 DOCTEST_CHECK(result[0] == doctest::Approx(0.1f));
1073 DOCTEST_CHECK(result[8] == doctest::Approx(0.9f));
1074
1075 // Test with large values
1076 result = linspace(1000000.f, 2000000.f, 11);
1077 DOCTEST_CHECK(result[0] == doctest::Approx(1000000.f));
1078 DOCTEST_CHECK(result[10] == doctest::Approx(2000000.f));
1079
1080 // Test with very small values
1081 result = linspace(1e-6f, 2e-6f, 3);
1082 DOCTEST_CHECK(result[0] == doctest::Approx(1e-6f));
1083 DOCTEST_CHECK(result[2] == doctest::Approx(2e-6f));
1084 }
1085
1086 SUBCASE("linspace zero-length intervals") {
1087 // Test when start == end
1088 std::vector<float> result = linspace(5.f, 5.f, 5);
1089 DOCTEST_CHECK(result.size() == 5);
1090 for (const auto &val: result) {
1091 DOCTEST_CHECK(val == doctest::Approx(5.f));
1092 }
1093
1094 // Test vec3 with zero-length interval
1095 vec3 point(1.f, 2.f, 3.f);
1096 std::vector<vec3> vec_result = linspace(point, point, 3);
1097 DOCTEST_CHECK(vec_result.size() == 3);
1098 for (const auto &v: vec_result) {
1099 DOCTEST_CHECK(v.x == doctest::Approx(point.x));
1100 DOCTEST_CHECK(v.y == doctest::Approx(point.y));
1101 DOCTEST_CHECK(v.z == doctest::Approx(point.z));
1102 }
1103 }
1104}
1105
1106TEST_CASE("Asset Resolution Functions") {
1107 SUBCASE("resolveAssetPath basic functionality") {
1108 // Test that resolveAssetPath works correctly - should throw for non-existent file
1109 bool exception_thrown = false;
1110 try {
1111 [[maybe_unused]] auto path = resolveAssetPath("nonexistent_test_file.txt");
1112 } catch (const std::runtime_error &) {
1113 exception_thrown = true;
1114 }
1115 DOCTEST_CHECK(exception_thrown);
1116 }
1117
1118 SUBCASE("resolvePluginAsset") {
1119 // Test plugin-specific asset resolution - should throw for non-existent file
1120 bool exception_thrown = false;
1121 try {
1122 [[maybe_unused]] auto path = resolvePluginAsset("visualizer", "nonexistent_font.ttf");
1123 } catch (const std::runtime_error &) {
1124 exception_thrown = true;
1125 }
1126 DOCTEST_CHECK(exception_thrown);
1127 }
1128
1129
1130 SUBCASE("resolveSpectraPath") {
1131 // Test spectra path resolution - should throw for non-existent file
1132 bool exception_thrown = false;
1133 try {
1134 [[maybe_unused]] auto path = resolveSpectraPath("nonexistent_spectra.xml");
1135 } catch (const std::runtime_error &) {
1136 exception_thrown = true;
1137 }
1138 DOCTEST_CHECK(exception_thrown);
1139 }
1140
1141 SUBCASE("validateAssetPath with valid path") {
1142 // Create a temporary file for testing
1143 std::filesystem::path temp_path = std::filesystem::temp_directory_path() / "helios_test_asset.txt";
1144 std::ofstream temp_file(temp_path);
1145 temp_file << "test content";
1146 temp_file.close();
1147
1148 // Test validation of existing file
1149 DOCTEST_CHECK(validateAssetPath(temp_path) == true);
1150
1151 // Clean up
1152 std::filesystem::remove(temp_path);
1153 }
1154
1155 SUBCASE("validateAssetPath with invalid path") {
1156 // Test validation of non-existent file
1157 std::filesystem::path non_existent = "/this/path/does/not/exist.txt";
1158 DOCTEST_CHECK(validateAssetPath(non_existent) == false);
1159 }
1160
1161 SUBCASE("resolveAssetPath with empty string") {
1162 // Test with empty input - should return a path (empty string is handled gracefully)
1163 auto path = resolveAssetPath("");
1164 DOCTEST_CHECK(!path.empty());
1165 DOCTEST_CHECK(path.is_absolute());
1166 }
1167
1168 SUBCASE("error message content") {
1169 // Test that error messages contain helpful information
1170 try {
1171 [[maybe_unused]] auto path = resolveAssetPath("nonexistent_file.txt");
1172 DOCTEST_FAIL("Expected exception was not thrown");
1173 } catch (const std::runtime_error &e) {
1174 std::string error_msg = e.what();
1175 DOCTEST_CHECK(error_msg.find("Could not locate asset file") != std::string::npos);
1176 DOCTEST_CHECK(error_msg.find("nonexistent_file.txt") != std::string::npos);
1177 }
1178 }
1179
1180 SUBCASE("asset resolution consistency") {
1181 // Test that multiple calls with same input produce consistent error messages
1182 std::string error1, error2;
1183 try {
1184 [[maybe_unused]] auto path1 = resolveAssetPath("test_consistency.txt");
1185 } catch (const std::runtime_error &e) {
1186 error1 = e.what();
1187 }
1188 try {
1189 [[maybe_unused]] auto path2 = resolveAssetPath("test_consistency.txt");
1190 } catch (const std::runtime_error &e) {
1191 error2 = e.what();
1192 }
1193 DOCTEST_CHECK(error1 == error2);
1194 }
1195
1196 SUBCASE("different plugin error messages") {
1197 // Test that different plugins produce different error messages
1198 std::string vis_error, rad_error;
1199 try {
1200 [[maybe_unused]] auto vis_path = resolvePluginAsset("visualizer", "test.txt");
1201 } catch (const std::runtime_error &e) {
1202 vis_error = e.what();
1203 }
1204 try {
1205 [[maybe_unused]] auto rad_path = resolvePluginAsset("radiation", "test.txt");
1206 } catch (const std::runtime_error &e) {
1207 rad_error = e.what();
1208 }
1209 // Error messages should be different for different plugins
1210 DOCTEST_CHECK(vis_error != rad_error);
1211 DOCTEST_CHECK(vis_error.find("visualizer") != std::string::npos);
1212 DOCTEST_CHECK(rad_error.find("radiation") != std::string::npos);
1213 }
1214}
1215
1216TEST_CASE("Project-based File Resolution") {
1217 SUBCASE("findProjectRoot basic functionality") {
1218 // Test from current working directory (which should contain CMakeLists.txt)
1219 std::filesystem::path cwd = std::filesystem::current_path();
1220 auto project_root = findProjectRoot(cwd);
1221
1222 // Should find a project root (not empty)
1223 DOCTEST_CHECK(!project_root.empty());
1224
1225 // Project root should contain CMakeLists.txt
1226 auto cmake_file = project_root / "CMakeLists.txt";
1227 DOCTEST_CHECK(std::filesystem::exists(cmake_file));
1228 }
1229
1230 SUBCASE("findProjectRoot with non-existent path") {
1231 // Test with a path that doesn't exist
1232 std::filesystem::path fake_path = "/this/path/does/not/exist";
1233 auto project_root = findProjectRoot(fake_path);
1234
1235 // Should return empty path when no project found
1236 DOCTEST_CHECK(project_root.empty());
1237 }
1238
1239 SUBCASE("findProjectRoot from root directory") {
1240 // Test from system root directory (should not find CMakeLists.txt)
1241 std::filesystem::path root_path = "/";
1242 auto project_root = findProjectRoot(root_path);
1243
1244 // Should return empty path when searching from root
1245 DOCTEST_CHECK(project_root.empty());
1246 }
1247
1248 SUBCASE("resolveProjectFile with existing file in cwd") {
1249 // Create a temporary test file in current directory
1250 std::string test_filename = "test_project_resolve.tmp";
1251 std::ofstream test_file(test_filename);
1252 test_file << "test content";
1253 test_file.close();
1254
1255 try {
1256 // Should find file in current working directory
1257 auto resolved_path = resolveProjectFile(test_filename);
1258 DOCTEST_CHECK(!resolved_path.empty());
1259 DOCTEST_CHECK(std::filesystem::exists(resolved_path));
1260 } catch (...) {
1261 // Clean up even if test fails
1262 std::filesystem::remove(test_filename);
1263 throw;
1264 }
1265
1266 // Clean up
1267 std::filesystem::remove(test_filename);
1268 }
1269
1270 SUBCASE("resolveProjectFile with non-existent file") {
1271 // Test with file that doesn't exist in current directory or project
1272 std::string fake_filename = "this_file_does_not_exist_anywhere.tmp";
1273
1274 std::string error_message;
1275 try {
1276 [[maybe_unused]] auto resolved_path = resolveProjectFile(fake_filename);
1277 } catch (const std::runtime_error &e) {
1278 error_message = e.what();
1279 }
1280
1281 // Should throw runtime error for non-existent file
1282 DOCTEST_CHECK(!error_message.empty());
1283 DOCTEST_CHECK(error_message.find("Could not locate file") != std::string::npos);
1284 DOCTEST_CHECK(error_message.find(fake_filename) != std::string::npos);
1285 }
1286
1287 SUBCASE("resolveProjectFile with empty filename") {
1288 // Test with empty filename
1289 std::string error_message;
1290 try {
1291 [[maybe_unused]] auto resolved_path = resolveProjectFile("");
1292 } catch (const std::runtime_error &e) {
1293 error_message = e.what();
1294 }
1295
1296 // Should handle empty filename gracefully
1297 DOCTEST_CHECK(!error_message.empty());
1298 }
1299
1300 SUBCASE("resolveProjectFile project directory fallback") {
1301 // This test verifies the fallback to project directory works
1302 // We'll create a test file in the project root and try to access it from a subdirectory
1303 auto project_root = findProjectRoot(std::filesystem::current_path());
1304 if (!project_root.empty()) {
1305 std::string test_filename = "test_project_fallback.tmp";
1306 auto test_file_path = project_root / test_filename;
1307
1308 // Create test file in project root
1309 std::ofstream test_file(test_file_path);
1310 test_file << "fallback test content";
1311 test_file.close();
1312
1313 try {
1314 // Should find file in project directory even if not in cwd
1315 auto resolved_path = resolveProjectFile(test_filename);
1316 DOCTEST_CHECK(!resolved_path.empty());
1317 DOCTEST_CHECK(std::filesystem::exists(resolved_path));
1318 DOCTEST_CHECK(resolved_path == test_file_path);
1319 } catch (...) {
1320 // Clean up even if test fails
1321 std::filesystem::remove(test_file_path);
1322 throw;
1323 }
1324
1325 // Clean up
1326 std::filesystem::remove(test_file_path);
1327 }
1328 }
1329}
1330
1331TEST_CASE("WarningAggregator") {
1332
1333 SUBCASE("Basic accumulation") {
1334 WarningAggregator agg;
1335 agg.addWarning("test_category", "test message 1");
1336 agg.addWarning("test_category", "test message 2");
1337 agg.addWarning("test_category", "test message 3");
1338
1339 DOCTEST_CHECK(agg.getCount("test_category") == 3);
1340 DOCTEST_CHECK(agg.getCount("nonexistent_category") == 0);
1341 }
1342
1343 SUBCASE("Multiple categories") {
1344 WarningAggregator agg;
1345 agg.addWarning("category_a", "message A1");
1346 agg.addWarning("category_a", "message A2");
1347 agg.addWarning("category_b", "message B1");
1348 agg.addWarning("category_b", "message B2");
1349 agg.addWarning("category_b", "message B3");
1350
1351 DOCTEST_CHECK(agg.getCount("category_a") == 2);
1352 DOCTEST_CHECK(agg.getCount("category_b") == 3);
1353 }
1354
1355 SUBCASE("Enable and disable") {
1356 WarningAggregator agg;
1357
1358 // Should be enabled by default
1359 DOCTEST_CHECK(agg.isEnabled());
1360
1361 agg.addWarning("test", "message 1");
1362 DOCTEST_CHECK(agg.getCount("test") == 1);
1363
1364 // Disable and add more warnings
1365 agg.setEnabled(false);
1366 DOCTEST_CHECK(!agg.isEnabled());
1367 agg.addWarning("test", "message 2");
1368 agg.addWarning("test", "message 3");
1369
1370 // Count should not increase when disabled
1371 DOCTEST_CHECK(agg.getCount("test") == 1);
1372
1373 // Re-enable
1374 agg.setEnabled(true);
1375 agg.addWarning("test", "message 4");
1376 DOCTEST_CHECK(agg.getCount("test") == 2);
1377 }
1378
1379 SUBCASE("Clear warnings") {
1380 WarningAggregator agg;
1381 agg.addWarning("test", "message 1");
1382 agg.addWarning("test", "message 2");
1383 DOCTEST_CHECK(agg.getCount("test") == 2);
1384
1385 agg.clear();
1386 DOCTEST_CHECK(agg.getCount("test") == 0);
1387 }
1388
1389 SUBCASE("Report to stream") {
1390 WarningAggregator agg;
1391 agg.addWarning("convergence_failure", "fzero did not converge after 100 iterations.");
1392 agg.addWarning("convergence_failure", "fzero did not converge after 100 iterations.");
1393 agg.addWarning("convergence_failure", "fzero did not converge after 100 iterations.");
1394
1395 // Capture output using capture_cerr utility
1396 std::ostringstream oss;
1397 agg.report(oss);
1398
1399 std::string output = oss.str();
1400
1401 // Check that output contains key information
1402 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
1403 DOCTEST_CHECK(output.find("3 instances") != std::string::npos);
1404 DOCTEST_CHECK(output.find("convergence_failure") != std::string::npos);
1405 DOCTEST_CHECK(output.find("showing first 3") != std::string::npos);
1406
1407 // After reporting, warnings should be cleared
1408 DOCTEST_CHECK(agg.getCount("convergence_failure") == 0);
1409 }
1410
1411 SUBCASE("Report with many warnings") {
1412 WarningAggregator agg;
1413
1414 // Add more than 3 warnings to test "showing first 3" behavior
1415 for (int i = 0; i < 10; i++) {
1416 agg.addWarning("test_many", "Warning message " + std::to_string(i));
1417 }
1418
1419 std::ostringstream oss;
1420 agg.report(oss);
1421
1422 std::string output = oss.str();
1423
1424 DOCTEST_CHECK(output.find("10 instances") != std::string::npos);
1425 DOCTEST_CHECK(output.find("showing first 3") != std::string::npos);
1426
1427 // Should show first 3 messages
1428 DOCTEST_CHECK(output.find("Warning message 0") != std::string::npos);
1429 DOCTEST_CHECK(output.find("Warning message 1") != std::string::npos);
1430 DOCTEST_CHECK(output.find("Warning message 2") != std::string::npos);
1431
1432 // Should not show later messages in detail
1433 DOCTEST_CHECK(output.find("Warning message 9") == std::string::npos);
1434 }
1435
1436 SUBCASE("Empty report") {
1437 WarningAggregator agg;
1438
1439 std::ostringstream oss;
1440 agg.report(oss);
1441
1442 // Empty aggregator should produce no output
1443 DOCTEST_CHECK(oss.str().empty());
1444 }
1445
1446 SUBCASE("Thread safety with OpenMP") {
1447#ifdef USE_OPENMP
1448 WarningAggregator agg;
1449
1450 const int num_threads = 4;
1451 const int warnings_per_thread = 250;
1452
1453#pragma omp parallel for num_threads(num_threads)
1454 for (int i = 0; i < num_threads * warnings_per_thread; i++) {
1455 agg.addWarning("parallel_test", "message from thread");
1456 }
1457
1458 // All warnings should be accumulated correctly
1459 DOCTEST_CHECK(agg.getCount("parallel_test") == num_threads * warnings_per_thread);
1460#endif
1461 }
1462
1463 SUBCASE("Maximum examples limit") {
1464 WarningAggregator agg;
1465
1466 // Add more than MAX_EXAMPLES (100) warnings
1467 for (int i = 0; i < 150; i++) {
1468 agg.addWarning("many_warnings", "Warning " + std::to_string(i));
1469 }
1470
1471 // Should count all warnings even though only MAX_EXAMPLES are stored
1472 DOCTEST_CHECK(agg.getCount("many_warnings") == 150);
1473
1474 std::ostringstream oss;
1475 agg.report(oss);
1476
1477 std::string output = oss.str();
1478
1479 // Output should indicate all 150 warnings were encountered
1480 DOCTEST_CHECK(output.find("150 instances") != std::string::npos);
1481 // Should also note that more than MAX_EXAMPLES were encountered
1482 DOCTEST_CHECK(output.find("More than 100 warnings") != std::string::npos);
1483 }
1484
1485 SUBCASE("Report with single vs multiple instances") {
1486 WarningAggregator agg;
1487
1488 // Single instance
1489 agg.addWarning("single", "Only one warning");
1490
1491 std::ostringstream oss1;
1492 agg.report(oss1);
1493 std::string output1 = oss1.str();
1494
1495 // Should say "1 instance" (singular)
1496 DOCTEST_CHECK(output1.find("1 instance") != std::string::npos);
1497 // Check it doesn't say "instances" in plural form after "1"
1498 auto instances_pos = output1.find(" instances");
1499 auto one_instance_pos = output1.find("1 instance");
1500 bool is_singular = (instances_pos == std::string::npos) || (one_instance_pos < instances_pos);
1501 DOCTEST_CHECK(is_singular);
1502
1503 // Multiple instances
1504 agg.addWarning("multiple", "Warning 1");
1505 agg.addWarning("multiple", "Warning 2");
1506
1507 std::ostringstream oss2;
1508 agg.report(oss2);
1509 std::string output2 = oss2.str();
1510
1511 // Should say "2 instances" (plural)
1512 DOCTEST_CHECK(output2.find("2 instances") != std::string::npos);
1513 }
1514}