1 module tests.support.DStepRunner;
2 
3 import std.process : execute;
4 import std.traits : ReturnType;
5 
6 version (linux)
7     version = OptionalGNUStep;
8 
9 version (Windows)
10     version = OptionalGNUStep;
11 
12 private alias TestRunDStepResult = ReturnType!execute;
13 
14 auto testRunDStep(
15     string[] sourcePaths,
16     string[] arguments,
17     string[]* outputContents = null,
18     string* command = null,
19     string file = __FILE__,
20     size_t line = __LINE__)
21 {
22     import core.exception : AssertError;
23 
24     import std.algorithm : canFind, map;
25     import std.file : exists, isFile, readText, rmdirRecurse;
26     import std.path : buildPath;
27     import std.range : join;
28     import std.array : empty;
29 
30     import dstep.driver.Util : makeDefaultOutputFile;
31 
32     version (OptionalGNUStep)
33     {
34         if (arguments.canFind("-ObjC") || arguments.canFind("--objective-c"))
35         {
36             auto extra = findExtraGNUStepPaths(file, line);
37 
38             if (extra.empty)
39                 throw new NoGNUStepException();
40             else
41                 arguments ~= extra;
42         }
43     }
44 
45     foreach (sourcePath; sourcePaths)
46         assertFileExists(sourcePath, file, line);
47 
48     string outputDir = namedTempDir("dstepUnitTest");
49     scope(exit) rmdirRecurse(outputDir);
50 
51     string[] outputPaths;
52 
53     if (sourcePaths.length == 1)
54     {
55         outputPaths ~= buildPath(outputDir,
56             makeDefaultOutputFile(sourcePaths[0], false));
57     }
58     else
59     {
60         foreach (sourcePath; sourcePaths)
61             outputPaths ~= buildPath(outputDir,
62                 makeDefaultOutputFile(sourcePath, false));
63     }
64 
65     auto localCommand = ["./bin/dstep"] ~ sourcePaths ~ arguments;
66 
67     if (outputPaths.length == 1)
68         localCommand ~= ["-o", outputPaths[0]];
69     else
70         localCommand ~= ["-o", outputDir];
71 
72     if (command)
73         *command = join(localCommand, " ");
74 
75     auto result = execute(localCommand);
76 
77     if (outputContents)
78         outputContents.length = outputPaths.length;
79 
80     foreach (i, outputPath; outputPaths)
81     {
82         if (!exists(outputPath) || !isFile(outputPath))
83             throw new NoOutputFile(result, outputPath);
84 
85         if (outputContents)
86             (*outputContents)[i] = readText(outputPath);
87     }
88 
89     return result;
90 }
91 
92 class NoOutputFile : object.Exception
93 {
94     TestRunDStepResult result;
95     string path;
96 
97     this (TestRunDStepResult result, string path, string file = __FILE__, size_t line = __LINE__)
98     {
99         super(path, file, line);
100         this.result = result;
101         this.path = path;
102     }
103 }
104 
105 class NoGNUStepException : object.Exception
106 {
107     this (string file = __FILE__, size_t line = __LINE__)
108     {
109         super("Cannot find GNUStep.", file, line);
110     }
111 }
112 
113 private:
114 
115 class NamedTempDirException : object.Exception
116 {
117     import std.format : format;
118 
119     immutable string path;
120 
121     this (string path, string file = __FILE__, size_t line = __LINE__)
122     {
123         this.path = path;
124 
125         super(
126             format("Cannot create temporary directory \"%s\".", path),
127             file,
128             line
129         );
130     }
131 }
132 
133 void assertFileExists(
134     string expected,
135     string file = __FILE__,
136     size_t line = __LINE__)
137 {
138     import core.exception : AssertError;
139 
140     import std.format : format;
141 
142     import tests.support.Util : fileExists;
143 
144     if (!fileExists(expected))
145     {
146         auto message = format("File %s doesn't exist.", expected);
147         throw new AssertError(message, file, line);
148     }
149 }
150 
151 version (Posix)
152     import core.sys.posix.stdlib : mkdtemp;
153 else
154 {
155     import core.sys.windows.objbase : CoCreateGuid;
156     import core.sys.windows.basetyps : GUID;
157 }
158 
159 string namedTempDir(string prefix)
160 {
161     import std.file;
162     import std.path;
163     import std.format;
164 
165     version (Posix)
166     {
167         static void randstr (char[] slice)
168         {
169             import std.random;
170 
171             foreach (i; 0 .. slice.length)
172                 slice[i] = uniform!("[]")('A', 'Z');
173         }
174 
175         string name = format("%sXXXXXXXXXXXXXXXX\0", prefix);
176         char[] path = buildPath(tempDir(), name).dup;
177         const size_t termAnd6XSize = 7;
178 
179         immutable size_t begin = path.length - name.length + prefix.length;
180 
181         randstr(path[begin .. $ - termAnd6XSize]);
182 
183         char* result = mkdtemp(path.ptr);
184 
185         path = path[0..$-1];
186 
187         if (result == null)
188             throw new NamedTempDirException(path.idup);
189 
190         return path.idup;
191     }
192     else
193     {
194         static string createGUID()
195         {
196             static char toHex(uint x)
197             {
198                 if (x < 10)
199                     return cast(char) ('0' + x);
200                 else
201                     return cast(char) ('A' + x - 10);
202             }
203 
204             GUID guid;
205             CoCreateGuid(&guid);
206 
207             ubyte* data = cast(ubyte*)&guid;
208             char[32] result;
209 
210             foreach (i; 0 .. 16)
211             {
212                 result[i * 2 + 0] = toHex(data[i] & 0x0fu);
213                 result[i * 2 + 1] = toHex(data[i] >> 16);
214             }
215 
216             return result.idup;
217         }
218 
219         string name = prefix ~ createGUID();
220         string path = buildPath(tempDir(), name);
221 
222         try
223             mkdirRecurse(path);
224         catch (FileException)
225             throw new NamedTempDirException(path);
226 
227         return path;
228     }
229 }
230 
231 string[] findExtraGNUStepPaths(string file, size_t line)
232 {
233     import std.stdio : stderr;
234     import std.format : format;
235 
236     auto gnuStepPath = findGNUStepIncludePath();
237 
238     if (gnuStepPath == null)
239     {
240         auto message = "Unable to check the assertion. GNUstep couldn't be found.";
241         stderr.writeln(format("Warning@%s(%d): %s", file, line, message));
242         return [];
243     }
244 
245     auto ccIncludePaths = findCcIncludePaths();
246 
247     if (ccIncludePaths == null)
248     {
249         auto message = "Unable to check the assertion. cc include paths couldn't be found.";
250         stderr.writeln(format("Warning@%s(%d): %s", file, line, message));
251         return [];
252     }
253 
254     return ccIncludePaths ~ gnuStepPath;
255 }
256 
257 string findGNUStepIncludePath()
258 {
259     import std.file : isDir, exists;
260     import std.format : format;
261 
262     string path = "/usr/include/GNUstep";
263 
264     if (exists(path) && isDir(path))
265         return format("-I%s", path);
266     else
267         return null;
268 }
269 
270 string[] findCcIncludePaths()
271 {
272     import std.process : executeShell;
273     auto result = executeShell("cc -E -v - < /dev/null");
274 
275     if (result.status == 0)
276         return extractIncludePaths(result.output);
277     else
278         return null;
279 }
280 
281 string[] extractIncludePaths(string output)
282 {
283     import std.algorithm.searching;
284     import std.algorithm.iteration;
285     import std.array : array;
286     import std..string;
287 
288     string start = "#include <...> search starts here:";
289     string stop = "End of search list.";
290 
291     auto paths = output.findSplitAfter(start)[1]
292         .findSplitBefore(stop)[0].strip();
293     auto args = map!(a => format("-I%s", a.strip()))(paths.splitLines());
294     return paths.empty ? null : args.array;
295 }