1 // madronalib: a C++ framework for DSP applications.
2 // Copyright (c) 2020 Madrona Labs LLC. http://www.madronalabs.com
3 // Distributed under the MIT license: http://madrona-labs.mit-license.org/
4
5 // a unit test made using the Catch framework in catch.hpp / tests.cpp.
6
7 #include <chrono>
8 #include <cstring>
9 #include <iostream>
10 #include <map>
11 #include <numeric>
12 #include <thread>
13 #include <unordered_map>
14 #include <vector>
15
16 #include "MLTextUtils.h"
17 #include "catch.hpp"
18 #include "madronalib.h"
19
20 #if _WIN32
21 #define HAVE_U8_LITERALS 0
22 #else
23 #define HAVE_U8_LITERALS 1
24 #endif
25
26 static const int kThreadTestSize = 1024;
27
28 using namespace ml;
29
30 typedef std::chrono::time_point<std::chrono::high_resolution_clock> myTimePoint;
now()31 myTimePoint now() { return std::chrono::high_resolution_clock::now(); }
32
threadTest(int threadID)33 void threadTest(int threadID)
34 {
35 textUtils::NameMaker namer;
36 for (int i = 0; i < kThreadTestSize; ++i)
37 {
38 Symbol sym(namer.nextName());
39 std::this_thread::yield();
40 }
41 }
42
43 TEST_CASE("madronalib/core/symbol/simple", "[symbol][simple]")
44 {
45 Symbol a("hello");
46 Symbol b("world");
47 Symbol c("hello");
48
49 REQUIRE(a.getID() == c.getID());
50
51 if (a.getID() != c.getID())
52 {
53 std::cout << "WTF\n";
54 }
55
56 else
57 {
58 std::cout << a << ", " << b << "!\n";
59 }
60
61 theSymbolTable().dump();
62 }
63
64 TEST_CASE("madronalib/core/symbol/threads", "[symbol][threads]")
65 {
66 // multithreaded test. multiple nameMakers will try to make duplicate names at
67 // about the same time, which will almost certainly lead to problems unless
68 // the symbol code is properly thread-safe.
69
70 // start timing
71 myTimePoint start, end;
72 std::chrono::duration<double> elapsed;
73 start = now();
74
75 theSymbolTable().clear();
76 int nThreads = 16;
77 std::vector<std::thread> threads;
78 for (int i = 0; i < nThreads; ++i)
79 {
80 threads.push_back(std::thread(threadTest, i));
81 }
82 for (int i = 0; i < nThreads; ++i)
83 {
84 threads[i].join();
85 }
86
87 end = now();
88 elapsed = end - start;
89 std::cout << "multithreaded test, elapsed time: " << elapsed.count() << "s\n";
90
91 REQUIRE(theSymbolTable().audit());
92 REQUIRE(theSymbolTable().getSize() == kThreadTestSize + 1);
93 }
94
95 TEST_CASE("madronalib/core/collision", "[collision]")
96 {
97 // nothing is checked here - these are two pairs of colliding symbols for
98 // reference with 12-bit hash:
99 Symbol a("KP");
100 Symbol aa("BAZ");
101 Symbol b("KL");
102 Symbol bb("mse");
103 // 16-bit hash:
104 Symbol c("FB");
105 Symbol cc("hombfbmohqqhombf");
106 }
107
108 template <size_t N>
hashTest1(const char (& sym)[N])109 constexpr int hashTest1(const char (&sym)[N])
110 {
111 return krHash1<N>(sym);
112 }
113
114 TEST_CASE("madronalib/core/hashes", "[hashes]")
115 {
116 // the compile time and runtime hashes need to be equivalent.
117 const char* str1("hello");
118 const char* str2(u8"محمد بن سعيد");
119
120 constexpr int a1 = hashTest1("hello");
121 constexpr int a2 = hashTest1(u8"محمد بن سعيد");
122
123 int b1 = krHash0(str1, strlen(str1));
124 int b2 = krHash0(str2, strlen(str2));
125
126 REQUIRE(a1 == b1);
127 REQUIRE(a2 == b2);
128 }
129
130 const char letters[24] = "abcdefghjklmnopqrstuvw";
131
132 TEST_CASE("madronalib/core/symbol/maps", "[symbol]")
133 {
134 const int kMapSize = 100;
135 const int kTestLength = 100000;
136
137 // main maps for testing
138 std::map<Symbol, float> testMapOrderedSym;
139 std::map<std::string, float> testMapOrderedStr;
140 std::unordered_map<Symbol, float> testMapUnorderedSym;
141 std::unordered_map<std::string, float> testMapUnorderedStr;
142
143 // make dictionaries of symbols, strings and chars for testing
144 std::vector<Symbol> symbolDict;
145 std::vector<std::string> stringDict;
146 std::vector<const char*> charDict;
147 int p = 0;
148 for (int i = 0; i < kMapSize; ++i)
149 {
150 // make procedural gibberish
151 std::string newString;
152 int length = 3 + (i % 12);
153 for (int j = 0; j < length; ++j)
154 {
155 p += (i * j + 1);
156 p += i % 37;
157 p += j % 23;
158 p = abs(p);
159 newString += letters[p % 22];
160 }
161
162 stringDict.push_back(newString);
163
164 // add it to symbol table
165 Symbol newSym(newString.c_str());
166 symbolDict.push_back(newSym);
167
168 // add an entry to each map
169 float val = i;
170 testMapOrderedSym[newSym] = val;
171 testMapOrderedStr[newString] = val;
172 testMapUnorderedSym[newSym] = val;
173 testMapUnorderedStr[newString] = val;
174 }
175
176 // make char dict after string dict is complete, otherwise ptrs may change!
177 for (int i = 0; i < stringDict.size(); ++i)
178 {
179 charDict.push_back(stringDict[i].c_str());
180 }
181
182 SECTION("test maps")
183 {
184 myTimePoint start, end;
185 std::chrono::duration<double> elapsed;
186 double symbolSum, stringSum;
187 int idx;
188 RandomScalarSource randSource;
189
190 std::vector<int> testIndexes;
191 for (int n = 0; n < kTestLength; ++n)
192 {
193 int i = fabs(randSource.getFloat()) * kMapSize;
194 testIndexes.push_back(i);
195 }
196
197 // ----------------------------------------------------------------
198 // existing symbols / strings
199
200 // lookup from existing std::strings
201 start = now();
202 stringSum = 0.f;
203 for (int i = 0; i < kTestLength; ++i)
204 {
205 stringSum += testMapOrderedStr[stringDict[testIndexes[i]]];
206 }
207 end = now();
208 elapsed = end - start;
209 std::cout << "existing strings, elapsed time: " << elapsed.count() << "s\n";
210
211 // lookup from existing MLSymbols
212 start = now();
213 symbolSum = 0.f;
214 for (int i = 0; i < kTestLength; ++i)
215 {
216 symbolSum += testMapOrderedSym[symbolDict[testIndexes[i]]];
217 }
218 end = now();
219 elapsed = end - start;
220 std::cout << "existing symbols, elapsed time: " << elapsed.count() << "s\n";
221
222 REQUIRE(stringSum == symbolSum);
223
224 // lookup from existing std::strings
225 start = now();
226 stringSum = 0.f;
227 for (int i = 0; i < kTestLength; ++i)
228 {
229 stringSum += testMapUnorderedStr[stringDict[testIndexes[i]]];
230 }
231 end = now();
232 elapsed = end - start;
233 std::cout << "existing strings, unordered, elapsed time: "
234 << elapsed.count() << "s\n";
235
236 // lookup from existing MLSymbols
237 start = now();
238 symbolSum = 0.f;
239 for (int i = 0; i < kTestLength; ++i)
240 {
241 symbolSum += testMapUnorderedSym[symbolDict[testIndexes[i]]];
242 }
243 end = now();
244 elapsed = end - start;
245 std::cout << "existing symbols, unordered, elapsed time: "
246 << elapsed.count() << "s\n";
247
248 REQUIRE(stringSum == symbolSum);
249
250 // ----------------------------------------------------------------
251 // constructing symbols / strings
252
253 // lookup from newly made std::strings
254 start = now();
255 stringSum = 0.f;
256 for (int i = 0; i < kTestLength; ++i)
257 {
258 stringSum += testMapOrderedStr[charDict[testIndexes[i]]];
259 }
260 end = now();
261 elapsed = end - start;
262 std::cout << "constructing strings, elapsed time: " << elapsed.count()
263 << "s\n";
264
265 // lookup from new MLSymbols made from char *
266 start = now();
267 symbolSum = 0.f;
268 for (int i = 0; i < kTestLength; ++i)
269 {
270 symbolSum += testMapOrderedSym[charDict[testIndexes[i]]];
271 }
272 end = now();
273 elapsed = end - start;
274 std::cout << "constructing symbols, elapsed time: " << elapsed.count()
275 << "s\n";
276
277 REQUIRE(stringSum == symbolSum);
278
279 // lookup from newly made std::strings
280 start = now();
281 stringSum = 0.f;
282 for (int i = 0; i < kTestLength; ++i)
283 {
284 stringSum += testMapUnorderedStr[charDict[testIndexes[i]]];
285 }
286 end = now();
287 elapsed = end - start;
288 std::cout << "constructing strings, unordered, elapsed time: "
289 << elapsed.count() << "s\n";
290
291 // unordered lookup from new MLSymbols made from char *
292 start = now();
293 symbolSum = 0.f;
294 for (int i = 0; i < kTestLength; ++i)
295 {
296 symbolSum += testMapUnorderedSym[charDict[testIndexes[i]]];
297 }
298 end = now();
299 elapsed = end - start;
300 std::cout << "constructing symbols, unordered, elapsed time: "
301 << elapsed.count() << "s\n";
302
303 REQUIRE(stringSum == symbolSum);
304
305 // lookup using literal (hashed at compile time)
306 // examples generated by above: fkjcouvrhqtrk uhe cbtktb wfdq
307 start = now();
308 symbolSum = 0.f;
309 for (int i = 0; i < kTestLength; ++i)
310 {
311 symbolSum += testMapOrderedSym["fkjcouvrhqtrk"];
312 }
313 end = now();
314 elapsed = end - start;
315 std::cout << "pre-hashed symbols, elapsed time: " << elapsed.count()
316 << "s\n";
317
318 // unordered lookup using literal (hashed at compile time)
319 start = now();
320 symbolSum = 0.f;
321 for (int i = 0; i < kTestLength; ++i)
322 {
323 symbolSum += testMapUnorderedSym["fkjcouvrhqtrk"];
324 }
325 end = now();
326 elapsed = end - start;
327 std::cout << "pre-hashed symbols, unordered, elapsed time: "
328 << elapsed.count() << "s\n";
329
330 REQUIRE(theSymbolTable().audit());
331
332 // theSymbolTable().dump();
333 }
334 }
335
336 TEST_CASE("madronalib/core/symbol/numbers", "[symbol]")
337 {
338 textUtils::NameMaker namer;
339 for (int i = 0; i < 10; ++i)
340 {
341 Symbol testSym = namer.nextName();
342 Symbol testSymWithNum = textUtils::addFinalNumber(testSym, i);
343 Symbol testSymWithoutNum = textUtils::stripFinalNumber(testSym);
344 int j = textUtils::getFinalNumber(testSymWithNum);
345
346 REQUIRE(testSym == testSymWithoutNum);
347 REQUIRE(i == j);
348 }
349 REQUIRE(theSymbolTable().audit());
350 }
351
352 TEST_CASE("madronalib/core/symbol/identity", "[symbol][identity]")
353 {
354 // things that should and shouldn't be the same as one another.
355 theSymbolTable().clear();
356 Symbol a("xxx_yyy");
357 Symbol b("xxx");
358 REQUIRE(a != b);
359 }
360
361 // hex char printing
362 struct HexCharStruct
363 {
364 unsigned char c;
HexCharStructHexCharStruct365 HexCharStruct(unsigned char _c) : c(_c) {}
366 };
367
operator <<(std::ostream & o,const HexCharStruct & hs)368 inline std::ostream& operator<<(std::ostream& o, const HexCharStruct& hs)
369 {
370 return (o << std::hex << (int)hs.c << std::dec);
371 }
372
hexchar(unsigned char _c)373 inline HexCharStruct hexchar(unsigned char _c) { return HexCharStruct(_c); }
374
375 TEST_CASE("madronalib/core/symbol/UTF8", "[symbol][UTF8]")
376 {
377 theSymbolTable().clear();
378 std::map<Symbol, int> sortedMap;
379 const int sortTestSize = 10;
380 int p = 0;
381
382 #if HAVE_U8_LITERALS
383 std::vector<std::string> strings = {u8"Федор", u8"小林 尊", u8"محمد بن سعيد"};
384 #else
385 const char* fedor("\xD0\xA4\xD0\xB5\xD0\xB4\xD0\xBE\xD1\x80");
386 const char* kobayashi("\xE5\xB0\x8F\xE6\x9E\x97\x20\xE5\xB0\x8A");
387 const char* muhammad(
388 "\xD9\x85\xD8\xAD\xD9\x85\xD8\xAF\x20\xD8\xA8\xD9\x86\x20\xD8\xB3\xD8\xB9"
389 "\xD9\x8A\xD8\xAF");
390 std::vector<std::string> strings = {fedor, kobayashi, muhammad};
391 #endif
392
393 int totalPoints = 0;
394 for (auto testString : strings)
395 {
396 totalPoints += TextFragment(testString.c_str()).lengthInCodePoints();
397 }
398
399 REQUIRE(totalPoints == 21);
400 }
401
402 // TODO move
403 TEST_CASE("madronalib/core/symbol/path", "[symbol][path]")
404 {
405 Path p("hello/world/a/b/c/d/e/f/g");
406
__anonf25b9f8f0102(Symbol a, Symbol b) 407 auto accumTest = [](Symbol a, Symbol b) {
408 return TextFragment(a.getTextFragment(), TextFragment("+"),
409 b.getTextFragment());
410 };
411 TextFragment pa = std::accumulate(++p.begin(), p.end(),
412 (*p.begin()).getTextFragment(), accumTest);
413
414 std::cout << "accumulated: " << pa << "\n";
415
416 Path a{"a"};
417 Path b{"b"};
418 Path d{"d"};
419 Path p4 (a, b, "c", d);
420 Path p5 (p4, "george", p4);
421 REQUIRE(p5.getSize() == 9);
422 }
423