1 /** Helper to read and write CSV files */
2 module test_csv;
3 
4 import std.datetime;
5 import std.digest;
6 import std.digest.md;
7 version (UseXXHSourceFromDub) import xxhash3;
8 version (UseXXHSourceFromLocalDir) import local_dev.xxh;
9 version (UseXXHSourceFromPhobos) import std.digest.xxh;
10 import std.digest.murmurhash;
11 import std.file;
12 import std.format;
13 import std.range : join;
14 import std.stdio;
15 import std.string;
16 import std.traits : FieldNameTuple;
17 
18 import test_helpers;
19 import test_pattern_generator;
20 
21 /** Container struct for compression benchmarking in CSV file */
22 struct CompressionTestData {
23 public:
24     TestPattern pattern;
25     size_t unpacked_size;
26     size_t packed_size;
27     Duration pack_duration;
28     Duration unpack_duration;
29 private:
30     float pack_ratio;
31     float pack_mbs;
32     float unpack_mbs;
33 
34     void updateHiddenFields() {
35         pack_ratio = getCompressionRatio(unpacked_size, packed_size);
36         pack_mbs = getMegaBytePerSeconds(unpacked_size, pack_duration);
37         unpack_mbs = getMegaBytePerSeconds(unpacked_size, unpack_duration);
38     }
39 
40 public:
41     /** Get the names of the CSV fields from the structure */
42     static string getCsvHeader() {
43         return [FieldNameTuple!CompressionTestData].join(';');
44     }
45 
46     /** Output the structure data as a CSV Line */
47     string getCsvLine() {
48         updateHiddenFields();
49         auto rc = format("%s;%d;%d;%f;%f;%f;%f;%f",
50             pattern, unpacked_size, packed_size,
51             getFloatSecond(pack_duration), getFloatSecond(unpack_duration),
52             pack_ratio, pack_mbs, unpack_mbs
53         );
54         return rc;
55     }
56 }
57 
58 /** Check empty values and zero devision handling */
59 unittest {
60     CompressionTestData tv;
61     const auto hdr = tv.getCsvHeader;
62     assert(
63         hdr == "pattern;unpacked_size;packed_size;pack_duration;unpack_duration;pack_ratio;pack_mbs;unpack_mbs");
64     const auto line = tv.getCsvLine;
65     assert(line == "(Unknown:00000:00000:00);0;0;0.000000;0.000000;nan;nan;nan");
66 }
67 
68 /** Check example values*/
69 unittest {
70     auto tp = TestPattern(PatterMode.RepeatN, 0x00100, 0x00200, 8);
71 
72     CompressionTestData tv = CompressionTestData(tp, 2 ^^ 20, 2 ^^ 10, dur!"seconds"(1), dur!"seconds"(
73             1));
74     auto line = tv.getCsvLine;
75     assert(
76         line == "(RepeatN:00100:00200:08);1048576;1024;1.000000;1.000000;0.000977;1.000000;1.000000");
77 
78     tv = CompressionTestData(tp, 10 ^^ 6, 10 ^^ 3, dur!"seconds"(1), dur!"seconds"(1));
79     line = tv.getCsvLine;
80     assert(
81         line == "(RepeatN:00100:00200:08);1000000;1000;1.000000;1.000000;0.001000;0.953674;0.953674");
82 }
83 
84 /** Container struct for hashing benchmarking in CSV file */
85 struct HashTestData(HASHTYPE) {
86 public:
87     size_t size;
88     size_t fill;
89     HASHTYPE hash;
90     Duration duration1;
91     Duration duration2;
92 private:
93     float mbs1;
94     float mbs2;
95 
96     void updateHiddenFields() {
97         mbs1 = getMegaBytePerSeconds(size, duration1);
98         mbs2 = getMegaBytePerSeconds(size, duration2);
99     }
100 
101 public:
102     /** Get the names of the CSV fields from the structure */
103     static string getCsvHeader() {
104         return [FieldNameTuple!MD5TestData].join(';');
105     }
106 
107     /** Output the structure data as a CSV Line */
108     string getCsvLine() {
109         import std.digest : toHexString;
110 
111         updateHiddenFields();
112         static if (is(HASHTYPE == MurmurHash3!32)) {
113             auto rc = format("0x%04x;0x%02x;%s;%f;%f;%f;%f", size, fill, hash.getBytes.toHexString,
114                 getFloatSecond(duration1), getFloatSecond(duration2),
115                 mbs2, mbs2);
116         } else static if (is(HASHTYPE == MurmurHash3!128)) {
117             auto rc = format("0x%04x;0x%02x;%s;%f;%f;%f;%f", size, fill, hash.getBytes.toHexString,
118                 getFloatSecond(duration1), getFloatSecond(duration2),
119                 mbs2, mbs2);
120         } else {
121             auto rc = format("0x%04x;0x%02x;%s;%f;%f;%f;%f", size, fill, hash.toHexString,
122                 getFloatSecond(duration1), getFloatSecond(duration2),
123                 mbs2, mbs2);
124         }
125         return rc;
126     }
127 }
128 
129 alias CRC32TestData = HashTestData!(ubyte[4]);
130 alias CRC64TestData = HashTestData!(ubyte[8]);
131 alias MD5TestData = HashTestData!(ubyte[16]);
132 alias XXH32TestData = HashTestData!XXH32_canonical_t;
133 alias XXH64TestData = HashTestData!XXH64_canonical_t;
134 alias XXH3_64TestData = HashTestData!XXH64_canonical_t;
135 alias XXH3_128TestData = HashTestData!XXH128_canonical_t;
136 alias Murmur32TestData = HashTestData!(ubyte[4]);
137 alias Murmur128TestData = HashTestData!(ubyte[16]);
138 
139 /** Check empty values and zero devision handling */
140 unittest {
141     MD5TestData tv;
142     const auto hdr = tv.getCsvHeader;
143     assert(hdr == "size;fill;hash;duration1;duration2;mbs1;mbs2");
144     const auto line = tv.getCsvLine;
145     assert(line == "0x0000;0x00;00000000000000000000000000000000;0.000000;0.000000;nan;nan");
146 }
147 
148 /** Check example values*/
149 unittest {
150     MD5TestData tv =
151         MD5TestData(
152             0x1000, 0xAA, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
153             dur!"seconds"(1), dur!"seconds"(1));
154     auto line = tv.getCsvLine;
155     assert(
156         line == "0x1000;0xaa;000102030405060708090A0B0C0D0E0F;1.000000;1.000000;0.003906;0.003906");
157 
158     tv = MD5TestData(
159         0x2000, 0x55, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
160         dur!"seconds"(1), dur!"seconds"(1));
161     line = tv.getCsvLine;
162     assert(
163         line == "0x2000;0x55;000102030405060708090A0B0C0D0E0F;1.000000;1.000000;0.007812;0.007812");
164 }
165 
166 /** Check empty values and zero devision handling */
167 unittest {
168     XXH32TestData tv;
169     const auto hdr = tv.getCsvHeader;
170     assert(hdr == "size;fill;hash;duration1;duration2;mbs1;mbs2");
171     const auto line = tv.getCsvLine;
172     assert(line == "0x0000;0x00;00000000;0.000000;0.000000;nan;nan");
173 }
174 
175 /** Check example values*/
176 unittest {
177     XXH32TestData tv =
178         XXH32TestData(
179             0x1000, 0xAA, [ 12,34,56,78 ],
180             dur!"seconds"(1), dur!"seconds"(1));
181     auto line = tv.getCsvLine;
182     assert(line == "0x1000;0xaa;0C22384E;1.000000;1.000000;0.003906;0.003906");
183 
184     tv = XXH32TestData(
185         0x2000, 0x55, [ 12,34,56,78 ],
186         dur!"seconds"(1), dur!"seconds"(1));
187     line = tv.getCsvLine;
188     assert(line == "0x2000;0x55;0C22384E;1.000000;1.000000;0.007812;0.007812");
189 }
190 
191 /** Check empty values and zero devision handling */
192 unittest {
193     XXH64TestData tv;
194     const auto hdr = tv.getCsvHeader;
195     assert(hdr == "size;fill;hash;duration1;duration2;mbs1;mbs2");
196     const auto line = tv.getCsvLine;
197     assert(line == "0x0000;0x00;0000000000000000;0.000000;0.000000;nan;nan");
198 }
199 
200 /** Check example values*/
201 unittest {
202     XXH64TestData tv =
203         XXH64TestData(
204             0x1000, 0xAA, [ 1,2,3,4,5,6,7,8 ],
205             dur!"seconds"(1), dur!"seconds"(1));
206     auto line = tv.getCsvLine;
207     assert(line == "0x1000;0xaa;0102030405060708;1.000000;1.000000;0.003906;0.003906");
208 
209     tv = XXH64TestData(
210         0x2000, 0x55, [ 1,2,3,4,5,6,7,8 ],
211         dur!"seconds"(1), dur!"seconds"(1));
212     line = tv.getCsvLine;
213     assert(line == "0x2000;0x55;0102030405060708;1.000000;1.000000;0.007812;0.007812");
214 }
215 
216 /** Simple class to collect CSV data and write it to disk */
217 class TestCSVFile(T) {
218 private:
219     T[] testEntries; /// The collected test entries
220     string fileName; /// Name of CSV
221 
222 public:
223     /** Constructors */
224     this(string filename) {
225         fileName = filename;
226     }
227 
228     /** Add a test entry */
229     void addEntry(ref T entry) {
230         testEntries ~= entry;
231     }
232 
233     /** Write data as a CSV file */
234     void writeFile() {
235         /* Convert data to CSV strings */
236         string[] csvlines;
237         csvlines ~= T.getCsvHeader;
238         foreach (entry; testEntries)
239             csvlines ~= entry.getCsvLine;
240         auto csvblob = csvlines.join("\n") ~ "\n\n";
241         /* And finally write them to disk */
242         std.file.write(fileName, csvblob);
243     }
244 }
245 
246 unittest {
247     auto fn = deleteme();
248     auto tcf = new TestCSVFile!CompressionTestData(fn);
249     scope (exit)
250         remove(fn);
251     auto tc = CompressionTestData();
252     tcf.addEntry(tc);
253     tcf.writeFile;
254 
255     enum expectedContents =
256         "pattern;unpacked_size;packed_size;pack_duration;unpack_duration;pack_ratio;pack_mbs;unpack_mbs\n" ~
257         "(Unknown:00000:00000:00);0;0;0.000000;0.000000;nan;nan;nan\n\n";
258     const auto csvcontents = readText(fn);
259     assert(csvcontents == expectedContents, "Mismatched file contents");
260 }
261 
262 unittest {
263     auto tp = TestPattern(PatterMode.RepeatN, 0x00100, 0x00200, 8);
264 
265     auto fn = deleteme();
266     auto tcf = new TestCSVFile!CompressionTestData(fn);
267     scope (exit)
268         remove(fn);
269     auto tc1 = CompressionTestData(tp, 2 ^^ 20, 2 ^^ 10, dur!"seconds"(1), dur!"seconds"(1));
270     tcf.addEntry(tc1);
271     auto tc2 = CompressionTestData(tp, 10 ^^ 6, 10 ^^ 3, dur!"seconds"(1), dur!"seconds"(1));
272     tcf.addEntry(tc2);
273     tcf.writeFile;
274 
275     enum expectedContents =
276         "pattern;unpacked_size;packed_size;pack_duration;unpack_duration;pack_ratio;pack_mbs;unpack_mbs\n" ~
277         "(RepeatN:00100:00200:08);1048576;1024;1.000000;1.000000;0.000977;1.000000;1.000000\n" ~
278         "(RepeatN:00100:00200:08);1000000;1000;1.000000;1.000000;0.001000;0.953674;0.953674\n" ~
279         "\n";
280     const auto csvcontents = readText(fn);
281     assert(csvcontents == expectedContents, "Mismatched file contents");
282 }
283 
284 unittest {
285     auto fn = deleteme();
286     auto tcf = new TestCSVFile!MD5TestData(fn);
287     scope (exit)
288         remove(fn);
289     auto tc = MD5TestData();
290     tcf.addEntry(tc);
291     tcf.writeFile;
292 
293     enum expectedContents =
294         "size;fill;hash;duration1;duration2;mbs1;mbs2\n" ~
295         "0x0000;0x00;00000000000000000000000000000000;0.000000;0.000000;nan;nan\n\n";
296     const auto csvcontents = readText(fn);
297     assert(csvcontents == expectedContents, "Mismatched file contents");
298 }
299 
300 unittest {
301     auto fn = deleteme();
302     auto tcf = new TestCSVFile!MD5TestData(fn);
303     scope (exit)
304         remove(fn);
305     auto tc1 = MD5TestData(
306         0x1000, 0xAA, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
307         dur!"seconds"(1), dur!"seconds"(1));
308     tcf.addEntry(tc1);
309     auto tc2 = MD5TestData(
310         0x2000, 0x55, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
311         dur!"seconds"(1), dur!"seconds"(1));
312     tcf.addEntry(tc2);
313     tcf.writeFile;
314 
315     enum expectedContents =
316         "size;fill;hash;duration1;duration2;mbs1;mbs2\n" ~
317         "0x1000;0xaa;000102030405060708090A0B0C0D0E0F;1.000000;1.000000;0.003906;0.003906\n" ~
318         "0x2000;0x55;000102030405060708090A0B0C0D0E0F;1.000000;1.000000;0.007812;0.007812\n" ~
319         "\n";
320     const auto csvcontents = readText(fn);
321     assert(csvcontents == expectedContents, "Mismatched file contents");
322 }