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 }