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 public import common: printChildren, shouldMatch; 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 /** 115 Create a variable called `tu` that is either a MockCursor or a real 116 clang one depending on the type T 117 */ 118 mixin template createTU(T, string moduleName, string testName) { 119 mixin(mockTuMixin); 120 static if(is(T == Cursor)) 121 const tu = parse!(moduleName, testName); 122 else 123 auto tu = mockTU.create(); 124 } 125 126 127 /** 128 To be used as a UDA on contract tests establishing how to create a mock 129 translation unit cursor that behaves _exactly_ the same as the one 130 obtained by libclang. This is enforced at contract test time. 131 */ 132 struct MockTU(alias F) { 133 alias create = F; 134 } 135 136 string mockTuMixin(in string file = __FILE__, in size_t line = __LINE__) @safe pure { 137 import std.format: format; 138 return q{ 139 import std.traits: getUDAs; 140 alias mockTuUdas = getUDAs!(__traits(parent, {}), MockTU); 141 static assert(mockTuUdas.length == 1, "%s:%s Only one @MockTU allowed"); 142 alias mockTU = mockTuUdas[0]; 143 }.format(file, line); 144 } 145 146 /// Walks like a clang.Cursor, quacks like a clang.Cursor 147 struct MockCursor { 148 import clang: Cursor; 149 150 alias Kind = Cursor.Kind; 151 152 Kind kind; 153 string spelling; 154 MockType type; 155 MockCursor[] children; 156 private MockType _underlyingType; 157 bool isDefinition; 158 bool isCanonical; 159 160 // Returns a pointer so that the child can be modified 161 auto child(this This)(int index) { 162 163 return index >= 0 && index < children.length 164 ? &children[index] 165 : null; 166 } 167 168 auto underlyingType(this This)() return scope { 169 return &_underlyingType; 170 } 171 172 string toString() @safe pure const { 173 import std.conv: text; 174 const children = children.length 175 ? text(", ", children) 176 : ""; 177 return text("MockCursor(", kind, `, "`, spelling, `"`, children, `)`); 178 } 179 } 180 181 const(Cursor) child(in Cursor cursor, int index) @safe { 182 return cursor.children[index]; 183 } 184 185 /// Walks like a clang.Type, quacks like a clang.Type 186 struct MockType { 187 import clang: Type; 188 189 alias Kind = Type.Kind; 190 191 Kind kind; 192 string spelling; 193 private MockType* _canonical; 194 195 auto canonical(this This)() return scope { 196 static if(!is(This == const)) 197 if(_canonical is null) _canonical = new MockType; 198 return _canonical; 199 } 200 } 201 202 203 struct TestName { string value; } 204 205 206 207 /** 208 Defines a contract test by mixing in a new test function. 209 210 The test function actually does a few things: 211 * Verify the contract that libclang returns what we expect it to. 212 * Use the *same* code to construct a mock translation unit cursor 213 that also satisfies the contract. 214 * Verifies the mock also passes the test. 215 216 Two functions are generated: the contract test, and a helper function 217 that does the heavy lifting. The separation is so the 2nd function can 218 be called from unit tests to generate the mock. 219 220 This 2nd function isn't supposed to be called directly, but is found 221 via compile-time reflection in mockTU. 222 223 Parameters: 224 testName = The name of the new test. 225 contractFunction = The function that verifies the contract or creates the mock. 226 */ 227 mixin template Contract(TestName testName, alias contractFunction, size_t line = __LINE__) { 228 import unit_threaded: unittestFunctionName; 229 import std.format: format; 230 import std.traits: getUDAs; 231 232 alias udas = getUDAs!(contractFunction, ContractFunction); 233 static assert(udas.length == 1, 234 "`" ~ __traits(identifier, contractFunction) ~ 235 "` is not a contract function without exactly one @ContractFunction`"); 236 enum codeURL = udas[0].codeURL; 237 238 enum testFunctionName = unittestFunctionName(line); 239 enum code = q{ 240 241 // This is the test function that will be run by unit-threaded 242 @Name("%s") 243 @UnitTest 244 @Types!(Cursor, MockCursor) 245 void %s(CursorType)() 246 { 247 auto tu = createTranslationUnit!(CursorType, codeURL, contractFunction); 248 contractFunction!(TestMode.verify)(cast(const) tu); 249 } 250 }.format(testName.value, testFunctionName); 251 252 //pragma(msg, code); 253 254 mixin(code); 255 256 } 257 258 /** 259 Creates a real or mock translation unit depending on the type 260 */ 261 auto createTranslationUnit(CursorType, CodeURL codeURL, alias contractFunction)() { 262 import std.traits: Unqual; 263 static if(is(Unqual!CursorType == Cursor)) 264 return cast(const) createRealTranslationUnit!codeURL; 265 else 266 return createMockTranslationUnit!contractFunction; 267 268 } 269 270 auto createRealTranslationUnit(CodeURL codeURL)() { 271 return parse!(codeURL.module_, codeURL.test); 272 } 273 274 275 auto createMockTranslationUnit(alias contractFunction)() { 276 MockCursor tu; 277 contractFunction!(TestMode.mock)(tu); 278 return tu; 279 } 280 281 enum TestMode { 282 verify, // check that the value is as expected (contract test) 283 mock, // create a mock object that behaves like the real thing 284 } 285 286 287 /** 288 To be used as a UDA indicating a function that does double duty as: 289 * a contract test 290 * builds a mock to satisfy the same contract 291 */ 292 struct ContractFunction { 293 CodeURL codeURL; 294 } 295 296 struct Module { 297 string name; 298 } 299 300 /** 301 Searches `moduleName` for a contract function that creates a mock 302 translation unit cursor, calls it, and returns the value 303 */ 304 auto mockTU(Module moduleName, CodeURL codeURL)() { 305 306 mixin(`import `, moduleName.name, `;`); 307 import std.meta: Alias, AliasSeq, Filter, staticMap; 308 import std.traits: hasUDA, getUDAs; 309 import std.algorithm: startsWith; 310 import std.conv: text; 311 312 alias module_ = Alias!(mixin(moduleName.name)); 313 alias memberNames = AliasSeq!(__traits(allMembers, module_)); 314 enum hasContractName(string name) = name.startsWith("contract_"); 315 alias contractNames = Filter!(hasContractName, memberNames); 316 317 alias Member(string name) = Alias!(mixin(name)); 318 alias contractFunctions = staticMap!(Member, contractNames); 319 enum hasURL(alias F) = 320 hasUDA!(F, ContractFunction) 321 && getUDAs!(F, ContractFunction).length == 1 322 && getUDAs!(F, ContractFunction)[0].codeURL == codeURL; 323 alias contractFunctionsWithURL = Filter!(hasURL, contractFunctions); 324 325 static assert(contractFunctionsWithURL.length > 0, 326 text("Cannot find ", codeURL, " anywhere in module ", moduleName.name)); 327 328 enum identifier(alias F) = __traits(identifier, F); 329 static assert(contractFunctionsWithURL.length == 1, 330 text("Too many (", contractFunctionsWithURL.length, 331 ") contract functions for ", codeURL, " in ", moduleName.name, ": ", 332 staticMap!(identifier, contractFunctionsWithURL))); 333 334 alias contractFunction = contractFunctionsWithURL[0]; 335 336 MockCursor cursor; 337 contractFunction!(TestMode.mock)(cursor); 338 return cursor; 339 } 340 341 342 auto expect(L) 343 (auto ref L lhs, in string file = __FILE__, in size_t line = __LINE__) 344 { 345 struct Expect { 346 347 bool opEquals(R)(auto ref R rhs) { 348 import std.functional: forward; 349 enum mode = InferTestMode!lhs; 350 expectEqualImpl!mode(forward!lhs, forward!rhs, file, line); 351 return true; 352 } 353 } 354 355 return Expect(); 356 } 357 358 // Used with Cursor and Type objects, simultaneously assert the kind and spelling 359 // of the passed in object, or actually set those values when mocking 360 auto expectEqual(L, K) 361 (auto ref L lhs, in K kind, in string spelling, in string file = __FILE__, in size_t line = __LINE__) 362 { 363 enum mode = InferTestMode!lhs; 364 expectEqualImpl!mode(lhs.kind, kind, file, line); 365 expectEqualImpl!mode(lhs.spelling, spelling, file, line); 366 } 367 368 369 /** 370 Calculate if we're in mocking or verifying mode using reflection 371 */ 372 template InferTestMode(alias lhs) { 373 import std.traits: isPointer; 374 375 alias L = typeof(lhs); 376 377 template isConst(T) { 378 import std.traits: isPointer, PointerTarget; 379 static if(isPointer!T) 380 enum isConst = isConst!(PointerTarget!T); 381 else 382 enum isConst = is(T == const); 383 } 384 385 static if(!__traits(isRef, lhs) && !isPointer!L) 386 enum InferTestMode = TestMode.verify; // can't modify non-ref 387 else static if(isConst!L) 388 enum InferTestMode = TestMode.verify; // can't modify const 389 else 390 enum InferTestMode = TestMode.mock; 391 } 392 393 /** 394 Depending the mode, either assign the given value to lhs 395 or assert that lhs == rhs. 396 Used in contract functions. 397 */ 398 private void expectEqualImpl(TestMode mode, L, R) 399 (auto ref L lhs, auto ref R rhs, in string file = __FILE__, in size_t line = __LINE__) 400 if(is(typeof(lhs == rhs) == bool) || is(R == L*)) 401 { 402 import std.traits: isPointer, PointerTarget; 403 404 static if(mode == TestMode.verify) { 405 static if(isPointer!L && isPointer!R) 406 (*lhs).shouldEqual(*rhs, file, line); 407 else static if(isPointer!L) 408 (*lhs).shouldEqual(rhs, file, line); 409 else static if(isPointer!R) 410 lhs.shouldEqual(*rhs, file, line); 411 else 412 lhs.shouldEqual(rhs, file, line); 413 } else static if(mode == TestMode.mock) { 414 415 static if(isPointer!L && isPointer!R) 416 *lhs = *rhs; 417 else static if(isPointer!L) 418 *lhs = rhs; 419 else static if(isPointer!R) 420 lhs = *rhs; 421 else { 422 lhs = rhs; 423 } 424 } else 425 static assert(false, "Unknown mode " ~ mode.stringof); 426 } 427 428 429 auto expectLength(L) 430 (auto ref L lhs, in string file = __FILE__, in size_t line = __LINE__) 431 { 432 struct Expect { 433 434 bool opEquals(in size_t length) { 435 import std.functional: forward; 436 enum mode = InferTestMode!lhs; 437 expectLengthEqualImpl!mode(forward!lhs, length, file, line); 438 return true; 439 } 440 } 441 442 return Expect(); 443 } 444 445 446 private void expectLengthEqualImpl(TestMode mode, R) 447 (auto ref R range, in size_t length, in string file = __FILE__, in size_t line = __LINE__) 448 { 449 enum mode = InferTestMode!range; 450 451 static if(mode == TestMode.verify) 452 range.length.shouldEqual(length, file, line); 453 else static if(mode == TestMode.mock) 454 range.length = length; 455 else 456 static assert(false, "Unknown mode " ~ mode.stringof); 457 }