6195d5f4008d849a966952a81bd8984a0204f888
[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;
19 using System.Text.RegularExpressions;
20
21 #if !FULL_AOT_DESKTOP && !MOBILE
22 using Mono.Unix.Native;
23 #endif
24
25 //
26 // This is a simple test runner with support for parallel execution
27 //
28
29 public class TestRunner
30 {
31         const string TEST_TIME_FORMAT = "mm\\:ss\\.fff";
32         const string ENV_TIMEOUT = "TEST_DRIVER_TIMEOUT_SEC";
33         const string MONO_PATH = "MONO_PATH";
34
35         class ProcessData {
36                 public string test;
37                 public StringBuilder stdout, stderr;
38                 public string stdoutName, stderrName;
39         }
40
41         class TestInfo {
42                 public string test, opt_set;
43         }
44
45         public static int Main (String[] args) {
46                 // Defaults
47                 int concurrency = 1;
48                 int timeout = 2 * 60; // in seconds
49                 int expectedExitCode = 0;
50                 bool verbose = false;
51                 string testsuiteName = null;
52                 string inputFile = null;
53
54                 string disabled_tests = null;
55                 string runtime = "mono";
56                 string config = null;
57                 string mono_path = null;
58                 var opt_sets = new List<string> ();
59
60                 string aot_run_flags = null;
61                 string aot_build_flags = null;
62
63                 // Process options
64                 int i = 0;
65                 while (i < args.Length) {
66                         if (args [i].StartsWith ("-")) {
67                                 if (args [i] == "-j") {
68                                         if (i + 1 >= args.Length) {
69                                                 Console.WriteLine ("Missing argument to -j command line option.");
70                                                 return 1;
71                                         }
72                                         if (args [i + 1] == "a")
73                                                 concurrency = Environment.ProcessorCount;
74                                         else
75                                                 concurrency = Int32.Parse (args [i + 1]);
76                                         i += 2;
77                                 } else if (args [i] == "--timeout") {
78                                         if (i + 1 >= args.Length) {
79                                                 Console.WriteLine ("Missing argument to --timeout command line option.");
80                                                 return 1;
81                                         }
82                                         timeout = Int32.Parse (args [i + 1]);
83                                         i += 2;
84                                 } else if (args [i] == "--disabled") {
85                                         if (i + 1 >= args.Length) {
86                                                 Console.WriteLine ("Missing argument to --disabled command line option.");
87                                                 return 1;
88                                         }
89                                         disabled_tests = args [i + 1];
90                                         i += 2;
91                                 } else if (args [i] == "--runtime") {
92                                         if (i + 1 >= args.Length) {
93                                                 Console.WriteLine ("Missing argument to --runtime command line option.");
94                                                 return 1;
95                                         }
96                                         runtime = args [i + 1];
97                                         i += 2;
98                                 } else if (args [i] == "--config") {
99                                         if (i + 1 >= args.Length) {
100                                                 Console.WriteLine ("Missing argument to --config command line option.");
101                                                 return 1;
102                                         }
103                                         config = args [i + 1];
104                                         i += 2;
105                                 } else if (args [i] == "--opt-sets") {
106                                         if (i + 1 >= args.Length) {
107                                                 Console.WriteLine ("Missing argument to --opt-sets command line option.");
108                                                 return 1;
109                                         }
110                                         foreach (var s in args [i + 1].Split ())
111                                                 opt_sets.Add (s);
112                                         i += 2;
113                                 } else if (args [i] == "--expected-exit-code") {
114                                         if (i + 1 >= args.Length) {
115                                                 Console.WriteLine ("Missing argument to --expected-exit-code command line option.");
116                                                 return 1;
117                                         }
118                                         expectedExitCode = Int32.Parse (args [i + 1]);
119                                         i += 2;
120                                 } else if (args [i] == "--testsuite-name") {
121                                         if (i + 1 >= args.Length) {
122                                                 Console.WriteLine ("Missing argument to --testsuite-name command line option.");
123                                                 return 1;
124                                         }
125                                         testsuiteName = args [i + 1];
126                                         i += 2;
127                                 } else if (args [i] == "--input-file") {
128                                         if (i + 1 >= args.Length) {
129                                                 Console.WriteLine ("Missing argument to --input-file command line option.");
130                                                 return 1;
131                                         }
132                                         inputFile = args [i + 1];
133                                         i += 2;
134                                 } else if (args [i] == "--runtime") {
135                                         if (i + 1 >= args.Length) {
136                                                 Console.WriteLine ("Missing argument to --runtime command line option.");
137                                                 return 1;
138                                         }
139                                         runtime = args [i + 1];
140                                         i += 2;
141                                 } else if (args [i] == "--mono-path") {
142                                         if (i + 1 >= args.Length) {
143                                                 Console.WriteLine ("Missing argument to --mono-path command line option.");
144                                                 return 1;
145                                         }
146                                         mono_path = args [i + 1].Substring(0, args [i + 1].Length);
147
148                                         i += 2;
149                                 } else if (args [i] == "--aot-run-flags") {
150                                         if (i + 1 >= args.Length) {
151                                                 Console.WriteLine ("Missing argument to --aot-run-flags command line option.");
152                                                 return 1;
153                                         }
154                                         aot_run_flags = args [i + 1].Substring(0, args [i + 1].Length);
155                                         i += 2;
156                                 } else if (args [i] == "--aot-build-flags") {
157                                         if (i + 1 >= args.Length) {
158                                                 Console.WriteLine ("Missing argument to --aot-build-flags command line option.");
159                                                 return 1;
160                                         }
161                                         aot_build_flags = args [i + 1].Substring(0, args [i + 1].Length);
162                                         i += 2;
163                                 } else if (args [i] == "--verbose") {
164                                         verbose = true;
165                                         i ++;
166                                 } else {
167                                         Console.WriteLine ("Unknown command line option: '" + args [i] + "'.");
168                                         return 1;
169                                 }
170                         } else {
171                                 break;
172                         }
173                 }
174
175                 if (String.IsNullOrEmpty (testsuiteName)) {
176                         Console.WriteLine ("Missing the required --testsuite-name command line option.");
177                         return 1;
178                 }
179
180                 var disabled = new Dictionary <string, string> ();
181
182                 if (disabled_tests != null) {
183                         foreach (string test in disabled_tests.Split ())
184                                 disabled [test] = test;
185                 }
186
187                 var tests = new List<string> ();
188
189                 if (!String.IsNullOrEmpty (inputFile)) {
190                         tests.AddRange (File.ReadAllLines (inputFile));
191                 } else {
192                         // The remaining arguments are the tests
193                         for (int j = i; j < args.Length; ++j)
194                                 if (!disabled.ContainsKey (args [j]))
195                                         tests.Add (args [j]);
196                 }
197
198                 var passed = new List<ProcessData> ();
199                 var failed = new List<ProcessData> ();
200                 var timedout = new List<ProcessData> ();
201
202                 object monitor = new object ();
203
204                 Console.WriteLine ("Running tests: ");
205
206                 var test_info = new Queue<TestInfo> ();
207                 if (opt_sets.Count == 0) {
208                         foreach (string s in tests)
209                                 test_info.Enqueue (new TestInfo { test = s });
210                 } else {
211                         foreach (string opt in opt_sets) {
212                                 foreach (string s in tests)
213                                         test_info.Enqueue (new TestInfo { test = s, opt_set = opt });
214                         }
215                 }
216
217                 /* compute the max length of test names, to have an optimal output width */
218                 int output_width = -1;
219                 foreach (TestInfo ti in test_info) {
220                         if (ti.test.Length > output_width)
221                                 output_width = Math.Min (120, ti.test.Length);
222                 }
223
224                 if (aot_build_flags != null)  {
225                         Console.WriteLine("AOT compiling tests");
226
227                         object aot_monitor = new object ();
228                         var aot_queue = new Queue<String> (tests); 
229
230                         List<Thread> build_threads = new List<Thread> (concurrency);
231
232                         for (int j = 0; j < concurrency; ++j) {
233                                 Thread thread = new Thread (() => {
234                                         while (true) {
235                                                 String test_name;
236
237                                                 lock (aot_monitor) {
238                                                         if (aot_queue.Count == 0)
239                                                                 break;
240                                                         test_name = aot_queue.Dequeue ();
241                                                 }
242
243                                                 string test_bitcode_output = test_name + "_bitcode_tmp";
244                                                 string test_bitcode_arg = ",temp-path=" + test_bitcode_output;
245                                                 string aot_args = aot_build_flags + test_bitcode_arg + " " + test_name;
246
247                                                 Directory.CreateDirectory(test_bitcode_output);
248
249                                                 ProcessStartInfo job = new ProcessStartInfo (runtime, aot_args);
250                                                 job.UseShellExecute = false;
251                                                 job.EnvironmentVariables[ENV_TIMEOUT] = timeout.ToString();
252                                                 job.EnvironmentVariables[MONO_PATH] = mono_path;
253                                                 Process compiler = new Process ();
254                                                 compiler.StartInfo = job;
255
256                                                 compiler.Start ();
257
258                                                 if (!compiler.WaitForExit (timeout * 1000)) {
259                                                         try {
260                                                                 compiler.Kill ();
261                                                         } catch {
262                                                         }
263                                                         throw new Exception(String.Format("Timeout AOT compiling tests, output in {0}", test_bitcode_output));
264                                                 } else if (compiler.ExitCode != 0) {
265                                                         throw new Exception(String.Format("Error AOT compiling tests, output in {0}", test_bitcode_output));
266                                                 } else {
267                                                         Directory.Delete (test_bitcode_output, true);
268                                                 }
269                                         }
270                                 });
271
272                                 thread.Start ();
273
274                                 build_threads.Add (thread);
275                         }
276
277                         for (int j = 0; j < build_threads.Count; ++j)
278                                 build_threads [j].Join ();
279
280                         Console.WriteLine("Done compiling");
281                 }
282
283                 List<Thread> threads = new List<Thread> (concurrency);
284
285                 DateTime test_start_time = DateTime.UtcNow;
286
287                 for (int j = 0; j < concurrency; ++j) {
288                         Thread thread = new Thread (() => {
289                                 while (true) {
290                                         TestInfo ti;
291
292                                         lock (monitor) {
293                                                 if (test_info.Count == 0)
294                                                         break;
295                                                 ti = test_info.Dequeue ();
296                                         }
297
298                                         var output = new StringWriter ();
299
300                                         string test = ti.test;
301                                         string opt_set = ti.opt_set;
302
303                                         if (verbose) {
304                                                 output.Write (String.Format ("{{0,-{0}}} ", output_width), test);
305                                         } else {
306                                                 Console.Write (".");
307                                         }
308
309                                         string test_invoke;
310
311                                         if (aot_run_flags != null)
312                                                 test_invoke = aot_run_flags + " " + test;
313                                         else
314                                                 test_invoke = test;
315
316                                         /* Spawn a new process */
317                                         string process_args;
318                                         if (opt_set == null)
319                                                 process_args = test_invoke;
320                                         else
321                                                 process_args = "-O=" + opt_set + " " + test_invoke;
322
323                                         ProcessStartInfo info = new ProcessStartInfo (runtime, process_args);
324                                         info.UseShellExecute = false;
325                                         info.RedirectStandardOutput = true;
326                                         info.RedirectStandardError = true;
327                                         info.EnvironmentVariables[ENV_TIMEOUT] = timeout.ToString();
328                                         if (config != null)
329                                                 info.EnvironmentVariables["MONO_CONFIG"] = config;
330                                         if (mono_path != null)
331                                                 info.EnvironmentVariables[MONO_PATH] = mono_path;
332                                         Process p = new Process ();
333                                         p.StartInfo = info;
334
335                                         ProcessData data = new ProcessData ();
336                                         data.test = test;
337
338                                         string log_prefix = "";
339                                         if (opt_set != null)
340                                                 log_prefix = "." + opt_set.Replace ("-", "no").Replace (",", "_");
341
342                                         data.stdoutName = test + log_prefix + ".stdout";
343                                         data.stdout = new StringBuilder ();
344
345                                         data.stderrName = test + log_prefix + ".stderr";
346                                         data.stderr = new StringBuilder ();
347
348                                         p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
349                                                 if (e.Data != null) {
350                                                         data.stdout.AppendLine (e.Data);
351                                                 }
352                                         };
353
354                                         p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
355                                                 if (e.Data != null) {
356                                                         data.stderr.AppendLine (e.Data);
357                                                 }
358                                         };
359
360                                         var start = DateTime.UtcNow;
361
362                                         p.Start ();
363
364                                         p.BeginOutputReadLine ();
365                                         p.BeginErrorReadLine ();
366
367                                         if (!p.WaitForExit (timeout * 1000)) {
368                                                 lock (monitor) {
369                                                         timedout.Add (data);
370                                                 }
371
372 #if !FULL_AOT_DESKTOP && !MOBILE
373                                                 // Force the process to print a thread dump
374                                                 try {
375                                                         Syscall.kill (p.Id, Signum.SIGQUIT);
376                                                         Thread.Sleep (1000);
377                                                 } catch {
378                                                 }
379 #endif
380
381                                                 if (verbose) {
382                                                         output.Write ($"timed out ({timeout}s)");
383                                                 }
384
385                                                 try {
386                                                         p.Kill ();
387                                                 } catch {
388                                                 }
389                                         } else if (p.ExitCode != expectedExitCode) {
390                                                 var end = DateTime.UtcNow;
391
392                                                 lock (monitor) {
393                                                         failed.Add (data);
394                                                 }
395
396                                                 if (verbose)
397                                                         output.Write ("failed, time: {0}, exit code: {1}", (end - start).ToString (TEST_TIME_FORMAT), p.ExitCode);
398                                         } else {
399                                                 var end = DateTime.UtcNow;
400
401                                                 lock (monitor) {
402                                                         passed.Add (data);
403                                                 }
404
405                                                 if (verbose)
406                                                         output.Write ("passed, time: {0}", (end - start).ToString (TEST_TIME_FORMAT));
407                                         }
408
409                                         p.Close ();
410
411                                         lock (monitor) {
412                                                 if (verbose)
413                                                         Console.WriteLine (output.ToString ());
414                                         }
415                                 }
416                         });
417
418                         thread.Start ();
419
420                         threads.Add (thread);
421                 }
422
423                 for (int j = 0; j < threads.Count; ++j)
424                         threads [j].Join ();
425
426                 TimeSpan test_time = DateTime.UtcNow - test_start_time;
427
428                 int npassed = passed.Count;
429                 int nfailed = failed.Count;
430                 int ntimedout = timedout.Count;
431
432                 XmlWriterSettings xmlWriterSettings = new XmlWriterSettings ();
433                 xmlWriterSettings.NewLineOnAttributes = true;
434                 xmlWriterSettings.Indent = true;
435
436                 string xmlPath = String.Format ("TestResult-{0}.xml", testsuiteName);
437                 using (XmlWriter writer = XmlWriter.Create (xmlPath, xmlWriterSettings)) {
438                         // <?xml version="1.0" encoding="utf-8" standalone="no"?>
439                         writer.WriteStartDocument ();
440                         // <!--This file represents the results of running a test suite-->
441                         writer.WriteComment ("This file represents the results of running a test suite");
442                         // <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">
443                         writer.WriteStartElement ("test-results");
444                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
445                         writer.WriteAttributeString ("total", (npassed + nfailed + ntimedout).ToString());
446                         writer.WriteAttributeString ("failures", (nfailed + ntimedout).ToString());
447                         writer.WriteAttributeString ("not-run", "0");
448                         writer.WriteAttributeString ("date", DateTime.Now.ToString ("yyyy-MM-dd"));
449                         writer.WriteAttributeString ("time", DateTime.Now.ToString ("HH:mm:ss"));
450                         //   <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" />
451                         writer.WriteStartElement ("environment");
452                         writer.WriteAttributeString ("nunit-version", "2.4.8.0" );
453                         writer.WriteAttributeString ("clr-version", Environment.Version.ToString() );
454                         writer.WriteAttributeString ("os-version", Environment.OSVersion.ToString() );
455                         writer.WriteAttributeString ("platform", Environment.OSVersion.Platform.ToString() );
456                         writer.WriteAttributeString ("cwd", Environment.CurrentDirectory );
457                         writer.WriteAttributeString ("machine-name", Environment.MachineName );
458                         writer.WriteAttributeString ("user", Environment.UserName );
459                         writer.WriteAttributeString ("user-domain", Environment.UserDomainName );
460                         writer.WriteEndElement ();
461                         //   <culture-info current-culture="en-GB" current-uiculture="en-GB" />
462                         writer.WriteStartElement ("culture-info");
463                         writer.WriteAttributeString ("current-culture", CultureInfo.CurrentCulture.Name );
464                         writer.WriteAttributeString ("current-uiculture", CultureInfo.CurrentUICulture.Name );
465                         writer.WriteEndElement ();
466                         //   <test-suite name="corlib_test_net_4_5.dll" success="True" time="114.318" asserts="0">
467                         writer.WriteStartElement ("test-suite");
468                         writer.WriteAttributeString ("name", String.Format ("{0}-tests.dummy", testsuiteName));
469                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
470                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
471                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
472                         //     <results>
473                         writer.WriteStartElement ("results");
474                         //       <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
475                         writer.WriteStartElement ("test-suite");
476                         writer.WriteAttributeString ("name","MonoTests");
477                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
478                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
479                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
480                         //         <results>
481                         writer.WriteStartElement ("results");
482                         //           <test-suite name="MonoTests" success="True" time="114.318" asserts="0">
483                         writer.WriteStartElement ("test-suite");
484                         writer.WriteAttributeString ("name", testsuiteName);
485                         writer.WriteAttributeString ("success", (nfailed + ntimedout == 0).ToString());
486                         writer.WriteAttributeString ("time", test_time.Seconds.ToString());
487                         writer.WriteAttributeString ("asserts", (nfailed + ntimedout).ToString());
488                         //             <results>
489                         writer.WriteStartElement ("results");
490                         // Dump all passing tests first
491                         foreach (ProcessData pd in passed) {
492                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
493                                 writer.WriteStartElement ("test-case");
494                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
495                                 writer.WriteAttributeString ("executed", "True");
496                                 writer.WriteAttributeString ("success", "True");
497                                 writer.WriteAttributeString ("time", "0");
498                                 writer.WriteAttributeString ("asserts", "0");
499                                 writer.WriteEndElement ();
500                         }
501                         // Now dump all failing tests
502                         foreach (ProcessData pd in failed) {
503                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
504                                 writer.WriteStartElement ("test-case");
505                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}", testsuiteName, pd.test));
506                                 writer.WriteAttributeString ("executed", "True");
507                                 writer.WriteAttributeString ("success", "False");
508                                 writer.WriteAttributeString ("time", "0");
509                                 writer.WriteAttributeString ("asserts", "1");
510                                 writer.WriteStartElement ("failure");
511                                 writer.WriteStartElement ("message");
512                                 writer.WriteCData (FilterInvalidXmlChars (pd.stdout.ToString ()));
513                                 writer.WriteEndElement ();
514                                 writer.WriteStartElement ("stack-trace");
515                                 writer.WriteCData (FilterInvalidXmlChars (pd.stderr.ToString ()));
516                                 writer.WriteEndElement ();
517                                 writer.WriteEndElement ();
518                                 writer.WriteEndElement ();
519                         }
520                         // Then dump all timing out tests
521                         foreach (ProcessData pd in timedout) {
522                                 // <test-case name="MonoTests.Microsoft.Win32.RegistryKeyTest.bug79051" executed="True" success="True" time="0.063" asserts="0" />
523                                 writer.WriteStartElement ("test-case");
524                                 writer.WriteAttributeString ("name", String.Format ("MonoTests.{0}.{1}_timedout", testsuiteName, pd.test));
525                                 writer.WriteAttributeString ("executed", "True");
526                                 writer.WriteAttributeString ("success", "False");
527                                 writer.WriteAttributeString ("time", "0");
528                                 writer.WriteAttributeString ("asserts", "1");
529                                 writer.WriteStartElement ("failure");
530                                 writer.WriteStartElement ("message");
531                                 writer.WriteCData (FilterInvalidXmlChars (pd.stdout.ToString ()));
532                                 writer.WriteEndElement ();
533                                 writer.WriteStartElement ("stack-trace");
534                                 writer.WriteCData (FilterInvalidXmlChars (pd.stderr.ToString ()));
535                                 writer.WriteEndElement ();
536                                 writer.WriteEndElement ();
537                                 writer.WriteEndElement ();
538                         }
539                         //             </results>
540                         writer.WriteEndElement ();
541                         //           </test-suite>
542                         writer.WriteEndElement ();
543                         //         </results>
544                         writer.WriteEndElement ();
545                         //       </test-suite>
546                         writer.WriteEndElement ();
547                         //     </results>
548                         writer.WriteEndElement ();
549                         //   </test-suite>
550                         writer.WriteEndElement ();
551                         // </test-results>
552                         writer.WriteEndElement ();
553                         writer.WriteEndDocument ();
554
555                         string babysitterXmlList = Environment.GetEnvironmentVariable("MONO_BABYSITTER_NUNIT_XML_LIST_FILE");
556                         if (!String.IsNullOrEmpty(babysitterXmlList)) {
557                                 try {
558                                         string fullXmlPath = Path.GetFullPath(xmlPath);
559                                         File.AppendAllText(babysitterXmlList, fullXmlPath + Environment.NewLine);
560                                 } catch (Exception e) {
561                                         Console.WriteLine("Attempted to record XML path to file {0} but failed.", babysitterXmlList);
562                                 }
563                         }
564                 }
565
566                 if (verbose) {
567                         Console.WriteLine ();
568                         Console.WriteLine ("Time: {0}", test_time.ToString (TEST_TIME_FORMAT));
569                         Console.WriteLine ();
570                         Console.WriteLine ("{0,4} test(s) passed", npassed);
571                         Console.WriteLine ("{0,4} test(s) failed", nfailed);
572                         Console.WriteLine ("{0,4} test(s) timed out", ntimedout);
573                 } else {
574                         Console.WriteLine ();
575                         Console.WriteLine (String.Format ("{0} test(s) passed, {1} test(s) did not pass.", npassed, nfailed));
576                 }
577
578                 if (nfailed > 0) {
579                         Console.WriteLine ();
580                         Console.WriteLine ("Failed test(s):");
581                         foreach (ProcessData pd in failed) {
582                                 Console.WriteLine ();
583                                 Console.WriteLine (pd.test);
584                                 DumpFile (pd.stdoutName, pd.stdout.ToString ());
585                                 DumpFile (pd.stderrName, pd.stderr.ToString ());
586                         }
587                 }
588
589                 if (ntimedout > 0) {
590                         Console.WriteLine ();
591                         Console.WriteLine ("Timed out test(s):");
592                         foreach (ProcessData pd in timedout) {
593                                 Console.WriteLine ();
594                                 Console.WriteLine (pd.test);
595                                 DumpFile (pd.stdoutName, pd.stdout.ToString ());
596                                 DumpFile (pd.stderrName, pd.stderr.ToString ());
597                         }
598                 }
599
600                 return (ntimedout == 0 && nfailed == 0) ? 0 : 1;
601         }
602         
603         static void DumpFile (string filename, string text) {
604                 Console.WriteLine ("=============== {0} ===============", filename);
605                 Console.WriteLine (text);
606                 Console.WriteLine ("=============== EOF ===============");
607         }
608
609         static string FilterInvalidXmlChars (string text) {
610                 // Spec at http://www.w3.org/TR/2008/REC-xml-20081126/#charsets says only the following chars are valid in XML:
611                 // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]      /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
612                 return Regex.Replace (text, @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]", "");
613         }
614 }