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