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 }