1 /**
2    Code to parse the output from `dub describe` and generate the main
3    test file automatically.
4  */
5 module unit_threaded.runtime.dub;
6 
7 import unit_threaded.from;
8 
9 
10 struct DubPackage {
11     string name;
12     string path;
13     string mainSourceFile;
14     string targetFileName;
15     string[] flags;
16     string[] importPaths;
17     string[] stringImportPaths;
18     string[] files;
19     string targetType;
20     string[] versions;
21     string[] dependencies;
22     string[] libs;
23     bool active;
24 }
25 
26 struct DubInfo {
27     DubPackage[] packages;
28 }
29 
30 DubInfo getDubInfo(string jsonString) @trusted {
31     import std.json: parseJSON;
32     import std.algorithm: map, filter;
33     import std.array: array;
34 
35     auto json = parseJSON(jsonString);
36     auto packages = json.byKey("packages").array;
37     return DubInfo(packages.
38                    map!(a => DubPackage(a.byKey("name").str,
39                                         a.byKey("path").str,
40                                         a.getOptional("mainSourceFile"),
41                                         a.getOptional("targetFileName"),
42                                         a.byKey("dflags").jsonValueToStrings,
43                                         a.byKey("importPaths").jsonValueToStrings,
44                                         a.byKey("stringImportPaths").jsonValueToStrings,
45                                         a.byKey("files").jsonValueToFiles,
46                                         a.getOptional("targetType"),
47                                         a.getOptionalList("versions"),
48                                         a.getOptionalList("dependencies"),
49                                         a.getOptionalList("libs"),
50                                         a.byOptionalKey("active", true), //true for backwards compatibility
51                             )).
52                    filter!(a => a.active).
53                    array);
54 }
55 
56 private string[] jsonValueToFiles(from!"std.json".JSONValue files) @trusted {
57     import std.algorithm: map, filter;
58     import std.array: array;
59 
60     return files.array.
61         filter!(a => ("type" in a && a.byKey("type").str == "source") ||
62                      ("role" in a && a.byKey("role").str == "source") ||
63                      ("type" !in a && "role" !in a)).
64         map!(a => a.byKey("path").str).
65         array;
66 }
67 
68 private string[] jsonValueToStrings(from!"std.json".JSONValue json) @trusted {
69     import std.algorithm: map, filter;
70     import std.array: array;
71 
72     return json.array.map!(a => a.str).array;
73 }
74 
75 
76 private auto byKey(from!"std.json".JSONValue json, in string key) @trusted {
77     import std.json: JSONException;
78     if (auto p = key in json.object)
79         return *p;
80     else throw new JSONException("\"" ~ key ~ "\" not found");
81 }
82 
83 private auto byOptionalKey(from!"std.json".JSONValue json, in string key, bool def) {
84     if (auto p = key in json.object)
85         return (*p).boolean;
86     else
87         return def;
88 }
89 
90 //std.json has no conversion to bool
91 private bool boolean(from!"std.json".JSONValue json) @trusted {
92     import std.exception: enforce;
93     import std.json: JSONException, JSONType;
94     enforce!JSONException(json.type == JSONType.true_ || json.type == JSONType.false_,
95                           "JSONValue is not a boolean");
96     return json.type == JSONType.true_;
97 }
98 
99 private string getOptional(from!"std.json".JSONValue json, in string key) @trusted {
100     if (auto p = key in json.object)
101         return p.str;
102     else
103         return "";
104 }
105 
106 private string[] getOptionalList(from!"std.json".JSONValue json, in string key) @trusted {
107     if (auto p = key in json.object)
108         return (*p).jsonValueToStrings;
109     else
110         return [];
111 }
112 
113 
114 DubInfo getDubInfo(in bool verbose, in string dubBinary) {
115     import std.json: JSONException;
116     import std.conv: text;
117     import std.algorithm: joiner, map, copy;
118     import std.range: empty;
119     import std.stdio: writeln;
120     import std.exception: enforce;
121     import std.process: environment, pipeProcess, Redirect, wait;
122     import std.array: join, appender;
123 
124     if(verbose)
125         writeln("Running dub describe");
126 
127     const args =
128         [dubBinary, "describe", "-c", "unittest"] ~
129         ("DC" in environment ? ["--compiler", environment["DC"]] : null);
130     auto pipes = pipeProcess(args, Redirect.stdout | Redirect.stderr);
131     scope(exit) wait(pipes.pid); // avoid zombies in all cases
132     string stdoutStr;
133     string stderrStr;
134     enum chunkSize = 4096;
135     pipes.stdout.byChunk(chunkSize).joiner
136         .map!"cast(immutable char)a".copy(appender(&stdoutStr));
137     pipes.stderr.byChunk(chunkSize).joiner
138         .map!"cast(immutable char)a".copy(appender(&stderrStr));
139     auto status = wait(pipes.pid);
140     auto allOutput = "stdout:\n" ~ stdoutStr ~ "\nstderr:\n" ~ stderrStr;
141 
142     enforce(status == 0, text("Could not execute ", args.join(" "),
143                 ":\n", allOutput));
144     try {
145         return getDubInfo(stdoutStr);
146     } catch(JSONException e) {
147         throw new Exception(text("Could not parse the output of dub describe:\n", allOutput, "\n", e.toString));
148     }
149 }
150 
151 bool isDubProject() {
152     import std.file;
153     return "dub.sdl".exists || "dub.json".exists || "package.json".exists;
154 }
155 
156 
157 // set import paths from dub information
158 void dubify(ref from!"unit_threaded.runtime.runtime".Options options) {
159 
160     import std.path: buildPath;
161     import std.algorithm: map, reduce;
162     import std.array: array;
163 
164     if(!isDubProject) return;
165 
166     auto dubInfo = getDubInfo(options.verbose, options.dubBinary);
167     options.includes = dubInfo.packages.
168         map!(a => a.importPaths.map!(b => buildPath(a.path, b)).array).
169         reduce!((a, b) => a ~ b).array;
170     options.files = dubInfo.packages[0].files;
171 }