1 /**
2    Code to parse the output from `dub describe` and generate the main
3    test file automatically.
4  */
5 module unit_threaded.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, JSON_TYPE;
94     enforce!JSONException(json.type == JSON_TYPE.TRUE || json.type == JSON_TYPE.FALSE,
95                           "JSONValue is not a boolean");
96     return json.type == JSON_TYPE.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) {
115     import std.json: JSONException;
116     import std.conv: text;
117     import std.algorithm: joiner, map, copy;
118     import std.stdio: writeln;
119     import std.exception: enforce;
120     import std.process: pipeProcess, Redirect, wait;
121     import std.array: join, appender;
122 
123     if(verbose)
124         writeln("Running dub describe");
125 
126     immutable args = ["dub", "describe", "-c", "unittest"];
127     auto pipes = pipeProcess(args, Redirect.stdout | Redirect.stderr);
128     scope(exit) wait(pipes.pid); // avoid zombies in all cases
129     string stdoutStr;
130     string stderrStr;
131     enum chunkSize = 4096;
132     pipes.stdout.byChunk(chunkSize).joiner
133         .map!"cast(immutable char)a".copy(appender(&stdoutStr));
134     pipes.stderr.byChunk(chunkSize).joiner
135         .map!"cast(immutable char)a".copy(appender(&stderrStr));
136     auto status = wait(pipes.pid);
137     auto allOutput = "stdout:\n" ~ stdoutStr ~ "\nstderr:\n" ~ stderrStr;
138 
139     enforce(status == 0, text("Could not execute ", args.join(" "),
140                 ":\n", allOutput));
141     try {
142         return getDubInfo(stdoutStr);
143     } catch(JSONException e) {
144         throw new Exception(text("Could not parse the output of dub describe:\n", allOutput, "\n", e.toString));
145     }
146 }
147 
148 bool isDubProject() {
149     import std.file;
150     return "dub.sdl".exists || "dub.json".exists || "package.json".exists;
151 }
152 
153 
154 // set import paths from dub information
155 void dubify(ref from!"unit_threaded.runtime".Options options) {
156 
157     import std.path: buildPath;
158     import std.algorithm: map, reduce;
159     import std.array: array;
160 
161     if(!isDubProject) return;
162 
163     auto dubInfo = getDubInfo(options.verbose);
164     options.includes = dubInfo.packages.
165         map!(a => a.importPaths.map!(b => buildPath(a.path, b)).array).
166         reduce!((a, b) => a ~ b).array;
167     options.files = dubInfo.packages[0].files;
168 }