1.3.49
 
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 capture_cerr cerr_buffer;
169 vec3 res_low = spline_interp3(-0.5f, p0, t0, p1, t1);
170 DOCTEST_CHECK(res_low.x == doctest::Approx(p0.x));
171 vec3 res_high = spline_interp3(1.5f, p0, t0, p1, t1);
172 DOCTEST_CHECK(res_high.x == doctest::Approx(p1.x));
173 }
174 SUBCASE("Angle conversion helpers") {
175 CHECK(deg2rad(180.f) == doctest::Approx(PI_F).epsilon(errtol));
176 CHECK(rad2deg(PI_F) == doctest::Approx(180.f).epsilon(errtol));
177
178 CHECK(atan2_2pi(0.f, 1.f) == doctest::Approx(0.f).epsilon(errtol));
179 CHECK(atan2_2pi(1.f, 0.f) == doctest::Approx(0.5f * PI_F * 2.f / 2.f).epsilon(errtol));
180 CHECK(atan2_2pi(0.f, -1.f) == doctest::Approx(PI_F).epsilon(errtol));
181 CHECK(atan2_2pi(-1.f, 0.f) == doctest::Approx(1.5f * PI_F).epsilon(errtol));
182 }
183
184 SUBCASE("clamp template for common types") {
185 CHECK(clamp(5, 1, 4) == 4);
186 CHECK(clamp(-1, 0, 10) == 0);
187 CHECK(clamp(3.5f, 0.f, 10.f) == doctest::Approx(3.5f).epsilon(errtol));
188 CHECK(clamp(12.0, -5.0, 11.0) == doctest::Approx(11.0).epsilon(errtol));
189 }
190}
191
192TEST_CASE("Matrix and Transformation Helpers") {
193 float M[16], T[16], R[16];
194
195 SUBCASE("makeIdentityMatrix") {
197 DOCTEST_CHECK(T[0] == 1.f);
198 DOCTEST_CHECK(T[1] == 0.f);
199 DOCTEST_CHECK(T[2] == 0.f);
200 DOCTEST_CHECK(T[3] == 0.f);
201 DOCTEST_CHECK(T[4] == 0.f);
202 DOCTEST_CHECK(T[5] == 1.f);
203 DOCTEST_CHECK(T[6] == 0.f);
204 DOCTEST_CHECK(T[7] == 0.f);
205 DOCTEST_CHECK(T[8] == 0.f);
206 DOCTEST_CHECK(T[9] == 0.f);
207 DOCTEST_CHECK(T[10] == 1.f);
208 DOCTEST_CHECK(T[11] == 0.f);
209 DOCTEST_CHECK(T[12] == 0.f);
210 DOCTEST_CHECK(T[13] == 0.f);
211 DOCTEST_CHECK(T[14] == 0.f);
212 DOCTEST_CHECK(T[15] == 1.f);
213 }
214
215 SUBCASE("makeRotationMatrix") {
216 // About x-axis
217 makeRotationMatrix(PI_F / 2.f, "x", T);
218 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
219 DOCTEST_CHECK(T[6] == doctest::Approx(-1.f));
220 DOCTEST_CHECK(T[9] == doctest::Approx(1.f));
221 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
222
223 // About y-axis
224 makeRotationMatrix(PI_F / 2.f, "y", T);
225 DOCTEST_CHECK(T[0] == doctest::Approx(0.f));
226 DOCTEST_CHECK(T[2] == doctest::Approx(1.f));
227 DOCTEST_CHECK(T[8] == doctest::Approx(-1.f));
228 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
229
230 // About z-axis
231 makeRotationMatrix(PI_F / 2.f, "z", T);
232 DOCTEST_CHECK(T[0] == doctest::Approx(0.f));
233 DOCTEST_CHECK(T[1] == doctest::Approx(-1.f));
234 DOCTEST_CHECK(T[4] == doctest::Approx(1.f));
235 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
236
237 // About arbitrary axis
238 vec3 axis(1, 0, 0);
239 makeRotationMatrix(PI_F / 2.f, axis, T);
240 DOCTEST_CHECK(T[5] == doctest::Approx(0.f));
241 DOCTEST_CHECK(T[6] == doctest::Approx(-1.f));
242 DOCTEST_CHECK(T[9] == doctest::Approx(1.f));
243 DOCTEST_CHECK(T[10] == doctest::Approx(0.f));
244 }
245
246 SUBCASE("makeRotationMatrix invalid axis") {
247 capture_cerr cerr_buffer;
248 float T[16];
249 DOCTEST_CHECK_THROWS(makeRotationMatrix(0.5f, "w", T));
250 }
251
252 SUBCASE("makeTranslationMatrix") {
253 makeTranslationMatrix(vec3(1, 2, 3), T);
254 DOCTEST_CHECK(T[3] == 1.f);
255 DOCTEST_CHECK(T[7] == 2.f);
256 DOCTEST_CHECK(T[11] == 3.f);
257 }
258
259 SUBCASE("makeScaleMatrix") {
260 // About origin
261 makeScaleMatrix(vec3(2, 3, 4), T);
262 DOCTEST_CHECK(T[0] == 2.f);
263 DOCTEST_CHECK(T[5] == 3.f);
264 DOCTEST_CHECK(T[10] == 4.f);
265
266 // About point
267 makeScaleMatrix(vec3(2, 2, 2), vec3(1, 1, 1), T);
268 vec3 v(2, 2, 2);
269 vec3 res;
270 vecmult(T, v, res);
271 DOCTEST_CHECK(res.x == doctest::Approx(3.f));
272 DOCTEST_CHECK(res.y == doctest::Approx(3.f));
273 DOCTEST_CHECK(res.z == doctest::Approx(3.f));
274 }
275
276 SUBCASE("matmult") {
277 makeRotationMatrix(PI_F / 2.f, "x", M);
278 makeRotationMatrix(-PI_F / 2.f, "x", T);
279 matmult(M, T, R); // Should be identity
280 DOCTEST_CHECK(R[0] == doctest::Approx(1.f));
281 DOCTEST_CHECK(R[5] == doctest::Approx(1.f));
282 DOCTEST_CHECK(R[10] == doctest::Approx(1.f));
283 DOCTEST_CHECK(R[15] == doctest::Approx(1.f));
284 }
285
286 SUBCASE("vecmult") {
287 makeRotationMatrix(PI_F / 2.f, "z", M);
288 vec3 v(1, 0, 0);
289 vec3 res;
290 vecmult(M, v, res);
291 DOCTEST_CHECK(res.x == doctest::Approx(0.f));
292 DOCTEST_CHECK(res.y == doctest::Approx(1.f));
293 DOCTEST_CHECK(res.z == doctest::Approx(0.f));
294
295 float v_arr[3] = {1, 0, 0};
296 float res_arr[3];
297 vecmult(M, v_arr, res_arr);
298 DOCTEST_CHECK(res_arr[0] == doctest::Approx(0.f));
299 DOCTEST_CHECK(res_arr[1] == doctest::Approx(1.f));
300 DOCTEST_CHECK(res_arr[2] == doctest::Approx(0.f));
301 }
302
303 SUBCASE("makeIdentityMatrix / matmult / vecmult") {
304 float I[16] = {};
306 for (int r = 0; r < 4; ++r) {
307 for (int c = 0; c < 4; ++c) {
308 CHECK(I[4 * r + c] == doctest::Approx(r == c ? 1.f : 0.f).epsilon(errtol));
309 }
310 }
311
312 float T[16];
313 makeTranslationMatrix(make_vec3(1.f, 2.f, 3.f), T);
314 float R[16];
315 matmult(I, T, R); // I * T == T
316 for (int i = 0; i < 16; ++i) {
317 CHECK(R[i] == doctest::Approx(T[i]).epsilon(errtol));
318 }
319
320 float v[3] = {4.f, 5.f, 6.f}, out[3] = {};
321 vecmult(I, v, out);
322 CHECK(out[0] == doctest::Approx(4.f).epsilon(errtol));
323 CHECK(out[1] == doctest::Approx(5.f).epsilon(errtol));
324 CHECK(out[2] == doctest::Approx(6.f).epsilon(errtol));
325 }
326
327 SUBCASE("rotatePoint & rotatePointAboutLine") {
328 vec3 p = make_vec3(1, 0, 0);
329 // Rotate 90° about z -> y axis
330 vec3 r = rotatePoint(p, 0.f, 0.5f * PI_F);
331 CHECK(r.x == doctest::Approx(0.f).epsilon(errtol));
332 CHECK(r.y == doctest::Approx(1.f).epsilon(errtol));
333
334 // Rotate about arbitrary line (x-axis) back to original
335 vec3 q = rotatePointAboutLine(r, make_vec3(0, 0, 0), make_vec3(1, 0, 0), -0.5f * PI_F);
336 CHECK(q.x == doctest::Approx(0.f).epsilon(errtol));
337 CHECK(q.y == doctest::Approx(0.f).epsilon(errtol));
338 }
339}
340
341TEST_CASE("String, File Path, and Parsing Utilities") {
342 SUBCASE("String Manipulation") {
343 DOCTEST_CHECK(deblank(" hello world ") == "helloworld");
344 DOCTEST_CHECK(trim_whitespace(" hello world ") == "hello world");
345 }
346 SUBCASE("String Delimiting") {
347 std::vector<std::string> result = separate_string_by_delimiter("a,b,c", ",");
348 DOCTEST_CHECK(result.size() == 3);
349 if (result.size() == 3) {
350 DOCTEST_CHECK(result[0] == "a");
351 DOCTEST_CHECK(result[1] == "b");
352 DOCTEST_CHECK(result[2] == "c");
353 }
354 }
355 SUBCASE("separate_string_by_delimiter edge cases") {
356 std::vector<std::string> result;
357 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("a,b,c", ";"));
358 DOCTEST_CHECK(result.size() == 1);
359 DOCTEST_CHECK(result[0] == "a,b,c");
360
361 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("a|b|c", "|"));
362 DOCTEST_CHECK(result.size() == 3);
363 DOCTEST_CHECK(result[0] == "a");
364 DOCTEST_CHECK(result[1] == "b");
365 DOCTEST_CHECK(result[2] == "c");
366
367 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter("", ","));
368 DOCTEST_CHECK(result.size() == 1);
369
370 capture_cerr cerr_buffer;
371 DOCTEST_CHECK_THROWS(result = separate_string_by_delimiter("a,b,c", ""));
372
373 DOCTEST_CHECK_NOTHROW(result = separate_string_by_delimiter(",", ","));
374 DOCTEST_CHECK(result.size() == 2);
375 if (result.size() == 2) {
376 DOCTEST_CHECK(result[0] == "");
377 DOCTEST_CHECK(result[1] == "");
378 }
379 }
380 SUBCASE("String to Vector Conversions") {
381 DOCTEST_CHECK(string2vec2("1.5 2.5") == vec2(1.5f, 2.5f));
382 DOCTEST_CHECK(string2vec3("1.5 2.5 3.5") == vec3(1.5f, 2.5f, 3.5f));
383 DOCTEST_CHECK(string2vec4("1.5 2.5 3.5 4.5") == vec4(1.5f, 2.5f, 3.5f, 4.5f));
384 DOCTEST_CHECK(string2int2("1 2") == int2(1, 2));
385 DOCTEST_CHECK(string2int3("1 2 3") == int3(1, 2, 3));
386 DOCTEST_CHECK(string2int4("1 2 3 4") == int4(1, 2, 3, 4));
387 }
388 SUBCASE("string to vector conversions with invalid input") {
389 capture_cerr cerr_buffer;
390 vec2 result_vec2;
391 vec3 result_vec3;
392 vec4 result_vec4;
393 int2 result_int2;
394 int3 result_int3;
395 int4 result_int4;
396 DOCTEST_CHECK_THROWS(result_vec2 = string2vec2("1.5"));
397 DOCTEST_CHECK_THROWS(result_vec3 = string2vec3("1.5 2.5"));
398 DOCTEST_CHECK_THROWS(result_vec4 = string2vec4("1.5 2.5 3.5"));
399 DOCTEST_CHECK_THROWS(result_int2 = string2int2("1"));
400 DOCTEST_CHECK_THROWS(result_int3 = string2int3("1 2"));
401 DOCTEST_CHECK_THROWS(result_int4 = string2int4("1 2 3"));
402 DOCTEST_CHECK_THROWS(result_vec2 = string2vec2("1.5 abc"));
403 }
404
405 SUBCASE("String to Color Conversion") {
406 RGBAcolor color = string2RGBcolor("0.1 0.2 0.3 0.4");
407 DOCTEST_CHECK(color.r == doctest::Approx(0.1f));
408 DOCTEST_CHECK(color.g == doctest::Approx(0.2f));
409 DOCTEST_CHECK(color.b == doctest::Approx(0.3f));
410 DOCTEST_CHECK(color.a == doctest::Approx(0.4f));
411
412 color = string2RGBcolor("0.5 0.6 0.7");
413 DOCTEST_CHECK(color.r == doctest::Approx(0.5f));
414 DOCTEST_CHECK(color.g == doctest::Approx(0.6f));
415 DOCTEST_CHECK(color.b == doctest::Approx(0.7f));
416 DOCTEST_CHECK(color.a == doctest::Approx(1.0f));
417 }
418 SUBCASE("string2RGBcolor with invalid input") {
419 capture_cerr cerr_buffer;
420 RGBAcolor result;
421 DOCTEST_CHECK_THROWS(result = string2RGBcolor("0.1 0.2"));
422 DOCTEST_CHECK_THROWS(result = string2RGBcolor("0.1 0.2 0.3 0.4 0.5"));
423 DOCTEST_CHECK_THROWS(result = string2RGBcolor("a b c"));
424 }
425 SUBCASE("File Path Parsing") {
426 std::string filepath = "/path/to/file/filename.ext";
427 DOCTEST_CHECK(getFileExtension(filepath) == ".ext");
428 DOCTEST_CHECK(getFileName(filepath) == "filename.ext");
429 DOCTEST_CHECK(getFileStem(filepath) == "filename");
430#ifndef _WIN32
431 DOCTEST_CHECK(getFilePath(filepath, true) == "/path/to/file/");
432 DOCTEST_CHECK(getFilePath(filepath, false) == "/path/to/file");
433#endif
434
435 std::string filepath_noext = "/path/to/file/filename";
436 DOCTEST_CHECK(getFileExtension(filepath_noext).empty());
437 DOCTEST_CHECK(getFileName(filepath_noext) == "filename");
438 DOCTEST_CHECK(getFileStem(filepath_noext) == "filename");
439
440 std::string filepath_nodir = "filename.ext";
441 DOCTEST_CHECK(getFileExtension(filepath_nodir) == ".ext");
442 DOCTEST_CHECK(getFileName(filepath_nodir) == "filename.ext");
443 DOCTEST_CHECK(getFileStem(filepath_nodir) == "filename");
444 DOCTEST_CHECK(getFilePath(filepath_nodir, true).empty());
445 }
446 SUBCASE("File path parsing edge cases") {
447 DOCTEST_CHECK(getFileExtension(".bashrc").empty());
448 DOCTEST_CHECK(getFileName(".bashrc") == ".bashrc");
449 DOCTEST_CHECK(getFileStem(".bashrc") == ".bashrc");
450#ifndef _WIN32
451 DOCTEST_CHECK(getFilePath("/path/to/file/", true) == "/path/to/file/");
452 DOCTEST_CHECK(getFilePath("/path/to/file/", false) == "/path/to/file");
453#endif
454 DOCTEST_CHECK(getFilePath("..", true).empty());
455 DOCTEST_CHECK(getFileName("..") == "..");
456 DOCTEST_CHECK(getFileStem("..") == "..");
457 }
458 SUBCASE("Primitive Type Parsing") {
459 float f;
460 DOCTEST_CHECK(parse_float("1.23", f));
461 DOCTEST_CHECK(f == doctest::Approx(1.23f));
462 DOCTEST_CHECK(!parse_float("abc", f));
463
464 double d;
465 DOCTEST_CHECK(parse_double("1.23", d));
466 DOCTEST_CHECK(d == doctest::Approx(1.23));
467 DOCTEST_CHECK(!parse_double("abc", d));
468
469 int i;
470 DOCTEST_CHECK(parse_int("123", i));
471 DOCTEST_CHECK(i == 123);
472 DOCTEST_CHECK(!parse_int("abc", i));
473 DOCTEST_CHECK(!parse_int("1.23", i));
474
475 unsigned int u;
476 DOCTEST_CHECK(parse_uint("123", u));
477 DOCTEST_CHECK(u == 123u);
478 DOCTEST_CHECK(!parse_uint("-123", u));
479 }
480 SUBCASE("Compound Type Parsing") {
481 int2 i2;
482 DOCTEST_CHECK(parse_int2("1 2", i2));
483 DOCTEST_CHECK(i2 == int2(1, 2));
484 DOCTEST_CHECK(!parse_int2("1", i2));
485
486 int3 i3;
487 DOCTEST_CHECK(parse_int3("1 2 3", i3));
488 DOCTEST_CHECK(i3 == int3(1, 2, 3));
489 DOCTEST_CHECK(!parse_int3("1 2", i3));
490
491 vec2 v2;
492 DOCTEST_CHECK(parse_vec2("1.1 2.2", v2));
493 DOCTEST_CHECK(v2.x == doctest::Approx(1.1f));
494 DOCTEST_CHECK(v2.y == doctest::Approx(2.2f));
495 DOCTEST_CHECK(!parse_vec2("1.1", v2));
496
497 vec3 v3;
498 DOCTEST_CHECK(parse_vec3("1.1 2.2 3.3", v3));
499 DOCTEST_CHECK(v3.x == doctest::Approx(1.1f));
500 DOCTEST_CHECK(!parse_vec3("1.1 2.2", v3));
501
502 RGBcolor rgb;
503 DOCTEST_CHECK(parse_RGBcolor("0.1 0.2 0.3", rgb));
504 DOCTEST_CHECK(rgb.r == doctest::Approx(0.1f));
505 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2", rgb));
506 }
507 SUBCASE("parse functions with whitespace") {
508 int i;
509 DOCTEST_CHECK(parse_int(" 123 ", i));
510 DOCTEST_CHECK(i == 123);
511 float f;
512 DOCTEST_CHECK(parse_float(" 1.23 ", f));
513 DOCTEST_CHECK(f == doctest::Approx(1.23f));
514 }
515 SUBCASE("parse_* invalid input") {
516 capture_cerr cerr_buffer;
517 int i;
518 DOCTEST_CHECK(!parse_int("1.5", i));
519 float f;
520 DOCTEST_CHECK(!parse_float("abc", f));
521 double d;
522 DOCTEST_CHECK(!parse_double("abc", d));
523 unsigned int u;
524 DOCTEST_CHECK(!parse_uint("-1", u));
525 int2 i2;
526 DOCTEST_CHECK(!parse_int2("1 abc", i2));
527 int3 i3;
528 DOCTEST_CHECK(!parse_int3("1 2 abc", i3));
529 vec2 v2;
530 DOCTEST_CHECK(!parse_vec2("1.1 abc", v2));
531 vec3 v3;
532 DOCTEST_CHECK(!parse_vec3("1.1 2.2 abc", v3));
533 RGBcolor c;
534 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2 abc", c));
535 DOCTEST_CHECK(!parse_RGBcolor("0.1 0.2 1.1", c)); // out of range
536 }
537}
538
539TEST_CASE("Vector Statistics and Manipulation") {
540 SUBCASE("Vector Statistics") {
541 std::vector<float> v = {1.f, 2.f, 3.f, 4.f, 5.f};
542 DOCTEST_CHECK(sum(v) == doctest::Approx(15.f));
543 DOCTEST_CHECK(mean(v) == doctest::Approx(3.f));
544 DOCTEST_CHECK(min(v) == doctest::Approx(1.f));
545 DOCTEST_CHECK(max(v) == doctest::Approx(5.f));
546 DOCTEST_CHECK(stdev(v) == doctest::Approx(sqrtf(2.f)));
547 DOCTEST_CHECK(median(v) == doctest::Approx(3.f));
548 std::vector<float> v2 = {1.f, 2.f, 3.f, 4.f};
549 DOCTEST_CHECK(median(v2) == doctest::Approx(2.5f));
550 }
551 SUBCASE("sum / mean / min / max / stdev / median") {
552 std::vector<float> vf{1.f, 2.f, 3.f, 4.f, 5.f};
553 CHECK(sum(vf) == doctest::Approx(15.f).epsilon(errtol));
554 CHECK(mean(vf) == doctest::Approx(3.f).epsilon(errtol));
555 CHECK(min(vf) == doctest::Approx(1.f).epsilon(errtol));
556 CHECK(max(vf) == doctest::Approx(5.f).epsilon(errtol));
557 CHECK(median(vf) == doctest::Approx(3.f).epsilon(errtol));
558 CHECK(stdev(vf) == doctest::Approx(std::sqrt(2.f)).epsilon(1e-4));
559
560 std::vector<int> vi{9, 4, -3, 10, 2};
561 CHECK(min(vi) == -3);
562 CHECK(max(vi) == 10);
563
564 std::vector<vec3> vv{make_vec3(2, 3, 4), make_vec3(-1, 7, 0)};
565 CHECK(min(vv) == make_vec3(-1, 3, 0));
566 CHECK(max(vv) == make_vec3(2, 7, 4));
567 }
568 SUBCASE("Vector statistics with single element") {
569 std::vector<float> v = {5.f};
570 DOCTEST_CHECK(stdev(v) == doctest::Approx(0.f));
571 }
572 SUBCASE("Vector Manipulation") {
573 SUBCASE("resize_vector") {
574 std::vector<std::vector<int>> vec2d;
575 resize_vector(vec2d, 2, 3);
576 DOCTEST_CHECK(vec2d.size() == 3);
577 DOCTEST_CHECK(vec2d[0].size() == 2);
578
579 std::vector<std::vector<std::vector<int>>> vec3d;
580 resize_vector(vec3d, 2, 3, 4);
581 DOCTEST_CHECK(vec3d.size() == 4);
582 DOCTEST_CHECK(vec3d[0].size() == 3);
583 DOCTEST_CHECK(vec3d[0][0].size() == 2);
584
585 std::vector<std::vector<std::vector<std::vector<int>>>> vec4d;
586 resize_vector(vec4d, 2, 3, 4, 5);
587 DOCTEST_CHECK(vec4d.size() == 5);
588 DOCTEST_CHECK(vec4d[0].size() == 4);
589 DOCTEST_CHECK(vec4d[0][0].size() == 3);
590 DOCTEST_CHECK(vec4d[0][0][0].size() == 2);
591 }
592
593 SUBCASE("flatten") {
594 std::vector<std::vector<int>> vec2d = {{1, 2}, {3, 4}};
595 std::vector<int> flat = flatten(vec2d);
596 DOCTEST_CHECK(flat.size() == 4);
597 DOCTEST_CHECK(flat[3] == 4);
598
599 std::vector<std::vector<std::vector<int>>> vec3d = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}};
600 flat = flatten(vec3d);
601 DOCTEST_CHECK(flat.size() == 8);
602 DOCTEST_CHECK(flat[7] == 8);
603
604 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}}}};
605 flat = flatten(vec4d);
606 DOCTEST_CHECK(flat.size() == 16);
607 DOCTEST_CHECK(flat[15] == 16);
608 }
609
610 SUBCASE("vector operators") {
611 std::vector<float> v1 = {1, 2, 3};
612 std::vector<float> v2 = {4, 5, 6};
613 std::vector<float> v_sum = v1 + v2;
614 DOCTEST_CHECK(v_sum[0] == 5.f);
615 DOCTEST_CHECK(v_sum[2] == 9.f);
616
617 v1 += v2;
618 DOCTEST_CHECK(v1[0] == 5.f);
619 DOCTEST_CHECK(v1[2] == 9.f);
620
621 std::vector<float> v3 = {1};
622 DOCTEST_CHECK_THROWS(v1 + v3);
623 }
624 }
625}
626
627TEST_CASE("Miscellaneous Utilities") {
628 SUBCASE("1D Interpolation") {
629 std::vector<vec2> points = {{0, 0}, {1, 1}, {2, 0}};
630 float res = interp1(points, 0.5f);
631 DOCTEST_CHECK(res == doctest::Approx(0.5f));
632 res = interp1(points, 1.5f);
633 DOCTEST_CHECK(res == doctest::Approx(0.5f));
634 res = interp1(points, -1.f);
635 DOCTEST_CHECK(res == doctest::Approx(0.f));
636 res = interp1(points, 3.f);
637 DOCTEST_CHECK(res == doctest::Approx(0.f));
638 }
639 SUBCASE("interp1 edge cases") {
640 capture_cerr cerr_buffer;
641 float result;
642 std::vector<vec2> empty_points;
643 DOCTEST_CHECK_THROWS(result = interp1(empty_points, 0.5f));
644
645 std::vector<vec2> single_point = {{1, 5}};
646 result = interp1(single_point, 0.5f);
647 DOCTEST_CHECK(result == doctest::Approx(5.f));
648 result = interp1(single_point, 2.f);
649 DOCTEST_CHECK(result == doctest::Approx(5.f));
650 }
651 SUBCASE("Timer") {
652 Timer t;
653 t.tic();
654 // Not a real test of time, just that it runs.
655 double elapsed = t.toc("mute");
656 DOCTEST_CHECK(elapsed >= 0);
657 }
658 SUBCASE("Custom Error") {
659 capture_cerr cerr_buffer;
660 DOCTEST_CHECK_THROWS_AS(helios_runtime_error("test error"), std::runtime_error);
661 }
662 SUBCASE("Random Number Generation") {
663 float r = randu();
664 DOCTEST_CHECK(r >= 0.f);
665 DOCTEST_CHECK(r <= 1.f);
666
667 int ri = randu(1, 10);
668 DOCTEST_CHECK(ri >= 1);
669 DOCTEST_CHECK(ri <= 10);
670 }
671 SUBCASE("randu(int, int)") {
672 int r = randu(5, 5);
673 DOCTEST_CHECK(r == 5);
674 for (int i = 0; i < 100; ++i) {
675 r = randu(1, 100);
676 DOCTEST_CHECK(r >= 1);
677 DOCTEST_CHECK(r <= 100);
678 }
679 }
680 SUBCASE("acos_safe & asin_safe clamp inputs") {
681 CHECK(acos_safe(1.5f) == doctest::Approx(0.f).epsilon(errtol));
682 CHECK(asin_safe(-2.f) == doctest::Approx(-0.5f * PI_F).epsilon(errtol));
683 }
684
685 SUBCASE("randu range checks") {
686 for (int i = 0; i < 100; ++i) {
687 float r = randu();
688 CHECK(r >= 0.f);
689 CHECK(r <= 1.f);
690
691 int ri = randu(5, 10);
692 CHECK(ri >= 5);
693 CHECK(ri <= 10);
694 CHECK(randu(3, 3) == 3);
695 }
696 }
697}
698
699static float quadratic(float x, std::vector<float> &, const void *) {
700 return x * x - 4.0f;
701}
702static float linear(float x, std::vector<float> &, const void *) {
703 return 3.5f - x;
704}
705static float flat(float, std::vector<float> &, const void *) {
706 return 2.0f;
707}
708static float cubic(float x, std::vector<float> &, const void *) {
709 return (x - 1.0f) * (x + 2.0f) * (x - 4.0f);
710}
711static float near_singular(float x, std::vector<float> &, const void *) {
712 return (x - 1e-3f) * (x - 1e-3f);
713}
714TEST_CASE("fzero") {
715 SUBCASE("fzero finds positive quadratic root") {
716 std::vector<float> v;
717 float root = helios::fzero(quadratic, v, nullptr, 1.0f, 1e-5f, 50);
718 DOCTEST_CHECK(root == doctest::Approx(2.0f).epsilon(errtol));
719 }
720
721 SUBCASE("fzero finds root far from initial guess") {
722 std::vector<float> v;
723 float root = helios::fzero(linear, v, nullptr, -10.0f, 1e-6f, 50);
724 DOCTEST_CHECK(root == doctest::Approx(3.5f).epsilon(errtol));
725 }
726
727 SUBCASE("fzero handles function without zero") {
728 std::vector<float> v;
729 capture_cerr cerr_buffer;
730 float root = helios::fzero(flat, v, nullptr, 0.0f, 1e-6f, 10);
731 DOCTEST_CHECK(std::isfinite(root));
732 DOCTEST_CHECK(cerr_buffer.has_output());
733 }
734
735 SUBCASE("fzero returns exact root at initial guess") {
736 std::vector<float> v;
737 float root = helios::fzero(quadratic, v, nullptr, 2.0f, 1e-6f, 5);
738 DOCTEST_CHECK(root == doctest::Approx(2.0f).epsilon(errtol));
739 }
740
741 SUBCASE("fzero finds a cubic root") {
742 std::vector<float> v;
743 float root = helios::fzero(cubic, v, nullptr, 3.5f, 1e-5f, 80);
744 DOCTEST_CHECK(root == doctest::Approx(4.0f).epsilon(errtol));
745 }
746
747 SUBCASE("fzero copes with near-singular derivative") {
748 std::vector<float> v;
749 float root = helios::fzero(near_singular, v, nullptr, 0.01f, 1e-4f, 50);
750 DOCTEST_CHECK(std::fabs(near_singular(root, v, nullptr)) < 1e-4f);
751 }
752}
753
754TEST_CASE("linspace - Linearly Spaced Values") {
755 SUBCASE("linspace float basic functionality") {
756 std::vector<float> result = linspace(0.f, 10.f, 11);
757 DOCTEST_CHECK(result.size() == 11);
758 DOCTEST_CHECK(result[0] == doctest::Approx(0.f));
759 DOCTEST_CHECK(result[5] == doctest::Approx(5.f));
760 DOCTEST_CHECK(result[10] == doctest::Approx(10.f));
761
762 // Check uniformly spaced
763 for (size_t i = 1; i < result.size(); ++i) {
764 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(1.f));
765 }
766 }
767
768 SUBCASE("linspace float with negative range") {
769 std::vector<float> result = linspace(-5.f, 5.f, 6);
770 DOCTEST_CHECK(result.size() == 6);
771 DOCTEST_CHECK(result[0] == doctest::Approx(-5.f));
772 DOCTEST_CHECK(result[2] == doctest::Approx(-1.f));
773 DOCTEST_CHECK(result[5] == doctest::Approx(5.f));
774
775 // Check spacing
776 for (size_t i = 1; i < result.size(); ++i) {
777 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(2.f));
778 }
779 }
780
781 SUBCASE("linspace float single point") {
782 std::vector<float> result = linspace(5.f, 10.f, 1);
783 DOCTEST_CHECK(result.size() == 1);
784 DOCTEST_CHECK(result[0] == doctest::Approx(5.f));
785 }
786
787 SUBCASE("linspace float two points") {
788 std::vector<float> result = linspace(1.f, 3.f, 2);
789 DOCTEST_CHECK(result.size() == 2);
790 DOCTEST_CHECK(result[0] == doctest::Approx(1.f));
791 DOCTEST_CHECK(result[1] == doctest::Approx(3.f));
792 }
793
794 SUBCASE("linspace float reversed range") {
795 std::vector<float> result = linspace(10.f, 0.f, 6);
796 DOCTEST_CHECK(result.size() == 6);
797 DOCTEST_CHECK(result[0] == doctest::Approx(10.f));
798 DOCTEST_CHECK(result[2] == doctest::Approx(6.f));
799 DOCTEST_CHECK(result[5] == doctest::Approx(0.f));
800
801 // Check negative spacing
802 for (size_t i = 1; i < result.size(); ++i) {
803 DOCTEST_CHECK(result[i] - result[i - 1] == doctest::Approx(-2.f));
804 }
805 }
806
807 SUBCASE("linspace float error handling") {
808 capture_cerr cerr_buffer;
809 std::vector<float> result;
810 DOCTEST_CHECK_THROWS(result = linspace(0.f, 1.f, 0));
811 DOCTEST_CHECK_THROWS(result = linspace(0.f, 1.f, -1));
812 }
813
814 SUBCASE("linspace vec2 basic functionality") {
815 vec2 start(0.f, 1.f);
816 vec2 end(4.f, 5.f);
817 std::vector<vec2> result = linspace(start, end, 5);
818
819 DOCTEST_CHECK(result.size() == 5);
820 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
821 DOCTEST_CHECK(result[0].y == doctest::Approx(1.f));
822 DOCTEST_CHECK(result[2].x == doctest::Approx(2.f));
823 DOCTEST_CHECK(result[2].y == doctest::Approx(3.f));
824 DOCTEST_CHECK(result[4].x == doctest::Approx(4.f));
825 DOCTEST_CHECK(result[4].y == doctest::Approx(5.f));
826 }
827
828 SUBCASE("linspace vec2 single point") {
829 vec2 start(1.f, 2.f);
830 vec2 end(3.f, 4.f);
831 std::vector<vec2> result = linspace(start, end, 1);
832
833 DOCTEST_CHECK(result.size() == 1);
834 DOCTEST_CHECK(result[0].x == doctest::Approx(start.x));
835 DOCTEST_CHECK(result[0].y == doctest::Approx(start.y));
836 }
837
838 SUBCASE("linspace vec2 error handling") {
839 capture_cerr cerr_buffer;
840 std::vector<vec2> result;
841 vec2 start(0.f, 0.f);
842 vec2 end(1.f, 1.f);
843 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
844 DOCTEST_CHECK_THROWS(result = linspace(start, end, -5));
845 }
846
847 SUBCASE("linspace vec3 basic functionality") {
848 vec3 start(0.f, 0.f, 0.f);
849 vec3 end(3.f, 6.f, 9.f);
850 std::vector<vec3> result = linspace(start, end, 4);
851
852 DOCTEST_CHECK(result.size() == 4);
853 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
854 DOCTEST_CHECK(result[0].y == doctest::Approx(0.f));
855 DOCTEST_CHECK(result[0].z == doctest::Approx(0.f));
856 DOCTEST_CHECK(result[1].x == doctest::Approx(1.f));
857 DOCTEST_CHECK(result[1].y == doctest::Approx(2.f));
858 DOCTEST_CHECK(result[1].z == doctest::Approx(3.f));
859 DOCTEST_CHECK(result[3].x == doctest::Approx(3.f));
860 DOCTEST_CHECK(result[3].y == doctest::Approx(6.f));
861 DOCTEST_CHECK(result[3].z == doctest::Approx(9.f));
862 }
863
864 SUBCASE("linspace vec3 with mixed positive/negative components") {
865 vec3 start(-1.f, 2.f, -3.f);
866 vec3 end(1.f, -2.f, 3.f);
867 std::vector<vec3> result = linspace(start, end, 3);
868
869 DOCTEST_CHECK(result.size() == 3);
870 DOCTEST_CHECK(result[0].x == doctest::Approx(-1.f));
871 DOCTEST_CHECK(result[0].y == doctest::Approx(2.f));
872 DOCTEST_CHECK(result[0].z == doctest::Approx(-3.f));
873 DOCTEST_CHECK(result[1].x == doctest::Approx(0.f));
874 DOCTEST_CHECK(result[1].y == doctest::Approx(0.f));
875 DOCTEST_CHECK(result[1].z == doctest::Approx(0.f));
876 DOCTEST_CHECK(result[2].x == doctest::Approx(1.f));
877 DOCTEST_CHECK(result[2].y == doctest::Approx(-2.f));
878 DOCTEST_CHECK(result[2].z == doctest::Approx(3.f));
879 }
880
881 SUBCASE("linspace vec3 error handling") {
882 capture_cerr cerr_buffer;
883 std::vector<vec3> result;
884 vec3 start(0.f, 0.f, 0.f);
885 vec3 end(1.f, 1.f, 1.f);
886 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
887 DOCTEST_CHECK_THROWS(result = linspace(start, end, -10));
888 }
889
890 SUBCASE("linspace vec4 basic functionality") {
891 vec4 start(0.f, 1.f, 2.f, 3.f);
892 vec4 end(4.f, 9.f, 14.f, 19.f);
893 std::vector<vec4> result = linspace(start, end, 5);
894
895 DOCTEST_CHECK(result.size() == 5);
896 DOCTEST_CHECK(result[0].x == doctest::Approx(0.f));
897 DOCTEST_CHECK(result[0].y == doctest::Approx(1.f));
898 DOCTEST_CHECK(result[0].z == doctest::Approx(2.f));
899 DOCTEST_CHECK(result[0].w == doctest::Approx(3.f));
900 DOCTEST_CHECK(result[2].x == doctest::Approx(2.f));
901 DOCTEST_CHECK(result[2].y == doctest::Approx(5.f));
902 DOCTEST_CHECK(result[2].z == doctest::Approx(8.f));
903 DOCTEST_CHECK(result[2].w == doctest::Approx(11.f));
904 DOCTEST_CHECK(result[4].x == doctest::Approx(4.f));
905 DOCTEST_CHECK(result[4].y == doctest::Approx(9.f));
906 DOCTEST_CHECK(result[4].z == doctest::Approx(14.f));
907 DOCTEST_CHECK(result[4].w == doctest::Approx(19.f));
908 }
909
910 SUBCASE("linspace vec4 two points") {
911 vec4 start(1.f, 2.f, 3.f, 4.f);
912 vec4 end(5.f, 6.f, 7.f, 8.f);
913 std::vector<vec4> result = linspace(start, end, 2);
914
915 DOCTEST_CHECK(result.size() == 2);
916 DOCTEST_CHECK(result[0] == start);
917 DOCTEST_CHECK(result[1] == end);
918 }
919
920 SUBCASE("linspace vec4 error handling") {
921 capture_cerr cerr_buffer;
922 std::vector<vec4> result;
923 vec4 start(0.f, 0.f, 0.f, 0.f);
924 vec4 end(1.f, 1.f, 1.f, 1.f);
925 DOCTEST_CHECK_THROWS(result = linspace(start, end, 0));
926 DOCTEST_CHECK_THROWS(result = linspace(start, end, -1));
927 }
928
929 SUBCASE("linspace precision and endpoint accuracy") {
930 // Test that endpoints are exactly preserved despite floating point arithmetic
931 std::vector<float> result = linspace(0.1f, 0.9f, 9);
932 DOCTEST_CHECK(result[0] == doctest::Approx(0.1f));
933 DOCTEST_CHECK(result[8] == doctest::Approx(0.9f));
934
935 // Test with large values
936 result = linspace(1000000.f, 2000000.f, 11);
937 DOCTEST_CHECK(result[0] == doctest::Approx(1000000.f));
938 DOCTEST_CHECK(result[10] == doctest::Approx(2000000.f));
939
940 // Test with very small values
941 result = linspace(1e-6f, 2e-6f, 3);
942 DOCTEST_CHECK(result[0] == doctest::Approx(1e-6f));
943 DOCTEST_CHECK(result[2] == doctest::Approx(2e-6f));
944 }
945
946 SUBCASE("linspace zero-length intervals") {
947 // Test when start == end
948 std::vector<float> result = linspace(5.f, 5.f, 5);
949 DOCTEST_CHECK(result.size() == 5);
950 for (const auto &val: result) {
951 DOCTEST_CHECK(val == doctest::Approx(5.f));
952 }
953
954 // Test vec3 with zero-length interval
955 vec3 point(1.f, 2.f, 3.f);
956 std::vector<vec3> vec_result = linspace(point, point, 3);
957 DOCTEST_CHECK(vec_result.size() == 3);
958 for (const auto &v: vec_result) {
959 DOCTEST_CHECK(v.x == doctest::Approx(point.x));
960 DOCTEST_CHECK(v.y == doctest::Approx(point.y));
961 DOCTEST_CHECK(v.z == doctest::Approx(point.z));
962 }
963 }
964}
965
966TEST_CASE("Asset Resolution Functions") {
967 SUBCASE("resolveAssetPath basic functionality") {
968 // Test that resolveAssetPath works correctly - should throw for non-existent file
969 bool exception_thrown = false;
970 try {
971 [[maybe_unused]] auto path = resolveAssetPath("nonexistent_test_file.txt");
972 } catch (const std::runtime_error &) {
973 exception_thrown = true;
974 }
975 DOCTEST_CHECK(exception_thrown);
976 }
977
978 SUBCASE("resolvePluginAsset") {
979 // Test plugin-specific asset resolution - should throw for non-existent file
980 bool exception_thrown = false;
981 try {
982 [[maybe_unused]] auto path = resolvePluginAsset("visualizer", "nonexistent_font.ttf");
983 } catch (const std::runtime_error &) {
984 exception_thrown = true;
985 }
986 DOCTEST_CHECK(exception_thrown);
987 }
988
989
990 SUBCASE("resolveSpectraPath") {
991 // Test spectra path resolution - should throw for non-existent file
992 bool exception_thrown = false;
993 try {
994 [[maybe_unused]] auto path = resolveSpectraPath("nonexistent_spectra.xml");
995 } catch (const std::runtime_error &) {
996 exception_thrown = true;
997 }
998 DOCTEST_CHECK(exception_thrown);
999 }
1000
1001 SUBCASE("validateAssetPath with valid path") {
1002 // Create a temporary file for testing
1003 std::filesystem::path temp_path = std::filesystem::temp_directory_path() / "helios_test_asset.txt";
1004 std::ofstream temp_file(temp_path);
1005 temp_file << "test content";
1006 temp_file.close();
1007
1008 // Test validation of existing file
1009 DOCTEST_CHECK(validateAssetPath(temp_path) == true);
1010
1011 // Clean up
1012 std::filesystem::remove(temp_path);
1013 }
1014
1015 SUBCASE("validateAssetPath with invalid path") {
1016 // Test validation of non-existent file
1017 std::filesystem::path non_existent = "/this/path/does/not/exist.txt";
1018 DOCTEST_CHECK(validateAssetPath(non_existent) == false);
1019 }
1020
1021 SUBCASE("resolveAssetPath with empty string") {
1022 // Test with empty input - should return a path (empty string is handled gracefully)
1023 auto path = resolveAssetPath("");
1024 DOCTEST_CHECK(!path.empty());
1025 DOCTEST_CHECK(path.is_absolute());
1026 }
1027
1028 SUBCASE("error message content") {
1029 // Test that error messages contain helpful information
1030 try {
1031 [[maybe_unused]] auto path = resolveAssetPath("nonexistent_file.txt");
1032 DOCTEST_FAIL("Expected exception was not thrown");
1033 } catch (const std::runtime_error &e) {
1034 std::string error_msg = e.what();
1035 DOCTEST_CHECK(error_msg.find("Could not locate asset file") != std::string::npos);
1036 DOCTEST_CHECK(error_msg.find("nonexistent_file.txt") != std::string::npos);
1037 }
1038 }
1039
1040 SUBCASE("asset resolution consistency") {
1041 // Test that multiple calls with same input produce consistent error messages
1042 std::string error1, error2;
1043 try {
1044 [[maybe_unused]] auto path1 = resolveAssetPath("test_consistency.txt");
1045 } catch (const std::runtime_error &e) {
1046 error1 = e.what();
1047 }
1048 try {
1049 [[maybe_unused]] auto path2 = resolveAssetPath("test_consistency.txt");
1050 } catch (const std::runtime_error &e) {
1051 error2 = e.what();
1052 }
1053 DOCTEST_CHECK(error1 == error2);
1054 }
1055
1056 SUBCASE("different plugin error messages") {
1057 // Test that different plugins produce different error messages
1058 std::string vis_error, rad_error;
1059 try {
1060 [[maybe_unused]] auto vis_path = resolvePluginAsset("visualizer", "test.txt");
1061 } catch (const std::runtime_error &e) {
1062 vis_error = e.what();
1063 }
1064 try {
1065 [[maybe_unused]] auto rad_path = resolvePluginAsset("radiation", "test.txt");
1066 } catch (const std::runtime_error &e) {
1067 rad_error = e.what();
1068 }
1069 // Error messages should be different for different plugins
1070 DOCTEST_CHECK(vis_error != rad_error);
1071 DOCTEST_CHECK(vis_error.find("visualizer") != std::string::npos);
1072 DOCTEST_CHECK(rad_error.find("radiation") != std::string::npos);
1073 }
1074}
1075
1076TEST_CASE("Project-based File Resolution") {
1077 SUBCASE("findProjectRoot basic functionality") {
1078 // Test from current working directory (which should contain CMakeLists.txt)
1079 std::filesystem::path cwd = std::filesystem::current_path();
1080 auto project_root = findProjectRoot(cwd);
1081
1082 // Should find a project root (not empty)
1083 DOCTEST_CHECK(!project_root.empty());
1084
1085 // Project root should contain CMakeLists.txt
1086 auto cmake_file = project_root / "CMakeLists.txt";
1087 DOCTEST_CHECK(std::filesystem::exists(cmake_file));
1088 }
1089
1090 SUBCASE("findProjectRoot with non-existent path") {
1091 // Test with a path that doesn't exist
1092 std::filesystem::path fake_path = "/this/path/does/not/exist";
1093 auto project_root = findProjectRoot(fake_path);
1094
1095 // Should return empty path when no project found
1096 DOCTEST_CHECK(project_root.empty());
1097 }
1098
1099 SUBCASE("findProjectRoot from root directory") {
1100 // Test from system root directory (should not find CMakeLists.txt)
1101 std::filesystem::path root_path = "/";
1102 auto project_root = findProjectRoot(root_path);
1103
1104 // Should return empty path when searching from root
1105 DOCTEST_CHECK(project_root.empty());
1106 }
1107
1108 SUBCASE("resolveProjectFile with existing file in cwd") {
1109 // Create a temporary test file in current directory
1110 std::string test_filename = "test_project_resolve.tmp";
1111 std::ofstream test_file(test_filename);
1112 test_file << "test content";
1113 test_file.close();
1114
1115 try {
1116 // Should find file in current working directory
1117 auto resolved_path = resolveProjectFile(test_filename);
1118 DOCTEST_CHECK(!resolved_path.empty());
1119 DOCTEST_CHECK(std::filesystem::exists(resolved_path));
1120 } catch (...) {
1121 // Clean up even if test fails
1122 std::filesystem::remove(test_filename);
1123 throw;
1124 }
1125
1126 // Clean up
1127 std::filesystem::remove(test_filename);
1128 }
1129
1130 SUBCASE("resolveProjectFile with non-existent file") {
1131 // Test with file that doesn't exist in current directory or project
1132 std::string fake_filename = "this_file_does_not_exist_anywhere.tmp";
1133
1134 std::string error_message;
1135 try {
1136 [[maybe_unused]] auto resolved_path = resolveProjectFile(fake_filename);
1137 } catch (const std::runtime_error &e) {
1138 error_message = e.what();
1139 }
1140
1141 // Should throw runtime error for non-existent file
1142 DOCTEST_CHECK(!error_message.empty());
1143 DOCTEST_CHECK(error_message.find("Could not locate file") != std::string::npos);
1144 DOCTEST_CHECK(error_message.find(fake_filename) != std::string::npos);
1145 }
1146
1147 SUBCASE("resolveProjectFile with empty filename") {
1148 // Test with empty filename
1149 std::string error_message;
1150 try {
1151 [[maybe_unused]] auto resolved_path = resolveProjectFile("");
1152 } catch (const std::runtime_error &e) {
1153 error_message = e.what();
1154 }
1155
1156 // Should handle empty filename gracefully
1157 DOCTEST_CHECK(!error_message.empty());
1158 }
1159
1160 SUBCASE("resolveProjectFile project directory fallback") {
1161 // This test verifies the fallback to project directory works
1162 // We'll create a test file in the project root and try to access it from a subdirectory
1163 auto project_root = findProjectRoot(std::filesystem::current_path());
1164 if (!project_root.empty()) {
1165 std::string test_filename = "test_project_fallback.tmp";
1166 auto test_file_path = project_root / test_filename;
1167
1168 // Create test file in project root
1169 std::ofstream test_file(test_file_path);
1170 test_file << "fallback test content";
1171 test_file.close();
1172
1173 try {
1174 // Should find file in project directory even if not in cwd
1175 auto resolved_path = resolveProjectFile(test_filename);
1176 DOCTEST_CHECK(!resolved_path.empty());
1177 DOCTEST_CHECK(std::filesystem::exists(resolved_path));
1178 DOCTEST_CHECK(resolved_path == test_file_path);
1179 } catch (...) {
1180 // Clean up even if test fails
1181 std::filesystem::remove(test_file_path);
1182 throw;
1183 }
1184
1185 // Clean up
1186 std::filesystem::remove(test_file_path);
1187 }
1188 }
1189}