[test-runner] Fix bug in commandline option parsing
[mono.git] / mono / tests / test-runner.cs
1 //
2 // test-runner.cs
3 //
4 // Author:
5 //   Zoltan Varga (vargaz@gmail.com)
6 //
7 // Copyright (C) 2008 Novell, Inc (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining
10 // a copy of this software and associated documentation files (the
11 // "Software"), to deal in the Software without restriction, including
12 // without limitation the rights to use, copy, modify, merge, publish,
13 // distribute, sublicense, and/or sell copies of the Software, and to
14 // permit persons to whom the Software is furnished to do so, subject to
15 // the following conditions:
16 // 
17 // The above copyright notice and this permission notice shall be
18 // included in all copies or substantial portions of the Software.
19 // 
20 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 //
28 using System;
29 using System.IO;
30 using System.Threading;
31 using System.Diagnostics;
32 using System.Collections.Generic;
33 using System.Globalization;
34 using System.Xml;
35 using System.Text.RegularExpressions;
36
37 //
38 // This is a simple test runner with support for parallel execution
39 //
40
41 public class TestRunner
42 {
43         class ProcessData {
44                 public string test;
45                 public StreamWriter stdout, stderr;
46                 public string stdoutFile, stderrFile;
47         }
48
49         class TestInfo {
50                 public string test, opt_set;
51         }
52
53         public static int Main (String[] args) {
54                 // Defaults
55                 int concurrency = 1;
56                 int timeout = 2 * 60; // in seconds
57                 int expectedExitCode = 0;
58                 string testsuiteName = null;
59
60                 DateTime test_start_time = DateTime.UtcNow;
61
62                 // FIXME: Add support for runtime arguments + env variables
63
64                 string disabled_tests = null;
65                 string runtime = "mono";
66                 var opt_sets = new List<string> ();
67
68                 // Process options
69                 int i = 0;
70                 while (i < args.Length) {
71                         if (args [i].StartsWith ("-")) {
72                                 if (args [i] == "-j") {
73                                         if (i + 1 >= args.Length) {
74                                                 Console.WriteLine ("Missing argument to -j command line option.");
75                                                 return 1;
76                                         }
77                                         if (args [i + 1] == "a")
78                                                 concurrency = Environment.ProcessorCount;
79                                         else
80                                                 concurrency = Int32.Parse (args [i + 1]);
81                                         i += 2;
82                                 } else if (args [i] == "--timeout") {
83                                         if (i + 1 >= args.Length) {
84                                                 Console.WriteLine ("Missing argument to --timeout command line option.");
85                                                 return 1;
86                                         }
87                                         timeout = Int32.Parse (args [i + 1]);
88                                         i += 2;
89                                 } else if (args [i] == "--disabled") {
90                                         if (i + 1 >= args.Length) {
91                                                 Console.WriteLine ("Missing argument to --disabled command line option.");
92                                                 return 1;
93                                         }
94                                         disabled_tests = args [i + 1];
95                                         i += 2;
96                                 } else if (args [i] == "--runtime") {
97                                         if (i + 1 >= args.Length) {
98                                                 Console.WriteLine ("Missing argument to --runtime command line option.");
99                                                 return 1;
100                                         }
101                                         runtime = args [i + 1];
102                                         i += 2;
103                                 } else if (args [i] == "--opt-sets") {
104                                         if (i + 1 >= args.Length) {
105                                                 Console.WriteLine ("Missing argument to --opt-sets command line option.");
106                                                 return 1;
107                                         }
108                                         foreach (var s in args [i + 1].Split ())
109                                                 opt_sets.Add (s);
110                                         i += 2;
111                                 } else if (args [i] == "--expected-exit-code") {
112                                         if (i + 1 >= args.Length) {
113                                                 Console.WriteLine ("Missing argument to --expected-exit-code command line option.");
114                                                 return 1;
115                                         }
116                                         expectedExitCode = Int32.Parse (args [i + 1]);
117                                         i += 2;
118                                 } else if (args [i] == "--testsuite-name") {
119                                         if (i + 1 >= args.Length) {
120                                                 Console.WriteLine ("Missing argument to --testsuite-name command line option.");
121                                                 return 1;
122                                         }
123                                         testsuiteName = args [i + 1];
124                                         i += 2;
125                                 } else {
126                                         Console.WriteLine ("Unknown command line option: '" + args [i] + "'.");
127                                         return 1;
128                                 }
129                         } else {
130                                 break;
131                         }
132                 }
133
134                 if (String.IsNullOrEmpty (testsuiteName)) {
135                         Console.WriteLine ("Missing the required --testsuite-name command line option.");
136                         return 1;
137                 }
138
139                 var disabled = new Dictionary <string, string> ();
140
141                 if (disabled_tests != null) {
142                         foreach (string test in disabled_tests.Split ())
143                                 disabled [test] = test;
144                 }
145
146                 // The remaining arguments are the tests
147                 var tests = new List<string> ();
148                 for (int j = i; j < args.Length; ++j)
149                         if (!disabled.ContainsKey (args [j]))
150                                 tests.Add (args [j]);
151
152                 var passed = new List<ProcessData> ();
153                 var failed = new List<ProcessData> ();
154                 var timedout = new List<ProcessData> ();
155
156                 object monitor = new object ();
157
158                 if (concurrency != 1)
159                         Console.WriteLine ("Running tests: ");
160
161                 var test_info = new Queue<TestInfo> ();
162                 if (opt_sets.Count == 0) {
163                         foreach (string s in tests)
164                                 test_info.Enqueue (new TestInfo { test = s });
165                 } else {
166                         foreach (string opt in opt_sets) {
167                                 foreach (string s in tests)
168                                         test_info.Enqueue (new TestInfo { test = s, opt_set = opt });
169                         }
170                 }               
171
172                 List<Thread> threads = new List<Thread> (concurrency);
173
174                 for (int j = 0; j < concurrency; ++j) {
175                         Thread thread = new Thread (() => {
176                                 while (true) {
177                                         TestInfo ti;
178
179                                         lock (monitor) {
180                                                 if (test_info.Count == 0)
181                                                         break;
182                                                 ti = test_info.Dequeue ();
183                                         }
184
185                                         string test = ti.test;
186                                         string opt_set = ti.opt_set;
187
188                                         if (concurrency == 1)
189                                                 Console.Write ("Testing " + test + "... ");
190
191                                         /* Spawn a new process */
192                                         string process_args;
193                                         if (opt_set == null)
194                                                 process_args = test;
195                                         else
196                                                 process_args = "-O=" + opt_set + " " + test;
197                                         ProcessStartInfo info = new ProcessStartInfo (runtime, process_args);
198                                         info.UseShellExecute = false;
199                                         info.RedirectStandardOutput = true;
200                                         info.RedirectStandardError = true;
201                                         Process p = new Process ();
202                                         p.StartInfo = info;
203
204                                         ProcessData data = new ProcessData ();
205                                         data.test = test;
206
207                                         string log_prefix = "";
208                                         if (opt_set != null)
209                                                 log_prefix = "." + opt_set.Replace ("-", "no").Replace (",", "_");
210
211                                         data.stdoutFile = test + log_prefix + ".stdout";
212                                         data.stdout = new StreamWriter (new FileStream (data.stdoutFile, FileMode.Create));
213
214                                         data.stderrFile = test + log_prefix + ".stderr";
215                                         data.stderr = new StreamWriter (new FileStream (data.stderrFile, FileMode.Create));
216
217                                         p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
218                                                 if (e.Data != null) {
219                                                         data.stdout.WriteLine (e.Data);
220                                                 } else {
221                                                         data.stdout.Flush ();
222                                                         data.stdout.Close ();
223                                                 }
224                                         };
225
226                                         p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
227                                                 if (e.Data != null) {
228                                                         data.stderr.WriteLine (e.Data);
229                                                 } else {
230                                                         data.stderr.Flush ();
231                                                         data.stderr.Close ();
232                                                 }
233                                         };
234
235                                         p.Start ();
236
237                                         p.BeginOutputReadLine ();
238                                         p.BeginErrorReadLine ();
239
240                                         if (!p.WaitForExit (timeout * 1000)) {
241                                                 lock (monitor) {
242                                                         timedout.Add (data);
243                                                 }
244
245                                                 if (concurrency == 1)
246                                                         Console.WriteLine ("timed out.");
247                                                 else
248                                                         Console.Write (".");
249
250                                                 p.Kill ();
251                                         } else if (p.ExitCode != expectedExitCode) {
252                                                 lock (monitor) {
253                                                         failed.Add (data);
254                                                 }
255
256                                                 if (concurrency == 1)
257                                                         Console.WriteLine ("failed.");
258                                                 else
259                                                         Console.Write (".");
260                                         } else {
261                                                 lock (monitor) {
262                                                         passed.Add (data);
263                                                 }
264
265                                                 if (concurrency == 1)
266                                                         Console.WriteLine ("passed.");
267                                                 else
268                                                         Console.Write (".");
269                                         }
270
271                                         p.Close ();
272                                 }
273                         });
274
275                         thread.Start ();
276
277                         threads.Add (thread);
278                 }
279
280                 for (int j = 0; j < threads.Count; ++j)
281                         threads [j].Join ();
282
283                 int npassed = passed.Count;
284                 int nfailed = failed.Count;
285                 int ntimedout = timedout.Count;
286
287                 TimeSpan test_time = DateTime.UtcNow - test_start_time;
288                 XmlWriterSettings xmlWriterSettings = new XmlWriterSettings ();
289                 xmlWriterSettings.NewLineOnAttributes = true;
290                 xmlWriterSettings.Indent = true;
291                 using (XmlWriter writer = XmlWriter.Create (String.Format ("TestResults_{0}.xml", testsuiteName), xmlWriterSettings)) {
292                         // <?xml version="1.0" encoding="utf-8" standalone="no"?>
293                         writer.WriteStartDocument ();
294                         // <!--This file represents the results of running a test suite-->
295                         writer.WriteComment ("This file represents the results of running a test suite");
296                         // <test-results name="/home/charlie/Dev/NUnit/nunit-2.5/work/src/bin/Debug/tests/mock-assembly.dll" total="21" errors="1" failures="1" not-run="7" inconclusive="1" ignored="4" skipped="0" invalid="3" date="2010-10-18" time="13:23:35">
297                         writer.WriteStartElement ("test-results");
298                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
299                         writer.WriteAttributeString ("total", (npassed + nfailed + ntimedout).ToString());
300                         writer.WriteAttributeString ("failures", (nfailed + ntimedout).ToString());
301                         writer.WriteAttributeString ("not-run", "0");
302                         writer.WriteAttributeString ("date", DateTime.Now.ToString ("yyyy-MM-dd"));
303                         writer.WriteAttributeString ("time", DateTime.Now.ToString ("HH:mm:ss"));
304                         //   <environment nunit-version="2.4.8.0" clr-version="4.0.30319.17020" os-version="Unix 3.13.0.45" platform="Unix" cwd="/home/directhex/Projects/mono/mcs/class/corlib" machine-name="marceline" user="directhex" user-domain="marceline" />
305                         writer.WriteStartElement ("environment");
306                         writer.WriteAttributeString ("nunit-version", "2.4.8.0" );
307                         writer.WriteAttributeString ("clr-version", Environment.Version.ToString() );
308                         writer.WriteAttributeString ("os-version", Environment.OSVersion.ToString() );
309                         writer.WriteAttributeString ("platform", Environment.OSVersion.Platform.ToString() );
310                         writer.WriteAttributeString ("cwd", Environment.CurrentDirectory );
311                         writer.WriteAttributeString ("machine-name", Environment.MachineName );
312                         writer.WriteAttributeString ("user", Environment.UserName );
313                         writer.WriteAttributeString ("user-domain", Environment.UserDomainName );
314                         writer.WriteEndElement ();
315                         //   <culture-info current-culture="en-GB" current-uiculture="en-GB" />
316                         writer.WriteStartElement ("culture-info");
317                         writer.WriteAttributeString ("current-culture", CultureInfo.CurrentCulture.Name );
318                         writer.WriteAttributeString ("current-uiculture", CultureInfo.CurrentUICulture.Name );
319                         writer.WriteEndElement ();
320                         //   <test-suite name="corlib_test_net_4_5.dll" success="True" time="114.318" asserts="0">
321                         writer.WriteStartElement ("test-suite");
322                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
323                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
324                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
325                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
326                         //     <results>
327                         writer.WriteStartElement ("results");
328                         //       <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
329                         writer.WriteStartElement ("test-suite");
330                         writer.WriteAttributeString ("name","MonoTests");
331                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
332                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
333                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
334                         //         <results>
335                         writer.WriteStartElement ("results");
336                         //           <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
337                         writer.WriteStartElement ("test-suite");
338                         writer.WriteAttributeString ("name", testsuiteName);
339                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
340                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
341                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
342                         //             <results>
343                         writer.WriteStartElement ("results");
344                         // Dump all passing tests first
345                         foreach (ProcessData pd in passed) {
346                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
347                                 writer.WriteStartElement ("test-case");
348                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
349                                 writer.WriteAttributeString ("executed", "True");
350                                 writer.WriteAttributeString ("success", "True");
351                                 writer.WriteAttributeString ("time", "0");
352                                 writer.WriteAttributeString ("asserts", "0");
353                                 writer.WriteEndElement ();
354                         }
355                         // Now dump all failing tests
356                         foreach (ProcessData pd in failed) {
357                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
358                                 writer.WriteStartElement ("test-case");
359                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
360                                 writer.WriteAttributeString ("executed", "True");
361                                 writer.WriteAttributeString ("success", "False");
362                                 writer.WriteAttributeString ("time", "0");
363                                 writer.WriteAttributeString ("asserts", "1");
364                                 writer.WriteStartElement ("failure");
365                                 writer.WriteStartElement ("message");
366                                 writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
367                                 writer.WriteEndElement ();
368                                 writer.WriteStartElement ("stack-trace");
369                                 writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
370                                 writer.WriteEndElement ();
371                                 writer.WriteEndElement ();
372                                 writer.WriteEndElement ();
373                         }
374                         // Then dump all timing out tests
375                         foreach (ProcessData pd in timedout) {
376                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
377                                 writer.WriteStartElement ("test-case");
378                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}_timedout", testsuiteName, pd.test));
379                                 writer.WriteAttributeString ("executed", "True");
380                                 writer.WriteAttributeString ("success", "False");
381                                 writer.WriteAttributeString ("time", "0");
382                                 writer.WriteAttributeString ("asserts", "1");
383                                 writer.WriteStartElement ("failure");
384                                 writer.WriteStartElement ("message");
385                                 writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
386                                 writer.WriteEndElement ();
387                                 writer.WriteStartElement ("stack-trace");
388                                 writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
389                                 writer.WriteEndElement ();
390                                 writer.WriteEndElement ();
391                                 writer.WriteEndElement ();
392                         }
393                         //             </results>
394                         writer.WriteEndElement ();
395                         //           </test-suite>
396                         writer.WriteEndElement ();
397                         //         </results>
398                         writer.WriteEndElement ();
399                         //       </test-suite>
400                         writer.WriteEndElement ();
401                         //     </results>
402                         writer.WriteEndElement ();
403                         //   </test-suite>
404                         writer.WriteEndElement ();
405                         // </test-results>
406                         writer.WriteEndElement ();
407                         writer.WriteEndDocument ();
408                 }
409
410                 Console.WriteLine ();
411                 Console.WriteLine ("{0,4} test(s) passed", npassed);
412                 Console.WriteLine ("{0,4} test(s) failed", nfailed);
413                 Console.WriteLine ("{0,4} test(s) timed out", ntimedout);
414
415                 if (nfailed > 0) {
416                         Console.WriteLine ();
417                         Console.WriteLine ("Failed test(s):");
418                         foreach (ProcessData pd in failed) {
419                                 Console.WriteLine ();
420                                 Console.WriteLine (pd.test);
421                                 DumpFile (pd.stdoutFile);
422                                 DumpFile (pd.stderrFile);
423                         }
424                 }
425
426                 if (ntimedout > 0) {
427                         Console.WriteLine ();
428                         Console.WriteLine ("Timed out test(s):");
429                         foreach (ProcessData pd in timedout) {
430                                 Console.WriteLine ();
431                                 Console.WriteLine (pd.test);
432                                 DumpFile (pd.stdoutFile);
433                                 DumpFile (pd.stderrFile);
434                         }
435                 }
436
437                 return (ntimedout == 0 && nfailed == 0) ? 0 : 1;
438         }
439         
440         static void DumpFile (string filename) {
441                 if (File.Exists (filename)) {
442                         Console.WriteLine ("=============== {0} ===============", filename);
443                         Console.WriteLine (File.ReadAllText (filename));
444                         Console.WriteLine ("=============== EOF ===============");
445                 }
446         }
447
448         static string DumpPseudoTrace (string filename) {
449                 if (File.Exists (filename))
450                         return FilterInvalidXmlChars (File.ReadAllText (filename));
451                 else
452                         return string.Empty;
453         }
454
455         static string FilterInvalidXmlChars (string text) {
456                 // Spec at http://www.w3.org/TR/2008/REC-xml-20081126/#charsets says only the following chars are valid in XML:
457                 // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]      /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
458                 return Regex.Replace (text, @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]", "");
459         }
460 }