1 /** 2 Contract tests for libclang. 3 https://martinfowler.com/bliki/ContractTest.html 4 */ 5 module contract; 6 7 import dpp.from; 8 9 public import unit_threaded; 10 public import clang: Cursor, Type; 11 12 13 struct C { 14 string value; 15 } 16 17 18 struct Cpp { 19 string value; 20 } 21 22 /** 23 A way to identify a snippet of C/C++ code for testing. 24 25 A test exists somewhere in the code base named `test` in a D module `module_`. 26 This test has an attached UDA with a code snippet. 27 */ 28 struct CodeURL { 29 string module_; 30 string test; 31 } 32 33 34 /// Parses C/C++ code located in a UDA on `codeURL` 35 auto parse(CodeURL codeURL)() { 36 return parse!(codeURL.module_, codeURL.test); 37 } 38 39 40 /** 41 Parses C/C++ code located in a UDA on `testName` 42 which is in module `moduleName` 43 */ 44 auto parse(string moduleName, string testName)() { 45 mixin(`static import ` ~ moduleName ~ ";"); 46 static import it; 47 import std.meta: AliasSeq, staticMap, Filter; 48 import std.traits: getUDAs, isSomeString; 49 50 alias tests = AliasSeq!(__traits(getUnitTests, mixin(moduleName))); 51 52 template TestName(alias T) { 53 alias attrs = AliasSeq!(__traits(getAttributes, T)); 54 55 template isSomeString_(alias S) { 56 static if(is(typeof(S))) 57 enum isSomeString_ = isSomeString!(typeof(S)); 58 else 59 enum isSomeString_ = false; 60 } 61 alias strAttrs = Filter!(isSomeString_, attrs); 62 static assert(strAttrs.length == 1); 63 enum TestName = strAttrs[0]; 64 } 65 66 enum hasRightName(alias T) = TestName!T == testName; 67 alias rightNameTests = Filter!(hasRightName, tests); 68 static assert(rightNameTests.length == 1); 69 alias test = rightNameTests[0]; 70 static assert(getUDAs!(test, it.C).length == 1, 71 "No `C` UDA on " ~ __traits(identifier, test)); 72 enum cCode = getUDAs!(test, it.C)[0]; 73 74 return .parse(C(cCode.code)); 75 } 76 77 78 C cCode(string moduleName, int index = 0)() { 79 mixin(`static import ` ~ moduleName ~ ";"); 80 static import it; 81 import std.meta: Alias; 82 import std.traits: getUDAs; 83 alias test = Alias!(__traits(getUnitTests, it.c.compile.struct_)[index]); 84 enum cCode = getUDAs!(test, it.C)[0]; 85 return C(cCode.code); 86 } 87 88 auto parse(T) 89 ( 90 in T code, 91 in from!"clang".TranslationUnitFlags tuFlags = from!"clang".TranslationUnitFlags.None, 92 ) 93 { 94 import unit_threaded.integration: Sandbox; 95 import clang: parse_ = parse; 96 97 enum isCpp = is(T == Cpp); 98 99 with(immutable Sandbox()) { 100 const extension = isCpp ? "cpp" : "c"; 101 const fileName = "code." ~ extension; 102 writeFile(fileName, code.value); 103 104 auto tu = parse_(inSandboxPath(fileName), 105 isCpp ? ["-std=c++14"] : [], 106 tuFlags) 107 .cursor; 108 printChildren(tu); 109 return tu; 110 } 111 } 112 113 114 void printChildren(T)(auto ref in T cursorOrTU) { 115 import clang: TranslationUnit, Cursor; 116 import std.traits: Unqual; 117 118 static if(is(Unqual!T == TranslationUnit) || is(Unqual!T == Cursor)) { 119 120 import unit_threaded.io: writelnUt; 121 import std.algorithm: map; 122 import std.array: join; 123 import std.conv: text; 124 125 static if(is(Unqual!T == TranslationUnit)) 126 const children = cursorOrTU.cursor.children; 127 else 128 const children = cursorOrTU.children; 129 130 writelnUt("\n", cursorOrTU, " children:\n[\n", children.map!(a => text(" ", a)).join(",\n")); 131 writelnUt("]\n"); 132 } 133 } 134 135 /** 136 Create a variable called `tu` that is either a MockCursor or a real 137 clang one depending on the type T 138 */ 139 mixin template createTU(T, string moduleName, string testName) { 140 mixin(mockTuMixin); 141 static if(is(T == Cursor)) 142 const tu = parse!(moduleName, testName); 143 else 144 auto tu = mockTU.create(); 145 } 146 147 148 /** 149 To be used as a UDA on contract tests establishing how to create a mock 150 translation unit cursor that behaves _exactly_ the same as the one 151 obtained by libclang. This is enforced at contract test time. 152 */ 153 struct MockTU(alias F) { 154 alias create = F; 155 } 156 157 string mockTuMixin(in string file = __FILE__, in size_t line = __LINE__) @safe pure { 158 import std.format: format; 159 return q{ 160 import std.traits: getUDAs; 161 alias mockTuUdas = getUDAs!(__traits(parent, {}), MockTU); 162 static assert(mockTuUdas.length == 1, "%s:%s Only one @MockTU allowed"); 163 alias mockTU = mockTuUdas[0]; 164 }.format(file, line); 165 } 166 167 /// Walks like a clang.Cursor, quacks like a clang.Cursor 168 struct MockCursor { 169 import clang: Cursor; 170 171 alias Kind = Cursor.Kind; 172 173 Kind kind; 174 string spelling; 175 MockType type; 176 MockCursor[] children; 177 private MockType _underlyingType; 178 bool isDefinition; 179 bool isCanonical; 180 181 // Returns a pointer so that the child can be modified 182 auto child(this This)(int index) { 183 184 return index >= 0 && index < children.length 185 ? &children[index] 186 : null; 187 } 188 189 auto underlyingType(this This)() return scope { 190 return &_underlyingType; 191 } 192 193 string toString() @safe pure const { 194 import std.conv: text; 195 const children = children.length 196 ? text(", ", children) 197 : ""; 198 return text("MockCursor(", kind, `, "`, spelling, `"`, children, `)`); 199 } 200 } 201 202 const(Cursor) child(in Cursor cursor, int index) @safe { 203 return cursor.children[index]; 204 } 205 206 /// Walks like a clang.Type, quacks like a clang.Type 207 struct MockType { 208 import clang: Type; 209 210 alias Kind = Type.Kind; 211 212 Kind kind; 213 string spelling; 214 private MockType* _canonical; 215 216 auto canonical(this This)() return scope { 217 static if(!is(This == const)) 218 if(_canonical is null) _canonical = new MockType; 219 return _canonical; 220 } 221 } 222 223 224 struct TestName { string value; } 225 226 227 228 /** 229 Defines a contract test by mixing in a new test function. 230 231 The test function actually does a few things: 232 * Verify the contract that libclang returns what we expect it to. 233 * Use the *same* code to construct a mock translation unit cursor 234 that also satisfies the contract. 235 * Verifies the mock also passes the test. 236 237 Two functions are generated: the contract test, and a helper function 238 that does the heavy lifting. The separation is so the 2nd function can 239 be called from unit tests to generate the mock. 240 241 This 2nd function isn't supposed to be called directly, but is found 242 via compile-time reflection in mockTU. 243 244 Parameters: 245 testName = The name of the new test. 246 contractFunction = The function that verifies the contract or creates the mock. 247 */ 248 mixin template Contract(TestName testName, alias contractFunction, size_t line = __LINE__) { 249 import unit_threaded: unittestFunctionName; 250 import std.format: format; 251 import std.traits: getUDAs; 252 253 alias udas = getUDAs!(contractFunction, ContractFunction); 254 static assert(udas.length == 1, 255 "`" ~ __traits(identifier, contractFunction) ~ 256 "` is not a contract function without exactly one @ContractFunction`"); 257 enum codeURL = udas[0].codeURL; 258 259 enum testFunctionName = unittestFunctionName(line); 260 enum code = q{ 261 262 // This is the test function that will be run by unit-threaded 263 @Name("%s") 264 @UnitTest 265 @Types!(Cursor, MockCursor) 266 void %s(CursorType)() 267 { 268 auto tu = createTranslationUnit!(CursorType, codeURL, contractFunction); 269 contractFunction!(TestMode.verify)(cast(const) tu); 270 } 271 }.format(testName.value, testFunctionName); 272 273 //pragma(msg, code); 274 275 mixin(code); 276 277 } 278 279 /** 280 Creates a real or mock translation unit depending on the type 281 */ 282 auto createTranslationUnit(CursorType, CodeURL codeURL, alias contractFunction)() { 283 import std.traits: Unqual; 284 static if(is(Unqual!CursorType == Cursor)) 285 return cast(const) createRealTranslationUnit!codeURL; 286 else 287 return createMockTranslationUnit!contractFunction; 288 289 } 290 291 auto createRealTranslationUnit(CodeURL codeURL)() { 292 return parse!(codeURL.module_, codeURL.test); 293 } 294 295 296 auto createMockTranslationUnit(alias contractFunction)() { 297 MockCursor tu; 298 contractFunction!(TestMode.mock)(tu); 299 return tu; 300 } 301 302 enum TestMode { 303 verify, // check that the value is as expected (contract test) 304 mock, // create a mock object that behaves like the real thing 305 } 306 307 308 /** 309 To be used as a UDA indicating a function that does double duty as: 310 * a contract test 311 * builds a mock to satisfy the same contract 312 */ 313 struct ContractFunction { 314 CodeURL codeURL; 315 } 316 317 struct Module { 318 string name; 319 } 320 321 /** 322 Searches `moduleName` for a contract function that creates a mock 323 translation unit cursor, calls it, and returns the value 324 */ 325 auto mockTU(Module moduleName, CodeURL codeURL)() { 326 327 mixin(`import `, moduleName.name, `;`); 328 import std.meta: Alias, AliasSeq, Filter, staticMap; 329 import std.traits: hasUDA, getUDAs; 330 import std.algorithm: startsWith; 331 import std.conv: text; 332 333 alias module_ = Alias!(mixin(moduleName.name)); 334 alias memberNames = AliasSeq!(__traits(allMembers, module_)); 335 enum hasContractName(string name) = name.startsWith("contract_"); 336 alias contractNames = Filter!(hasContractName, memberNames); 337 338 alias Member(string name) = Alias!(mixin(name)); 339 alias contractFunctions = staticMap!(Member, contractNames); 340 enum hasURL(alias F) = 341 hasUDA!(F, ContractFunction) 342 && getUDAs!(F, ContractFunction).length == 1 343 && getUDAs!(F, ContractFunction)[0].codeURL == codeURL; 344 alias contractFunctionsWithURL = Filter!(hasURL, contractFunctions); 345 346 static assert(contractFunctionsWithURL.length > 0, 347 text("Cannot find ", codeURL, " anywhere in module ", moduleName.name)); 348 349 enum identifier(alias F) = __traits(identifier, F); 350 static assert(contractFunctionsWithURL.length == 1, 351 text("Too many (", contractFunctionsWithURL.length, 352 ") contract functions for ", codeURL, " in ", moduleName.name, ": ", 353 staticMap!(identifier, contractFunctionsWithURL))); 354 355 alias contractFunction = contractFunctionsWithURL[0]; 356 357 MockCursor cursor; 358 contractFunction!(TestMode.mock)(cursor); 359 return cursor; 360 } 361 362 363 auto expect(L) 364 (auto ref L lhs, in string file = __FILE__, in size_t line = __LINE__) 365 { 366 struct Expect { 367 368 bool opEquals(R)(auto ref R rhs) { 369 import std.functional: forward; 370 enum mode = InferTestMode!lhs; 371 expectEqualImpl!mode(forward!lhs, forward!rhs, file, line); 372 return true; 373 } 374 } 375 376 return Expect(); 377 } 378 379 // Used with Cursor and Type objects, simultaneously assert the kind and spelling 380 // of the passed in object, or actually set those values when mocking 381 auto expectEqual(L, K) 382 (auto ref L lhs, in K kind, in string spelling, in string file = __FILE__, in size_t line = __LINE__) 383 { 384 enum mode = InferTestMode!lhs; 385 expectEqualImpl!mode(lhs.kind, kind, file, line); 386 expectEqualImpl!mode(lhs.spelling, spelling, file, line); 387 } 388 389 390 /** 391 Calculate if we're in mocking or verifying mode using reflection 392 */ 393 template InferTestMode(alias lhs) { 394 import std.traits: isPointer; 395 396 alias L = typeof(lhs); 397 398 template isConst(T) { 399 import std.traits: isPointer, PointerTarget; 400 static if(isPointer!T) 401 enum isConst = isConst!(PointerTarget!T); 402 else 403 enum isConst = is(T == const); 404 } 405 406 static if(!__traits(isRef, lhs) && !isPointer!L) 407 enum InferTestMode = TestMode.verify; // can't modify non-ref 408 else static if(isConst!L) 409 enum InferTestMode = TestMode.verify; // can't modify const 410 else 411 enum InferTestMode = TestMode.mock; 412 } 413 414 /** 415 Depending the mode, either assign the given value to lhs 416 or assert that lhs == rhs. 417 Used in contract functions. 418 */ 419 private void expectEqualImpl(TestMode mode, L, R) 420 (auto ref L lhs, auto ref R rhs, in string file = __FILE__, in size_t line = __LINE__) 421 if(is(typeof(lhs == rhs) == bool) || is(R == L*)) 422 { 423 import std.traits: isPointer, PointerTarget; 424 425 static if(mode == TestMode.verify) { 426 static if(isPointer!L && isPointer!R) 427 (*lhs).shouldEqual(*rhs, file, line); 428 else static if(isPointer!L) 429 (*lhs).shouldEqual(rhs, file, line); 430 else static if(isPointer!R) 431 lhs.shouldEqual(*rhs, file, line); 432 else 433 lhs.shouldEqual(rhs, file, line); 434 } else static if(mode == TestMode.mock) { 435 436 static if(isPointer!L && isPointer!R) 437 *lhs = *rhs; 438 else static if(isPointer!L) 439 *lhs = rhs; 440 else static if(isPointer!R) 441 lhs = *rhs; 442 else { 443 lhs = rhs; 444 } 445 } else 446 static assert(false, "Unknown mode " ~ mode.stringof); 447 } 448 449 450 auto expectLength(L) 451 (auto ref L lhs, in string file = __FILE__, in size_t line = __LINE__) 452 { 453 struct Expect { 454 455 bool opEquals(in size_t length) { 456 import std.functional: forward; 457 enum mode = InferTestMode!lhs; 458 expectLengthEqualImpl!mode(forward!lhs, length, file, line); 459 return true; 460 } 461 } 462 463 return Expect(); 464 } 465 466 467 private void expectLengthEqualImpl(TestMode mode, R) 468 (auto ref R range, in size_t length, in string file = __FILE__, in size_t line = __LINE__) 469 { 470 enum mode = InferTestMode!range; 471 472 static if(mode == TestMode.verify) 473 range.length.shouldEqual(length, file, line); 474 else static if(mode == TestMode.mock) 475 range.length = length; 476 else 477 static assert(false, "Unknown mode " ~ mode.stringof); 478 } 479 480 481 void shouldMatch(T, K)(T obj, in K kind, in string spelling, in string file = __FILE__, in size_t line = __LINE__) { 482 static assert(is(K == T.Kind)); 483 obj.kind.shouldEqual(kind, file, line); 484 obj.spelling.shouldEqual(spelling, file, line); 485 }