Merge pull request #2394 from Mailaender/patch-1
[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 using Mono.Unix.Native;
37
38 //
39 // This is a simple test runner with support for parallel execution
40 //
41
42 public class TestRunner
43 {
44         const string TEST_TIME_FORMAT = "mm\\:ss\\.fff";
45         const string ENV_TIMEOUT = "TEST_DRIVER_TIMEOUT_SEC";
46
47         class ProcessData {
48                 public string test;
49                 public StreamWriter stdout, stderr;
50                 public string stdoutFile, stderrFile;
51         }
52
53         class TestInfo {
54                 public string test, opt_set;
55         }
56
57         public static int Main (String[] args) {
58                 // Defaults
59                 int concurrency = 1;
60                 int timeout = 2 * 60; // in seconds
61                 int expectedExitCode = 0;
62                 string testsuiteName = null;
63                 string inputFile = null;
64
65                 // FIXME: Add support for runtime arguments + env variables
66
67                 string disabled_tests = null;
68                 string runtime = "mono";
69                 var opt_sets = new List<string> ();
70
71                 // Process options
72                 int i = 0;
73                 while (i < args.Length) {
74                         if (args [i].StartsWith ("-")) {
75                                 if (args [i] == "-j") {
76                                         if (i + 1 >= args.Length) {
77                                                 Console.WriteLine ("Missing argument to -j command line option.");
78                                                 return 1;
79                                         }
80                                         if (args [i + 1] == "a")
81                                                 concurrency = Environment.ProcessorCount;
82                                         else
83                                                 concurrency = Int32.Parse (args [i + 1]);
84                                         i += 2;
85                                 } else if (args [i] == "--timeout") {
86                                         if (i + 1 >= args.Length) {
87                                                 Console.WriteLine ("Missing argument to --timeout command line option.");
88                                                 return 1;
89                                         }
90                                         timeout = Int32.Parse (args [i + 1]);
91                                         i += 2;
92                                 } else if (args [i] == "--disabled") {
93                                         if (i + 1 >= args.Length) {
94                                                 Console.WriteLine ("Missing argument to --disabled command line option.");
95                                                 return 1;
96                                         }
97                                         disabled_tests = args [i + 1];
98                                         i += 2;
99                                 } else if (args [i] == "--runtime") {
100                                         if (i + 1 >= args.Length) {
101                                                 Console.WriteLine ("Missing argument to --runtime command line option.");
102                                                 return 1;
103                                         }
104                                         runtime = args [i + 1];
105                                         i += 2;
106                                 } else if (args [i] == "--opt-sets") {
107                                         if (i + 1 >= args.Length) {
108                                                 Console.WriteLine ("Missing argument to --opt-sets command line option.");
109                                                 return 1;
110                                         }
111                                         foreach (var s in args [i + 1].Split ())
112                                                 opt_sets.Add (s);
113                                         i += 2;
114                                 } else if (args [i] == "--expected-exit-code") {
115                                         if (i + 1 >= args.Length) {
116                                                 Console.WriteLine ("Missing argument to --expected-exit-code command line option.");
117                                                 return 1;
118                                         }
119                                         expectedExitCode = Int32.Parse (args [i + 1]);
120                                         i += 2;
121                                 } else if (args [i] == "--testsuite-name") {
122                                         if (i + 1 >= args.Length) {
123                                                 Console.WriteLine ("Missing argument to --testsuite-name command line option.");
124                                                 return 1;
125                                         }
126                                         testsuiteName = args [i + 1];
127                                         i += 2;
128                                 } else if (args [i] == "--input-file") {
129                                         if (i + 1 >= args.Length) {
130                                                 Console.WriteLine ("Missing argument to --input-file command line option.");
131                                                 return 1;
132                                         }
133                                         inputFile = args [i + 1];
134                                         i += 2;
135                                 } else {
136                                         Console.WriteLine ("Unknown command line option: '" + args [i] + "'.");
137                                         return 1;
138                                 }
139                         } else {
140                                 break;
141                         }
142                 }
143
144                 if (String.IsNullOrEmpty (testsuiteName)) {
145                         Console.WriteLine ("Missing the required --testsuite-name command line option.");
146                         return 1;
147                 }
148
149                 var disabled = new Dictionary <string, string> ();
150
151                 if (disabled_tests != null) {
152                         foreach (string test in disabled_tests.Split ())
153                                 disabled [test] = test;
154                 }
155
156                 var tests = new List<string> ();
157
158                 if (!String.IsNullOrEmpty (inputFile)) {
159                         tests.AddRange (File.ReadAllLines (inputFile));
160                 } else {
161                         // The remaining arguments are the tests
162                         for (int j = i; j < args.Length; ++j)
163                                 if (!disabled.ContainsKey (args [j]))
164                                         tests.Add (args [j]);
165                 }
166
167                 var passed = new List<ProcessData> ();
168                 var failed = new List<ProcessData> ();
169                 var timedout = new List<ProcessData> ();
170
171                 object monitor = new object ();
172
173                 Console.WriteLine ("Running tests: ");
174
175                 var test_info = new Queue<TestInfo> ();
176                 if (opt_sets.Count == 0) {
177                         foreach (string s in tests)
178                                 test_info.Enqueue (new TestInfo { test = s });
179                 } else {
180                         foreach (string opt in opt_sets) {
181                                 foreach (string s in tests)
182                                         test_info.Enqueue (new TestInfo { test = s, opt_set = opt });
183                         }
184                 }
185
186                 /* compute the max length of test names, to have an optimal output width */
187                 int output_width = -1;
188                 foreach (TestInfo ti in test_info) {
189                         if (ti.test.Length > output_width)
190                                 output_width = Math.Min (120, ti.test.Length);
191                 }
192
193                 List<Thread> threads = new List<Thread> (concurrency);
194
195                 DateTime test_start_time = DateTime.UtcNow;
196
197                 for (int j = 0; j < concurrency; ++j) {
198                         Thread thread = new Thread (() => {
199                                 while (true) {
200                                         TestInfo ti;
201
202                                         lock (monitor) {
203                                                 if (test_info.Count == 0)
204                                                         break;
205                                                 ti = test_info.Dequeue ();
206                                         }
207
208                                         var output = new StringWriter ();
209
210                                         string test = ti.test;
211                                         string opt_set = ti.opt_set;
212
213                                         output.Write (String.Format ("{{0,-{0}}} ", output_width), test);
214
215                                         /* Spawn a new process */
216                                         string process_args;
217                                         if (opt_set == null)
218                                                 process_args = test;
219                                         else
220                                                 process_args = "-O=" + opt_set + " " + test;
221                                         ProcessStartInfo info = new ProcessStartInfo (runtime, process_args);
222                                         info.UseShellExecute = false;
223                                         info.RedirectStandardOutput = true;
224                                         info.RedirectStandardError = true;
225                                         info.EnvironmentVariables[ENV_TIMEOUT] = timeout.ToString();
226                                         Process p = new Process ();
227                                         p.StartInfo = info;
228
229                                         ProcessData data = new ProcessData ();
230                                         data.test = test;
231
232                                         string log_prefix = "";
233                                         if (opt_set != null)
234                                                 log_prefix = "." + opt_set.Replace ("-", "no").Replace (",", "_");
235
236                                         data.stdoutFile = test + log_prefix + ".stdout";
237                                         data.stdout = new StreamWriter (new FileStream (data.stdoutFile, FileMode.Create));
238
239                                         data.stderrFile = test + log_prefix + ".stderr";
240                                         data.stderr = new StreamWriter (new FileStream (data.stderrFile, FileMode.Create));
241
242                                         p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
243                                                 if (e.Data != null) {
244                                                         data.stdout.WriteLine (e.Data);
245                                                 } else {
246                                                         data.stdout.Flush ();
247                                                         data.stdout.Close ();
248                                                 }
249                                         };
250
251                                         p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
252                                                 if (e.Data != null) {
253                                                         data.stderr.WriteLine (e.Data);
254                                                 } else {
255                                                         data.stderr.Flush ();
256                                                         data.stderr.Close ();
257                                                 }
258                                         };
259
260                                         var start = DateTime.UtcNow;
261
262                                         p.Start ();
263
264                                         p.BeginOutputReadLine ();
265                                         p.BeginErrorReadLine ();
266
267                                         if (!p.WaitForExit (timeout * 1000)) {
268                                                 lock (monitor) {
269                                                         timedout.Add (data);
270                                                 }
271
272                                                 // Force the process to print a thread dump
273                                                 try {
274                                                         Syscall.kill (p.Id, Signum.SIGQUIT);
275                                                         Thread.Sleep (1000);
276                                                 } catch {
277                                                 }
278
279                                                 output.Write ("timed out");
280
281                                                 p.Kill ();
282                                         } else if (p.ExitCode != expectedExitCode) {
283                                                 var end = DateTime.UtcNow;
284
285                                                 lock (monitor) {
286                                                         failed.Add (data);
287                                                 }
288
289                                                 output.Write ("failed, time: {0}, exit code: {1}", (end - start).ToString (TEST_TIME_FORMAT), p.ExitCode);
290                                         } else {
291                                                 var end = DateTime.UtcNow;
292
293                                                 lock (monitor) {
294                                                         passed.Add (data);
295                                                 }
296
297                                                 output.Write ("passed, time: {0}", (end - start).ToString (TEST_TIME_FORMAT));
298                                         }
299
300                                         p.Close ();
301
302                                         lock (monitor) {
303                                                 Console.WriteLine (output.ToString ());
304                                         }
305                                 }
306                         });
307
308                         thread.Start ();
309
310                         threads.Add (thread);
311                 }
312
313                 for (int j = 0; j < threads.Count; ++j)
314                         threads [j].Join ();
315
316                 TimeSpan test_time = DateTime.UtcNow - test_start_time;
317
318                 int npassed = passed.Count;
319                 int nfailed = failed.Count;
320                 int ntimedout = timedout.Count;
321
322                 XmlWriterSettings xmlWriterSettings = new XmlWriterSettings ();
323                 xmlWriterSettings.NewLineOnAttributes = true;
324                 xmlWriterSettings.Indent = true;
325                 using (XmlWriter writer = XmlWriter.Create (String.Format ("TestResult-{0}.xml", testsuiteName), xmlWriterSettings)) {
326                         // <?xml version="1.0" encoding="utf-8" standalone="no"?>
327                         writer.WriteStartDocument ();
328                         // <!--This file represents the results of running a test suite-->
329                         writer.WriteComment ("This file represents the results of running a test suite");
330                         // <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">
331                         writer.WriteStartElement ("test-results");
332                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
333                         writer.WriteAttributeString ("total", (npassed + nfailed + ntimedout).ToString());
334                         writer.WriteAttributeString ("failures", (nfailed + ntimedout).ToString());
335                         writer.WriteAttributeString ("not-run", "0");
336                         writer.WriteAttributeString ("date", DateTime.Now.ToString ("yyyy-MM-dd"));
337                         writer.WriteAttributeString ("time", DateTime.Now.ToString ("HH:mm:ss"));
338                         //   <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" />
339                         writer.WriteStartElement ("environment");
340                         writer.WriteAttributeString ("nunit-version", "2.4.8.0" );
341                         writer.WriteAttributeString ("clr-version", Environment.Version.ToString() );
342                         writer.WriteAttributeString ("os-version", Environment.OSVersion.ToString() );
343                         writer.WriteAttributeString ("platform", Environment.OSVersion.Platform.ToString() );
344                         writer.WriteAttributeString ("cwd", Environment.CurrentDirectory );
345                         writer.WriteAttributeString ("machine-name", Environment.MachineName );
346                         writer.WriteAttributeString ("user", Environment.UserName );
347                         writer.WriteAttributeString ("user-domain", Environment.UserDomainName );
348                         writer.WriteEndElement ();
349                         //   <culture-info current-culture="en-GB" current-uiculture="en-GB" />
350                         writer.WriteStartElement ("culture-info");
351                         writer.WriteAttributeString ("current-culture", CultureInfo.CurrentCulture.Name );
352                         writer.WriteAttributeString ("current-uiculture", CultureInfo.CurrentUICulture.Name );
353                         writer.WriteEndElement ();
354                         //   <test-suite name="corlib_test_net_4_5.dll" success="True" time="114.318" asserts="0">
355                         writer.WriteStartElement ("test-suite");
356                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
357                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
358                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
359                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
360                         //     <results>
361                         writer.WriteStartElement ("results");
362                         //       <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
363                         writer.WriteStartElement ("test-suite");
364                         writer.WriteAttributeString ("name","MonoTests");
365                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
366                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
367                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
368                         //         <results>
369                         writer.WriteStartElement ("results");
370                         //           <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
371                         writer.WriteStartElement ("test-suite");
372                         writer.WriteAttributeString ("name", testsuiteName);
373                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
374                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
375                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
376                         //             <results>
377                         writer.WriteStartElement ("results");
378                         // Dump all passing tests first
379                         foreach (ProcessData pd in passed) {
380                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
381                                 writer.WriteStartElement ("test-case");
382                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
383                                 writer.WriteAttributeString ("executed", "True");
384                                 writer.WriteAttributeString ("success", "True");
385                                 writer.WriteAttributeString ("time", "0");
386                                 writer.WriteAttributeString ("asserts", "0");
387                                 writer.WriteEndElement ();
388                         }
389                         // Now dump all failing tests
390                         foreach (ProcessData pd in failed) {
391                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
392                                 writer.WriteStartElement ("test-case");
393                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
394                                 writer.WriteAttributeString ("executed", "True");
395                                 writer.WriteAttributeString ("success", "False");
396                                 writer.WriteAttributeString ("time", "0");
397                                 writer.WriteAttributeString ("asserts", "1");
398                                 writer.WriteStartElement ("failure");
399                                 writer.WriteStartElement ("message");
400                                 writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
401                                 writer.WriteEndElement ();
402                                 writer.WriteStartElement ("stack-trace");
403                                 writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
404                                 writer.WriteEndElement ();
405                                 writer.WriteEndElement ();
406                                 writer.WriteEndElement ();
407                         }
408                         // Then dump all timing out tests
409                         foreach (ProcessData pd in timedout) {
410                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
411                                 writer.WriteStartElement ("test-case");
412                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}_timedout", testsuiteName, pd.test));
413                                 writer.WriteAttributeString ("executed", "True");
414                                 writer.WriteAttributeString ("success", "False");
415                                 writer.WriteAttributeString ("time", "0");
416                                 writer.WriteAttributeString ("asserts", "1");
417                                 writer.WriteStartElement ("failure");
418                                 writer.WriteStartElement ("message");
419                                 writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
420                                 writer.WriteEndElement ();
421                                 writer.WriteStartElement ("stack-trace");
422                                 writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
423                                 writer.WriteEndElement ();
424                                 writer.WriteEndElement ();
425                                 writer.WriteEndElement ();
426                         }
427                         //             </results>
428                         writer.WriteEndElement ();
429                         //           </test-suite>
430                         writer.WriteEndElement ();
431                         //         </results>
432                         writer.WriteEndElement ();
433                         //       </test-suite>
434                         writer.WriteEndElement ();
435                         //     </results>
436                         writer.WriteEndElement ();
437                         //   </test-suite>
438                         writer.WriteEndElement ();
439                         // </test-results>
440                         writer.WriteEndElement ();
441                         writer.WriteEndDocument ();
442                 }
443
444                 Console.WriteLine ();
445                 Console.WriteLine ("Time: {0}", test_time.ToString (TEST_TIME_FORMAT));
446                 Console.WriteLine ();
447                 Console.WriteLine ("{0,4} test(s) passed", npassed);
448                 Console.WriteLine ("{0,4} test(s) failed", nfailed);
449                 Console.WriteLine ("{0,4} test(s) timed out", ntimedout);
450
451                 if (nfailed > 0) {
452                         Console.WriteLine ();
453                         Console.WriteLine ("Failed test(s):");
454                         foreach (ProcessData pd in failed) {
455                                 Console.WriteLine ();
456                                 Console.WriteLine (pd.test);
457                                 DumpFile (pd.stdoutFile);
458                                 DumpFile (pd.stderrFile);
459                         }
460                 }
461
462                 if (ntimedout > 0) {
463                         Console.WriteLine ();
464                         Console.WriteLine ("Timed out test(s):");
465                         foreach (ProcessData pd in timedout) {
466                                 Console.WriteLine ();
467                                 Console.WriteLine (pd.test);
468                                 DumpFile (pd.stdoutFile);
469                                 DumpFile (pd.stderrFile);
470                         }
471                 }
472
473                 return (ntimedout == 0 && nfailed == 0) ? 0 : 1;
474         }
475         
476         static void DumpFile (string filename) {
477                 if (File.Exists (filename)) {
478                         Console.WriteLine ("=============== {0} ===============", filename);
479                         Console.WriteLine (File.ReadAllText (filename));
480                         Console.WriteLine ("=============== EOF ===============");
481                 }
482         }
483
484         static string DumpPseudoTrace (string filename) {
485                 if (File.Exists (filename))
486                         return FilterInvalidXmlChars (File.ReadAllText (filename));
487                 else
488                         return string.Empty;
489         }
490
491         static string FilterInvalidXmlChars (string text) {
492                 // Spec at http://www.w3.org/TR/2008/REC-xml-20081126/#charsets says only the following chars are valid in XML:
493                 // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]      /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
494                 return Regex.Replace (text, @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]", "");
495         }
496 }