1 /**
2 Copyright: Copyright (c) 2021, Joakim Brännström. All rights reserved.
3 License: MPL-2
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This Source Code Form is subject to the terms of the Mozilla Public License,
7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain
8 one at http://mozilla.org/MPL/2.0/.
9 */
10 module app;
11 
12 import logger = std.experimental.logger;
13 import std.algorithm;
14 import std.array;
15 import std.conv : to;
16 import std.format : format;
17 import std.path : relativePath;
18 import std.stdio : File;
19 
20 import colorlog;
21 import my.path;
22 
23 int main(string[] args) {
24     import std.format : format;
25     static import std.getopt;
26     import std.file : thisExePath;
27     import std.path : baseName;
28 
29     confLogger(VerboseMode.info);
30 
31     bool var;
32     AbsolutePath[] searchDirs;
33     auto relative = AbsolutePath(".");
34     auto output = AbsolutePath("test_metadata.json");
35     try {
36         string[] searchDirsRaw;
37         string relativeRaw;
38         string outputRaw;
39         auto helpInfo = std.getopt.getopt(args, std.getopt.config.required, "d|directory",
40                 "directory to search for googletest test cases", &searchDirsRaw,
41                 "r|relative", "paths are relative this path",
42                 &relativeRaw, "o|output", "write the json output to this file", &outputRaw);
43         searchDirs = searchDirsRaw.map!(a => AbsolutePath(a)).array;
44         if (!relativeRaw.empty)
45             relative = AbsolutePath(relativeRaw);
46         if (!outputRaw.empty)
47             output = AbsolutePath(outputRaw);
48 
49         if (helpInfo.helpWanted) {
50             std.getopt.defaultGetoptPrinter(format!"usage: %s <options>\n"(thisExePath.baseName),
51                     helpInfo.options);
52         }
53     } catch (Exception e) {
54         logger.error(e.msg);
55         return 1;
56     }
57 
58     auto testCases = appender!(TestCase[])();
59 
60     foreach (d; searchDirs) {
61         logger.info("Searching ", d);
62         try {
63             import std.file : dirEntries, SpanMode;
64 
65             foreach (f; dirEntries(d.toString, SpanMode.depth).filter!"a.isFile"
66                     .map!(a => AbsolutePath(a.name))) {
67                 logger.info("Parsing ", f);
68                 auto found = parse(f);
69                 foreach (tc; found)
70                     logger.info("  ", tc.name, " at ", tc.line);
71                 testCases.put(found);
72             }
73         } catch (Exception e) {
74         }
75     }
76 
77     import std.json;
78 
79     auto jrval = appender!(JSONValue[])();
80 
81     foreach (testCase; testCases.data) {
82         JSONValue tc;
83         tc["name"] = testCase.name;
84         JSONValue loc;
85         loc["file"] = relativePath(testCase.file.toString, relative.toString);
86         loc["line"] = testCase.line;
87         tc["location"] = loc;
88 
89         jrval.put(tc);
90     }
91 
92     logger.info("Saving result to ", output);
93     File(output.toString, "w").write(JSONValue(jrval.data).toPrettyString);
94 
95     return 0;
96 }
97 
98 struct TestCase {
99     AbsolutePath file;
100     uint line;
101     string name;
102 }
103 
104 TestCase[] parse(AbsolutePath testPath) {
105     import std.ascii : isWhite;
106     import std..string : startsWith, strip;
107 
108     static TestCase parseTest(string[] lines) {
109         TestCase tc;
110         string def;
111         bool open;
112         int parenthesis;
113 
114         foreach (c; lines.joiner) {
115             if (c == '(') {
116                 open = true;
117                 parenthesis++;
118             } else if (c == ')')
119                 parenthesis--;
120             else if (parenthesis == 1 && !c.isWhite)
121                 def ~= c;
122 
123             if (open && parenthesis == 0)
124                 break;
125         }
126 
127         auto s = split(def, ',');
128         if (s.length == 2 && !s[0].empty && !s[1].empty) {
129             tc.name = format!"%s.%s"(s[0].strip, s[1].strip);
130         }
131 
132         return tc;
133     }
134 
135     static TestCase parseTestParam(string[] lines) {
136         TestCase tc;
137         string def;
138         bool open;
139         int commas;
140 
141         foreach (c; lines.joiner) {
142             if (c == '(')
143                 open = true;
144             else if (c == ',')
145                 commas++;
146             else if (open && !c.isWhite)
147                 def ~= c;
148 
149             if (open && commas == 2)
150                 break;
151         }
152 
153         if (def.empty)
154             return tc;
155 
156         auto s = split(def[0 .. $ - 1], ',');
157         if (s.length == 2 && !s[0].empty && !s[1].empty)
158             tc.name = format!"%s.%s"(s[0].strip, s[1].strip);
159 
160         return tc;
161     }
162 
163     alias Parser = TestCase function(string[]);
164     Parser[string] parsers;
165     parsers["TEST"] = &parseTest;
166     parsers["TEST_F"] = &parseTest;
167     parsers["TEST_P"] = &parseTest;
168     parsers["INSTANTIATE_TEST_SUITE_P"] = &parseTestParam;
169 
170     auto lines = File(testPath.toString).byLineCopy.array;
171 
172     auto testCases = appender!(TestCase[])();
173 
174     foreach (i; 0 .. lines.length) {
175         () {
176             foreach (p; parsers.byKeyValue) {
177                 try {
178                     if (lines[i].startsWith(p.key)) {
179                         auto tc = p.value()(lines[i .. $]);
180                         tc.line = cast(uint) i + 1;
181                         tc.file = testPath;
182                         if (!tc.name.empty)
183                             testCases.put(tc);
184                         return;
185                     }
186                 } catch (Exception e) {
187                     logger.info(e.msg);
188                 }
189             }
190         }();
191     }
192 
193     return testCases.data;
194 }