1 /*
2 *             Copyright Lodovico Giaretta 2016 - .
3 *  Distributed under the Boost Software License, Version 1.0.
4 *      (See accompanying file LICENSE_1_0.txt or copy at
5 *            http://www.boost.org/LICENSE_1_0.txt)
6 */
7 
8 module test;
9 
10 import std.experimental.xml;
11 
12 import std.encoding: transcode, Latin1String;
13 import std.file: read, readText;
14 import std.functional: toDelegate;
15 import std.path;
16 import std.stdio: write, writeln;
17 import std.utf: UTFException;
18 
19 auto indexes = 
20 [
21     "tests/sun/sun-valid.xml",
22     "tests/sun/sun-error.xml",
23     "tests/sun/sun-invalid.xml",
24     "tests/sun/sun-not-wf.xml",
25     "tests/xmltest/xmltest.xml",
26     "tests/oasis/oasis.xml",
27     "tests/ibm/ibm_oasis_invalid.xml",
28     "tests/ibm/ibm_oasis_not-wf.xml",
29     "tests/ibm/ibm_oasis_valid.xml",
30     "tests/ibm/xml-1.1/ibm_invalid.xml",
31     "tests/ibm/xml-1.1/ibm_not-wf.xml",
32     "tests/ibm/xml-1.1/ibm_valid.xml",
33     "tests/eduni/errata-2e/errata2e.xml",
34     "tests/eduni/xml-1.1/xml11.xml",
35     "tests/eduni/namespaces/1.0/rmt-ns10.xml",
36     "tests/eduni/namespaces/1.1/rmt-ns11.xml",
37     "tests/eduni/errata-3e/errata3e.xml",
38     "tests/eduni/namespaces/errata-1e/errata1e.xml",
39     "tests/eduni/errata-4e/errata4e.xml",
40     "tests/eduni/misc/ht-bh.xml"
41 ];
42 
43 struct Results
44 {
45     int[string] totals;
46     int[string] wrong;
47     
48     static Results opCall()
49     {
50         Results result;
51         result.totals = ["valid": 0, "linted": 0, "invalid": 0, "not-wf": 0, "error": 0, "skipped": 0];
52         result.wrong = ["valid": 0, "linted":0, "invalid": 0, "not-wf": 0, "error": 0];
53         return result;
54     }
55     
56     void opOpAssign(string op)(Results other)
57     {
58         static if (op == "+")
59         {
60             foreach (key, val; other.totals)
61                 totals[key] += val;
62             foreach (key, val; other.wrong)
63                 wrong[key] += val;
64         }
65         else
66         {
67             static assert(0);
68         }
69     }
70 }
71 
72 void writeIndent(int depth)
73 {
74     for (int i = 0; i < depth; i++)
75         write("\t");
76 }
77 
78 void printResults(Results results, int depth)
79 {
80     writeIndent(depth);
81     writeln("== RESULTS ==");
82     writeIndent(depth);
83     writeln(results.wrong["valid"], " valid inputs rejected out of ", results.totals["valid"], " total.");
84     writeIndent(depth);
85     writeln(results.wrong["linted"], " wrong outputs out of ", results.totals["linted"], " total written files.");
86     writeIndent(depth);
87     writeln(results.wrong["invalid"], " invalid inputs accepted out of ", results.totals["invalid"], " total.");
88     writeIndent(depth);
89     writeln(results.wrong["not-wf"], " ill-formed inputs accepted out of ", results.totals["not-wf"], " total.");
90     writeIndent(depth);
91     writeln(results.wrong["error"], " erroneous inputs accepted out of ", results.totals["error"], " total.");
92     writeIndent(depth);
93     writeln(results.totals["skipped"], " inputs skipped because of unsupported features.");
94 }
95 
96 Results handleTestcases(T)(string directory, ref T cursor, int depth)
97 {
98     auto results = Results();
99     do
100     {
101         if (cursor.name == "TESTCASES")
102         {
103             writeIndent(depth);
104             write("TESTCASES");
105             foreach (att; cursor.attributes)
106                 if (att.name == "PROFILE")
107                     write(" -- ", att.value);
108             writeln();
109             
110             if (cursor.enter())
111             {
112                 results += handleTestcases(directory, cursor, depth + 1);
113                 cursor.exit();
114             }
115         }
116         else if (cursor.name == "TEST")
117         {
118             results += handleTest(directory, cursor, depth);
119         }
120     }
121     while (cursor.next());
122     printResults(results, depth);
123     writeln();
124     return results;
125 }
126 
127 Results handleTest(T)(string directory, ref T cursor, int depth)
128 {
129     auto result = Results();
130     
131     string file, kind;
132     foreach (att; cursor.attributes)
133         if (att.name == "ENTITIES" && att.value != "none")
134         {
135             result.totals["skipped"]++;
136             return result;
137         }
138         else if (att.name == "TYPE" && att.value in result.totals)
139             kind = att.value;
140         else if (att.name == "URI")
141             file = att.value;
142     
143     result.totals[kind]++;
144     
145     bool passed = true, linted = false, linted_ok = false;
146     try
147     {
148         linted_ok = parseFile(directory ~ dirSeparator ~ file, linted);
149     }
150     catch (MyException err)
151     {
152         passed = false;
153     }
154     if (passed && kind != "valid")
155     {
156         writeIndent(depth);
157         write("FAILED: accepted ");
158         result.wrong[kind]++;
159         switch (kind)
160         {
161             case "invalid":
162                 write("invalid ");
163                 break;
164             case "not-wf":
165                 write("ill-formed ");
166                 break;
167             case "error":
168                 write("erroneous ");
169                 break;
170             default:
171                 assert(0);
172         }
173         writeln("file ", file);
174     }
175     else if (!passed && kind == "valid")
176     {
177         writeIndent(depth);
178         result.wrong["valid"]++;
179         writeln("FAILED: rejected valid file ", file);
180     }
181     else
182     {
183         writeIndent(depth);
184         writeln("OK: ", file);
185         if (passed && linted)
186         {
187             result.totals["linted"]++;
188             if (!linted_ok)
189             {
190                 result.wrong["linted"]++;
191                 writeIndent(depth + 1);
192                 writeln("[WRONG DIFF]");
193             }
194         }
195     }
196     
197     return result;
198 }
199 
200 class MyException: Exception
201 {
202     this(string msg)
203     {
204         super(msg);
205     }
206 }
207 
208 // callback used to ignore missing xml declaration, while throwing on invalid attributes
209 void uselessCallback(CursorError err)
210 {
211     if (err != CursorError.missingXMLDeclaration)
212         throw new MyException("AAAAHHHHH");
213 }
214 
215 /++
216 + Most tests are currently not working for the following reasons:
217 + - We don't have any validation, so we accept all files that seem well formed;
218 +/
219 void main()
220 {
221     auto cursor = 
222          chooseLexer!string
223         .parser
224         .cursor(&uselessCallback); // If an index is not well-formed, just tell us but continue parsing
225     
226     auto results = Results();
227     foreach (i, index; indexes)
228     {
229         writeln(i, " -- ", index);
230         
231         cursor.setSource(readText(index));
232         cursor.enter();
233         
234         results += handleTestcases(dirName(index), cursor, 1);
235     }
236     
237     printResults(results, 0);
238     writeln();
239 }
240 
241 bool parseFile(string filename, ref bool lint)
242 {
243     void inspectOneLevel(T)(ref T cursor)
244     {
245         do
246         {
247             if (cursor.enter)
248             {
249                 inspectOneLevel(cursor);
250                 cursor.exit();
251             }
252         }
253         while (cursor.next());
254     }
255 
256     string text;
257     try
258     {
259         text = readText(filename);
260     }
261     catch (UTFException)
262     {
263         try
264         {
265             auto raw = read(filename);
266             auto bytes = cast(ubyte[])raw;
267         
268             if(bytes.length > 1 && bytes[0] == 0xFF && bytes[1] == 0xFE)
269             {
270                 auto shorts = cast(ushort[])raw;
271                 transcode(cast(wstring)(shorts[1..$]), text);
272             }
273             else
274                 transcode(cast(Latin1String)raw, text);
275         }
276         catch(Throwable)
277         {
278             throw new MyException("AAAAHHHHH");
279         }
280     }
281     
282     auto cursor = 
283          text
284         .parser(() { throw new MyException("AAAAHHHHH"); })
285         .cursor(&uselessCallback); // lots of tests do not have an xml declaration
286     
287     cursor.setSource(text);
288     
289     lint = false;
290     foreach (attr; cursor.attributes)
291         if (attr.name == "version" && attr.value == "1.0")
292             lint = true;
293     
294     inspectOneLevel(cursor);
295     
296     if (lint)
297     {
298         import std.process, std.stdio, std.array;
299         import std.experimental.xml.writer;
300     
301         lint = false;
302         bool result = false;
303         auto xmllint = executeShell("xmllint --pretty 2 --c14n11 " ~ filename ~ " > linted_input.xml");
304         if (xmllint.status == 0)
305         {
306             {
307                 cursor.setSource(text);
308                 auto file = File("output.xml", "w");
309                 auto ltw = file.lockingTextWriter;
310                 auto writer = Writer!(string, typeof(ltw))();
311                 
312                 writer.setSink(ltw);
313                 writer.writeCursor(cursor);
314             }
315             
316             xmllint = executeShell("xmllint --pretty 2 --c14n11 output.xml > linted_output.xml");
317             if (xmllint.status == 0)
318             {
319                 lint = true;
320                 auto diff = executeShell("diff linted_input.xml linted_output.xml");
321                 result = diff.status == 0;
322             }
323         }
324         executeShell("rm -f linted_output.xml linted_input.xml output.xml");
325         return result;
326     }
327     return false;
328 }