1 // This file is licensed under the Boost License, with code adopted from Silly (https://gitlab.com/AntonMeep/silly), 2 // which is licensed under the ISC license: 3 // Copyright (c) 2019, Anton Fediushin 4 // 5 // Permission to use, copy, modify, and/or distribute this software for any 6 // purpose with or without fee is hereby granted, provided that the above 7 // copyright notice and this permission notice appear in all copies. 8 // 9 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 17 module tests.data; 18 import tests.utils; 19 import std.path : buildPath; 20 21 __gshared string testDataLocation = "../ion-tests/iontestdata"; 22 23 enum IonTestData { 24 good, 25 goodTypecodes, 26 goodTimestamp, 27 bad, 28 badTypecodes, 29 badTimestamp, 30 equivs, 31 nonequivs, 32 roundtrip 33 }; 34 35 // iontestdata/good 36 enum ION_GOOD_TEST_DATA = "good"; 37 // iontestdata/good/typecodes 38 enum ION_GOOD_TYPECODES_TEST_DATA = buildPath(ION_GOOD_TEST_DATA, "typecodes"); 39 // iontestdata/good/timestamp 40 enum ION_GOOD_TIMESTAMP_TEST_DATA = buildPath(ION_GOOD_TEST_DATA, "timestamp"); 41 // iontestdata/bad 42 enum ION_BAD_TEST_DATA = "bad"; 43 // iontestdata/bad/typecodes 44 enum ION_BAD_TYPECODES_TEST_DATA = buildPath(ION_BAD_TEST_DATA, "typecodes"); 45 // iontestdata/bad/timestamp 46 enum ION_BAD_TIMESTAMP_TEST_DATA = buildPath(ION_BAD_TEST_DATA, "timestamp"); 47 // iontestdata/good/equivs 48 enum ION_EQUIVS_TEST_DATA = buildPath(ION_GOOD_TEST_DATA, "equivs"); 49 // iontestdata/good/non-equivs 50 enum ION_NONEQUIVS_TEST_DATA = buildPath(ION_GOOD_TEST_DATA, "non-equivs"); 51 enum ION_ROUNDTRIP_TEST_DATA = "good"; 52 53 static immutable const(char)[][] ION_TEST_DATA = [ 54 ION_GOOD_TEST_DATA, 55 ION_GOOD_TYPECODES_TEST_DATA, 56 ION_GOOD_TIMESTAMP_TEST_DATA, 57 ION_BAD_TEST_DATA, 58 ION_BAD_TYPECODES_TEST_DATA, 59 ION_BAD_TIMESTAMP_TEST_DATA, 60 ION_EQUIVS_TEST_DATA, 61 ION_NONEQUIVS_TEST_DATA, 62 ION_ROUNDTRIP_TEST_DATA 63 ]; 64 65 static immutable ION_GOOD_TEST_DATA_SKIP = [ 66 // upstream implementations can't parse these files, don't bother 67 "good/subfieldVarUInt32bit.ion", 68 "good/utf16.ion", 69 "good/utf32.ion", 70 "good/whitespace.ion", 71 "good/item1.10n", 72 "good/testfile26.ion", 73 "good/localSymbolTableImportZeroMaxId.ion", 74 // Shared symbol tables support is TBD 75 "good/subfieldVarUInt.ion", 76 "good/subfieldVarUInt15bit.ion", 77 "good/testfile35.ion", 78 "good/subfieldVarUInt16bit.ion", 79 // We shouldn't have a IonValueStream that's fully empty in real data 80 "good/empty.ion", 81 "good/blank.ion", 82 // Mir supports up to 1024 bytes big integers/decimal for coefficient. 83 // This test requires 1201 bytes for coefficient. 84 "good/intBigSize1201.10n", 85 ]; 86 87 static immutable ION_GOOD_TYPECODES_TEST_DATA_SKIP = []; 88 89 static immutable ION_GOOD_TIMESTAMP_TEST_DATA_SKIP = []; 90 91 static immutable ION_BAD_TEST_DATA_SKIP = [ 92 "bad/clobWithNullCharacter.ion" 93 ]; 94 95 static immutable ION_BAD_TYPECODES_TEST_DATA_SKIP = [ 96 "bad/typecodes/type_6_length_0.10n" 97 ]; 98 99 static immutable ION_BAD_TIMESTAMP_TEST_DATA_SKIP = []; 100 101 static immutable ION_EQUIVS_TEST_DATA_SKIP = [ 102 "good/equivs/clobNewlines.ion", 103 ]; 104 105 static immutable ION_NONEQUIVS_TEST_DATA_SKIP = []; 106 107 static immutable ION_ROUNDTRIP_TEST_DATA_SKIP = ION_GOOD_TEST_DATA_SKIP ~ [ 108 "good/testfile37.ion", 109 "good/testfile23.ion", 110 ]; 111 112 static immutable const(char)[][][] ION_TEST_DATA_SKIP = [ 113 ION_GOOD_TEST_DATA_SKIP, 114 ION_GOOD_TYPECODES_TEST_DATA_SKIP, 115 ION_GOOD_TIMESTAMP_TEST_DATA_SKIP, 116 ION_BAD_TEST_DATA_SKIP, 117 ION_BAD_TYPECODES_TEST_DATA_SKIP, 118 ION_BAD_TIMESTAMP_TEST_DATA_SKIP, 119 ION_EQUIVS_TEST_DATA_SKIP, 120 ION_NONEQUIVS_TEST_DATA_SKIP, 121 ION_ROUNDTRIP_TEST_DATA_SKIP, 122 ]; 123 124 bool isSkippedFile(IonTestData testData, string path) { 125 import std.algorithm.searching : any, canFind; 126 return ION_TEST_DATA_SKIP[testData].any!(e => path.canFind(e)); 127 } 128 129 enum IonDataType { 130 binary, 131 text, 132 all 133 }; 134 135 struct Test { 136 string filePath; 137 string name; 138 bool verbose; 139 bool expectedFail; 140 IonDataType type; 141 ubyte[] data; 142 143 void run(alias m)(out TestResult result, bool failFast, bool verbose) { 144 result.test = this; 145 this.verbose = verbose; 146 147 try { 148 m(this); 149 150 if (!expectedFail) { 151 result.passed = true; 152 } 153 } catch (Throwable t) { 154 if (expectedFail) { 155 result.passed = true; 156 } 157 158 import core.exception : AssertError; 159 foreach(th; t) { 160 Thrown thrown; 161 162 foreach(exc; th.info) { 163 thrown.info ~= exc.idup; 164 } 165 thrown.type = typeid(th).name; 166 thrown.file = th.file; 167 thrown.message = th.message.idup; 168 thrown.line = th.line; 169 170 result.thrown ~= thrown; 171 } 172 173 if (!(cast(Exception) t || cast(AssertError) t)) { 174 throw t; 175 } else if (failFast && !expectedFail) { 176 throw t; 177 } 178 } 179 } 180 } 181 182 struct Thrown { 183 string type; 184 string message; 185 string file; 186 size_t line; 187 immutable(string)[] info; 188 } 189 190 struct TestResult { 191 Test test; 192 bool passed; 193 immutable(Thrown)[] thrown; 194 195 void print(bool failuresOnly = false, bool verbose = false) { 196 import std.format : formattedWrite; 197 import std.stdio : stdout; 198 if (failuresOnly && passed) 199 return; 200 201 auto writer = stdout.lockingTextWriter; 202 writer.formattedWrite("[%s] %s\n", 203 passed ? "✓".okayText 204 : "✗".failText, 205 test.name.emphasizeText, 206 test.filePath); 207 208 // If this is an expected failure test case AND we want verbose messaging, 209 // or if this is just a plain test case, then print it out 210 if (test.expectedFail && !passed) { 211 writer.formattedWrite(" expectedFail: Test did not throw when it was expected to.\n"); 212 } 213 else if ((test.expectedFail && verbose) || (!test.expectedFail)) { 214 foreach(th; thrown) { 215 writer.formattedWrite(" %s: %s (file: %s:%d)\n", 216 th.type, 217 th.message, 218 th.file, 219 th.line); 220 221 if (verbose) { 222 writer.formattedWrite(" ------ STACK TRACE -----\n"); 223 foreach(line; th.info) { 224 writer.formattedWrite(" %s\n", line); 225 } 226 } 227 } 228 } 229 } 230 } 231 232 Test[] loadIonTestData(string root, IonTestData dataType, bool expectedFail, IonDataType wantedType) { 233 import std.array : array, join; 234 import std.file : read, dirEntries, SpanMode, DirEntry; 235 import std.path : buildPath, relativePath, extension; 236 string path = buildPath(root, ION_TEST_DATA[dataType]); 237 Test[] testCases; 238 string searchPattern = "*{.ion,.10n}"; 239 if (wantedType == IonDataType.binary) 240 searchPattern = "*{.10n}"; 241 else if (wantedType == IonDataType.text) 242 searchPattern = "*{.ion}"; 243 244 foreach (DirEntry e; dirEntries(path, searchPattern, SpanMode.shallow)) { 245 if (dataType.isSkippedFile(e.name)) { 246 continue; 247 } 248 249 Test testCase; 250 testCase.filePath = e.name; 251 testCase.name = relativePath(e.name, testDataLocation); 252 testCase.data = cast(ubyte[])read(e.name); 253 testCase.type = e.name.extension == ".10n" ? IonDataType.binary : IonDataType.text; 254 testCase.expectedFail = expectedFail; 255 256 testCases ~= testCase; 257 } 258 259 return testCases; 260 }