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