1 module unit_threaded.list;
2 
3 import std.traits;
4 import std.uni;
5 import std.typetuple;
6 import unit_threaded.check; //enum labels
7 
8 /**
9  * Common data for test functions and test classes
10  */
11 alias void function() TestFunction;
12 struct TestData {
13     string name;
14     bool hidden;
15     bool shouldFail;
16     TestFunction test; //only used for functions, null for classes
17     bool singleThreaded;
18     bool builtin;
19 }
20 
21 /**
22  * Finds all test classes (classes implementing a test() function)
23  * in the given module
24  */
25 auto getTestClasses(alias mod)() pure nothrow {
26     return getTestCases!(mod, isTestClass);
27 }
28 
29 /**
30  * Finds all test functions in the given module.
31  * Returns an array of TestData structs
32  */
33 auto getTestFunctions(alias mod)() pure nothrow {
34     return getTestCases!(mod, isTestFunction);
35 }
36 
37 /**
38  * Finds all built-in unittest blocks in the given module.
39  * @return An array of TestData structs
40  */
41 auto getBuiltinTests(alias mod)() pure nothrow {
42     import std.conv;
43     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
44     TestData[] testData;
45     int index;
46     foreach(test; __traits(getUnitTests, mod)) {
47         enum hidden = false;
48         enum shouldFail = false;
49         enum singleThreaded = false;
50         enum builtin = true;
51         try {
52             testData ~= TestData(fullyQualifiedName!mod ~ ".unittest" ~ (++index).to!string,
53                                  hidden, shouldFail, &test, singleThreaded, builtin);
54         } catch(Throwable) {
55             assert(false, text("Error converting ", index, " to string"));
56         }
57     }
58     return testData;
59 }
60 
61 private template HasAttribute(alias mod, string T, alias A) {
62     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
63     enum index = staticIndexOf!(A, __traits(getAttributes, mixin(T)));
64     static if(index >= 0) {
65         enum HasAttribute = true;
66     } else {
67         enum HasAttribute = false;
68     }
69 }
70 
71 
72 private template HasHidden(alias mod, string member) {
73     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
74     alias attrs = Filter!(isAHiddenStruct, __traits(getAttributes, mixin(member)));
75     static assert(attrs.length == 0 || attrs.length == 1,
76                   "Maximum number of HiddenTest attributes is 1");
77     static if(attrs.length == 0) {
78         enum HasHidden = false;
79     } else {
80         enum HasHidden = true;
81     }
82 }
83 
84 private template HasShouldFail(alias mod, string member) {
85     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
86     alias attrs = Filter!(isAShouldFailStruct, __traits(getAttributes, mixin(member)));
87     static assert(attrs.length == 0 || attrs.length == 1,
88                   "Maximum number of ShouldFail attributes is 1");
89     static if(attrs.length == 0) {
90         enum HasShouldFail = false;
91     } else {
92         enum HasShouldFail = true;
93     }
94 }
95 
96 
97 private auto getTestCases(alias mod, alias pred)() pure nothrow {
98     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
99     TestData[] testData;
100     foreach(moduleMember; __traits(allMembers, mod)) {
101 
102         enum notPrivate = __traits(compiles, mixin(moduleMember)); //only way I know to check if private
103 
104         static if(notPrivate && pred!(mod, moduleMember)) {
105             static if(!HasAttribute!(mod, moduleMember, DontTest)) {
106                 testData ~= createTestData!(mod, moduleMember);
107             }
108         }
109     }
110 
111     return testData;
112 }
113 
114 private auto createTestData(alias mod, string moduleMember)() pure nothrow {
115     TestFunction getTestFunction(alias mod, string moduleMember)() {
116     //returns a function pointer for test functions, null for test classes
117         static if(__traits(compiles, &__traits(getMember, mod, moduleMember))) {
118             return &__traits(getMember, mod, moduleMember);
119         } else {
120             return null;
121         }
122     }
123 
124     return TestData(fullyQualifiedName!mod ~ "." ~ moduleMember,
125                     HasHidden!(mod, moduleMember),
126                     HasShouldFail!(mod, moduleMember),
127                     getTestFunction!(mod, moduleMember),
128                     HasAttribute!(mod, moduleMember, SingleThreaded));
129 }
130 
131 private template isTestClass(alias mod, string moduleMember) {
132     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
133     static if(__traits(compiles, isAggregateType!(mixin(moduleMember)))) {
134         static if(isAggregateType!(mixin(moduleMember))) {
135 
136             enum hasUnitTest = HasAttribute!(mod, moduleMember, UnitTest);
137             enum hasTestMethod = __traits(hasMember, mixin(moduleMember), "test");
138 
139             enum isTestClass = hasTestMethod || hasUnitTest;
140         } else {
141             enum isTestClass = false;
142         }
143     } else {
144         enum isTestClass = false;
145     }
146 }
147 
148 
149 private template isTestFunction(alias mod, string moduleMember) {
150     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
151     static if(isSomeFunction!(mixin(moduleMember))) {
152         enum isTestFunction = hasTestPrefix!(mod, moduleMember) ||
153             HasAttribute!(mod, moduleMember, UnitTest);
154     } else {
155         enum isTestFunction = false;
156     }
157 }
158 
159 private template hasTestPrefix(alias mod, alias T) {
160     mixin("import " ~ fullyQualifiedName!mod ~ ";"); //so it's visible
161 
162     enum prefix = "test";
163     enum minSize = prefix.length + 1;
164 
165     static if(isSomeFunction!(mixin(T)) &&
166               T.length >= minSize && T[0 .. prefix.length] == "test" &&
167               isUpper(T[prefix.length])) {
168         enum hasTestPrefix = true;
169     } else {
170         enum hasTestPrefix = false;
171     }
172 }
173 
174 
175 import unit_threaded.tests.module_with_tests; //defines tests and non-tests
176 import unit_threaded.asserts;
177 import std.algorithm;
178 import std.array;
179 
180 //helper function for the unittest blocks below
181 private auto addModPrefix(string[] elements, string mod = "unit_threaded.tests.module_with_tests") nothrow {
182     return elements.map!(a => mod ~ "." ~ a).array;
183 }
184 
185 unittest {
186     const expected = addModPrefix([ "FooTest", "BarTest", "Blergh"]);
187     const actual = getTestClasses!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
188     assertEqual(actual, expected);
189 }
190 
191 unittest {
192     static assert(hasTestPrefix!(unit_threaded.tests.module_with_tests, "testFoo"));
193     static assert(!hasTestPrefix!(unit_threaded.tests.module_with_tests, "funcThatShouldShowUpCosOfAttr"));
194 }
195 
196 unittest {
197     const expected = addModPrefix([ "testFoo", "testBar", "funcThatShouldShowUpCosOfAttr" ]);
198     const actual = getTestFunctions!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
199     assertEqual(actual, expected);
200 }
201 
202 
203 unittest {
204     const expected = addModPrefix(["unittest1", "unittest2"]);
205     const actual = getBuiltinTests!(unit_threaded.tests.module_with_tests).map!(a => a.name).array;
206     assertEqual(actual, expected);
207 }