[test-runner] Try to dump threads on timeout with lldb and gdb
[mono.git] / mono / tests / test-runner.cs
index ae52220e335db404bdfe5ff59d2c6d0aa701d702..025e01cd46f5d0225770cee3df9fd98b6a382b05 100644 (file)
@@ -15,8 +15,12 @@ using System.Diagnostics;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Xml;
+using System.Text;
 using System.Text.RegularExpressions;
+
+#if !FULL_AOT_DESKTOP && !MOBILE
 using Mono.Unix.Native;
+#endif
 
 //
 // This is a simple test runner with support for parallel execution
@@ -26,11 +30,13 @@ public class TestRunner
 {
        const string TEST_TIME_FORMAT = "mm\\:ss\\.fff";
        const string ENV_TIMEOUT = "TEST_DRIVER_TIMEOUT_SEC";
+       const string MONO_PATH = "MONO_PATH";
 
        class ProcessData {
                public string test;
-               public StreamWriter stdout, stderr;
-               public string stdoutFile, stderrFile;
+               public StringBuilder stdout, stderr;
+               public object stdoutLock = new object (), stderrLock = new object ();
+               public string stdoutName, stderrName;
        }
 
        class TestInfo {
@@ -42,15 +48,19 @@ public class TestRunner
                int concurrency = 1;
                int timeout = 2 * 60; // in seconds
                int expectedExitCode = 0;
+               bool verbose = false;
                string testsuiteName = null;
                string inputFile = null;
 
-               // FIXME: Add support for runtime arguments + env variables
-
                string disabled_tests = null;
                string runtime = "mono";
+               string config = null;
+               string mono_path = null;
                var opt_sets = new List<string> ();
 
+               string aot_run_flags = null;
+               string aot_build_flags = null;
+
                // Process options
                int i = 0;
                while (i < args.Length) {
@@ -86,6 +96,13 @@ public class TestRunner
                                        }
                                        runtime = args [i + 1];
                                        i += 2;
+                               } else if (args [i] == "--config") {
+                                       if (i + 1 >= args.Length) {
+                                               Console.WriteLine ("Missing argument to --config command line option.");
+                                               return 1;
+                                       }
+                                       config = args [i + 1];
+                                       i += 2;
                                } else if (args [i] == "--opt-sets") {
                                        if (i + 1 >= args.Length) {
                                                Console.WriteLine ("Missing argument to --opt-sets command line option.");
@@ -115,6 +132,38 @@ public class TestRunner
                                        }
                                        inputFile = args [i + 1];
                                        i += 2;
+                               } else if (args [i] == "--runtime") {
+                                       if (i + 1 >= args.Length) {
+                                               Console.WriteLine ("Missing argument to --runtime command line option.");
+                                               return 1;
+                                       }
+                                       runtime = args [i + 1];
+                                       i += 2;
+                               } else if (args [i] == "--mono-path") {
+                                       if (i + 1 >= args.Length) {
+                                               Console.WriteLine ("Missing argument to --mono-path command line option.");
+                                               return 1;
+                                       }
+                                       mono_path = args [i + 1].Substring(0, args [i + 1].Length);
+
+                                       i += 2;
+                               } else if (args [i] == "--aot-run-flags") {
+                                       if (i + 1 >= args.Length) {
+                                               Console.WriteLine ("Missing argument to --aot-run-flags command line option.");
+                                               return 1;
+                                       }
+                                       aot_run_flags = args [i + 1].Substring(0, args [i + 1].Length);
+                                       i += 2;
+                               } else if (args [i] == "--aot-build-flags") {
+                                       if (i + 1 >= args.Length) {
+                                               Console.WriteLine ("Missing argument to --aot-build-flags command line option.");
+                                               return 1;
+                                       }
+                                       aot_build_flags = args [i + 1].Substring(0, args [i + 1].Length);
+                                       i += 2;
+                               } else if (args [i] == "--verbose") {
+                                       verbose = true;
+                                       i ++;
                                } else {
                                        Console.WriteLine ("Unknown command line option: '" + args [i] + "'.");
                                        return 1;
@@ -173,6 +222,65 @@ public class TestRunner
                                output_width = Math.Min (120, ti.test.Length);
                }
 
+               if (aot_build_flags != null)  {
+                       Console.WriteLine("AOT compiling tests");
+
+                       object aot_monitor = new object ();
+                       var aot_queue = new Queue<String> (tests); 
+
+                       List<Thread> build_threads = new List<Thread> (concurrency);
+
+                       for (int j = 0; j < concurrency; ++j) {
+                               Thread thread = new Thread (() => {
+                                       while (true) {
+                                               String test_name;
+
+                                               lock (aot_monitor) {
+                                                       if (aot_queue.Count == 0)
+                                                               break;
+                                                       test_name = aot_queue.Dequeue ();
+                                               }
+
+                                               string test_bitcode_output = test_name + "_bitcode_tmp";
+                                               string test_bitcode_arg = ",temp-path=" + test_bitcode_output;
+                                               string aot_args = aot_build_flags + test_bitcode_arg + " " + test_name;
+
+                                               Directory.CreateDirectory(test_bitcode_output);
+
+                                               ProcessStartInfo job = new ProcessStartInfo (runtime, aot_args);
+                                               job.UseShellExecute = false;
+                                               job.EnvironmentVariables[ENV_TIMEOUT] = timeout.ToString();
+                                               job.EnvironmentVariables[MONO_PATH] = mono_path;
+                                               Process compiler = new Process ();
+                                               compiler.StartInfo = job;
+
+                                               compiler.Start ();
+
+                                               if (!compiler.WaitForExit (timeout * 1000)) {
+                                                       try {
+                                                               compiler.Kill ();
+                                                       } catch {
+                                                       }
+                                                       throw new Exception(String.Format("Timeout AOT compiling tests, output in {0}", test_bitcode_output));
+                                               } else if (compiler.ExitCode != 0) {
+                                                       throw new Exception(String.Format("Error AOT compiling tests, output in {0}", test_bitcode_output));
+                                               } else {
+                                                       Directory.Delete (test_bitcode_output, true);
+                                               }
+                                       }
+                               });
+
+                               thread.Start ();
+
+                               build_threads.Add (thread);
+                       }
+
+                       for (int j = 0; j < build_threads.Count; ++j)
+                               build_threads [j].Join ();
+
+                       Console.WriteLine("Done compiling");
+               }
+
                List<Thread> threads = new List<Thread> (concurrency);
 
                DateTime test_start_time = DateTime.UtcNow;
@@ -193,19 +301,35 @@ public class TestRunner
                                        string test = ti.test;
                                        string opt_set = ti.opt_set;
 
-                                       output.Write (String.Format ("{{0,-{0}}} ", output_width), test);
+                                       if (verbose) {
+                                               output.Write (String.Format ("{{0,-{0}}} ", output_width), test);
+                                       } else {
+                                               Console.Write (".");
+                                       }
+
+                                       string test_invoke;
+
+                                       if (aot_run_flags != null)
+                                               test_invoke = aot_run_flags + " " + test;
+                                       else
+                                               test_invoke = test;
 
                                        /* Spawn a new process */
                                        string process_args;
                                        if (opt_set == null)
-                                               process_args = test;
+                                               process_args = test_invoke;
                                        else
-                                               process_args = "-O=" + opt_set + " " + test;
+                                               process_args = "-O=" + opt_set + " " + test_invoke;
+
                                        ProcessStartInfo info = new ProcessStartInfo (runtime, process_args);
                                        info.UseShellExecute = false;
                                        info.RedirectStandardOutput = true;
                                        info.RedirectStandardError = true;
                                        info.EnvironmentVariables[ENV_TIMEOUT] = timeout.ToString();
+                                       if (config != null)
+                                               info.EnvironmentVariables["MONO_CONFIG"] = config;
+                                       if (mono_path != null)
+                                               info.EnvironmentVariables[MONO_PATH] = mono_path;
                                        Process p = new Process ();
                                        p.StartInfo = info;
 
@@ -216,27 +340,23 @@ public class TestRunner
                                        if (opt_set != null)
                                                log_prefix = "." + opt_set.Replace ("-", "no").Replace (",", "_");
 
-                                       data.stdoutFile = test + log_prefix + ".stdout";
-                                       data.stdout = new StreamWriter (new FileStream (data.stdoutFile, FileMode.Create));
+                                       data.stdoutName = test + log_prefix + ".stdout";
+                                       data.stdout = new StringBuilder ();
 
-                                       data.stderrFile = test + log_prefix + ".stderr";
-                                       data.stderr = new StreamWriter (new FileStream (data.stderrFile, FileMode.Create));
+                                       data.stderrName = test + log_prefix + ".stderr";
+                                       data.stderr = new StringBuilder ();
 
                                        p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
-                                               if (e.Data != null) {
-                                                       data.stdout.WriteLine (e.Data);
-                                               } else {
-                                                       data.stdout.Flush ();
-                                                       data.stdout.Close ();
+                                               lock (data.stdoutLock) {
+                                                       if (e.Data != null)
+                                                               data.stdout.AppendLine (e.Data);
                                                }
                                        };
 
                                        p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
-                                               if (e.Data != null) {
-                                                       data.stderr.WriteLine (e.Data);
-                                               } else {
-                                                       data.stderr.Flush ();
-                                                       data.stderr.Close ();
+                                               lock (data.stderrLock) {
+                                                       if (e.Data != null)
+                                                               data.stderr.AppendLine (e.Data);
                                                }
                                        };
 
@@ -253,15 +373,16 @@ public class TestRunner
                                                }
 
                                                // Force the process to print a thread dump
+                                               TryThreadDump (p.Id, data);
+
+                                               if (verbose) {
+                                                       output.Write ($"timed out ({timeout}s)");
+                                               }
+
                                                try {
-                                                       Syscall.kill (p.Id, Signum.SIGQUIT);
-                                                       Thread.Sleep (1000);
+                                                       p.Kill ();
                                                } catch {
                                                }
-
-                                               output.Write ("timed out");
-
-                                               p.Kill ();
                                        } else if (p.ExitCode != expectedExitCode) {
                                                var end = DateTime.UtcNow;
 
@@ -269,7 +390,8 @@ public class TestRunner
                                                        failed.Add (data);
                                                }
 
-                                               output.Write ("failed, time: {0}, exit code: {1}", (end - start).ToString (TEST_TIME_FORMAT), p.ExitCode);
+                                               if (verbose)
+                                                       output.Write ("failed, time: {0}, exit code: {1}", (end - start).ToString (TEST_TIME_FORMAT), p.ExitCode);
                                        } else {
                                                var end = DateTime.UtcNow;
 
@@ -277,13 +399,15 @@ public class TestRunner
                                                        passed.Add (data);
                                                }
 
-                                               output.Write ("passed, time: {0}", (end - start).ToString (TEST_TIME_FORMAT));
+                                               if (verbose)
+                                                       output.Write ("passed, time: {0}", (end - start).ToString (TEST_TIME_FORMAT));
                                        }
 
                                        p.Close ();
 
                                        lock (monitor) {
-                                               Console.WriteLine (output.ToString ());
+                                               if (verbose)
+                                                       Console.WriteLine (output.ToString ());
                                        }
                                }
                        });
@@ -305,7 +429,9 @@ public class TestRunner
                XmlWriterSettings xmlWriterSettings = new XmlWriterSettings ();
                xmlWriterSettings.NewLineOnAttributes = true;
                xmlWriterSettings.Indent = true;
-               using (XmlWriter writer = XmlWriter.Create (String.Format ("TestResult-{0}.xml", testsuiteName), xmlWriterSettings)) {
+
+               string xmlPath = String.Format ("TestResult-{0}.xml", testsuiteName);
+               using (XmlWriter writer = XmlWriter.Create (xmlPath, xmlWriterSettings)) {
                        // <?xml version="1.0" encoding="utf-8" standalone="no"?>
                        writer.WriteStartDocument ();
                        // <!--This file represents the results of running a test suite-->
@@ -380,10 +506,10 @@ public class TestRunner
                                writer.WriteAttributeString ("asserts", "1");
                                writer.WriteStartElement ("failure");
                                writer.WriteStartElement ("message");
-                               writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
+                               writer.WriteCData (FilterInvalidXmlChars (pd.stdout.ToString ()));
                                writer.WriteEndElement ();
                                writer.WriteStartElement ("stack-trace");
-                               writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
+                               writer.WriteCData (FilterInvalidXmlChars (pd.stderr.ToString ()));
                                writer.WriteEndElement ();
                                writer.WriteEndElement ();
                                writer.WriteEndElement ();
@@ -399,10 +525,10 @@ public class TestRunner
                                writer.WriteAttributeString ("asserts", "1");
                                writer.WriteStartElement ("failure");
                                writer.WriteStartElement ("message");
-                               writer.WriteCData (DumpPseudoTrace (pd.stdoutFile));
+                               writer.WriteCData (FilterInvalidXmlChars (pd.stdout.ToString ()));
                                writer.WriteEndElement ();
                                writer.WriteStartElement ("stack-trace");
-                               writer.WriteCData (DumpPseudoTrace (pd.stderrFile));
+                               writer.WriteCData (FilterInvalidXmlChars (pd.stderr.ToString ()));
                                writer.WriteEndElement ();
                                writer.WriteEndElement ();
                                writer.WriteEndElement ();
@@ -422,14 +548,29 @@ public class TestRunner
                        // </test-results>
                        writer.WriteEndElement ();
                        writer.WriteEndDocument ();
+
+                       string babysitterXmlList = Environment.GetEnvironmentVariable("MONO_BABYSITTER_NUNIT_XML_LIST_FILE");
+                       if (!String.IsNullOrEmpty(babysitterXmlList)) {
+                               try {
+                                       string fullXmlPath = Path.GetFullPath(xmlPath);
+                                       File.AppendAllText(babysitterXmlList, fullXmlPath + Environment.NewLine);
+                               } catch (Exception e) {
+                                       Console.WriteLine("Attempted to record XML path to file {0} but failed.", babysitterXmlList);
+                               }
+                       }
                }
 
-               Console.WriteLine ();
-               Console.WriteLine ("Time: {0}", test_time.ToString (TEST_TIME_FORMAT));
-               Console.WriteLine ();
-               Console.WriteLine ("{0,4} test(s) passed", npassed);
-               Console.WriteLine ("{0,4} test(s) failed", nfailed);
-               Console.WriteLine ("{0,4} test(s) timed out", ntimedout);
+               if (verbose) {
+                       Console.WriteLine ();
+                       Console.WriteLine ("Time: {0}", test_time.ToString (TEST_TIME_FORMAT));
+                       Console.WriteLine ();
+                       Console.WriteLine ("{0,4} test(s) passed", npassed);
+                       Console.WriteLine ("{0,4} test(s) failed", nfailed);
+                       Console.WriteLine ("{0,4} test(s) timed out", ntimedout);
+               } else {
+                       Console.WriteLine ();
+                       Console.WriteLine (String.Format ("{0} test(s) passed, {1} test(s) did not pass.", npassed, nfailed));
+               }
 
                if (nfailed > 0) {
                        Console.WriteLine ();
@@ -437,8 +578,8 @@ public class TestRunner
                        foreach (ProcessData pd in failed) {
                                Console.WriteLine ();
                                Console.WriteLine (pd.test);
-                               DumpFile (pd.stdoutFile);
-                               DumpFile (pd.stderrFile);
+                               DumpFile (pd.stdoutName, pd.stdout.ToString ());
+                               DumpFile (pd.stderrName, pd.stderr.ToString ());
                        }
                }
 
@@ -448,27 +589,18 @@ public class TestRunner
                        foreach (ProcessData pd in timedout) {
                                Console.WriteLine ();
                                Console.WriteLine (pd.test);
-                               DumpFile (pd.stdoutFile);
-                               DumpFile (pd.stderrFile);
+                               DumpFile (pd.stdoutName, pd.stdout.ToString ());
+                               DumpFile (pd.stderrName, pd.stderr.ToString ());
                        }
                }
 
                return (ntimedout == 0 && nfailed == 0) ? 0 : 1;
        }
        
-       static void DumpFile (string filename) {
-               if (File.Exists (filename)) {
-                       Console.WriteLine ("=============== {0} ===============", filename);
-                       Console.WriteLine (File.ReadAllText (filename));
-                       Console.WriteLine ("=============== EOF ===============");
-               }
-       }
-
-       static string DumpPseudoTrace (string filename) {
-               if (File.Exists (filename))
-                       return FilterInvalidXmlChars (File.ReadAllText (filename));
-               else
-                       return string.Empty;
+       static void DumpFile (string filename, string text) {
+               Console.WriteLine ("=============== {0} ===============", filename);
+               Console.WriteLine (text);
+               Console.WriteLine ("=============== EOF ===============");
        }
 
        static string FilterInvalidXmlChars (string text) {
@@ -476,4 +608,119 @@ public class TestRunner
                // Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]      /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
                return Regex.Replace (text, @"[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]", "");
        }
+
+       static void TryThreadDump (int pid, ProcessData data)
+       {
+               try {
+                       TryGDB (pid, data);
+                       return;
+               } catch {
+               }
+
+#if !FULL_AOT_DESKTOP && !MOBILE
+               /* LLDB cannot produce managed stacktraces for all the threads */
+               try {
+                       Syscall.kill (pid, Signum.SIGQUIT);
+                       Thread.Sleep (1000);
+               } catch {
+               }
+#endif
+
+               try {
+                       TryLLDB (pid, data);
+                       return;
+               } catch {
+               }
+       }
+
+       static void TryLLDB (int pid, ProcessData data)
+       {
+               string filename = Path.GetTempFileName ();
+
+               using (StreamWriter sw = new StreamWriter (new FileStream (filename, FileMode.Open, FileAccess.Write)))
+               {
+                       sw.WriteLine ("process attach --pid " + pid);
+                       sw.WriteLine ("thread list");
+                       sw.WriteLine ("thread backtrace all");
+                       sw.WriteLine ("detach");
+                       sw.WriteLine ("quit");
+                       sw.Flush ();
+
+                       ProcessStartInfo psi = new ProcessStartInfo {
+                               FileName = "lldb",
+                               Arguments = "--batch --source \"" + filename + "\" --no-lldbinit",
+                               UseShellExecute = false,
+                               RedirectStandardError = true,
+                               RedirectStandardOutput = true,
+                       };
+
+                       using (Process process = new Process { StartInfo = psi })
+                       {
+                               process.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
+                                       lock (data.stdoutLock) {
+                                               if (e.Data != null)
+                                                       data.stdout.AppendLine (e.Data);
+                                       }
+                               };
+
+                               process.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
+                                       lock (data.stderrLock) {
+                                               if (e.Data != null)
+                                                       data.stderr.AppendLine (e.Data);
+                                       }
+                               };
+
+                               process.Start ();
+                               process.BeginOutputReadLine ();
+                               process.BeginErrorReadLine ();
+                               if (!process.WaitForExit (60 * 1000))
+                                       process.Kill ();
+                       }
+               }
+       }
+
+       static void TryGDB (int pid, ProcessData data)
+       {
+               string filename = Path.GetTempFileName ();
+
+               using (StreamWriter sw = new StreamWriter (new FileStream (filename, FileMode.Open, FileAccess.Write)))
+               {
+                       sw.WriteLine ("attach " + pid);
+                       sw.WriteLine ("info threads");
+                       sw.WriteLine ("thread apply all p mono_print_thread_dump(0)");
+                       sw.WriteLine ("thread apply all backtrace");
+                       sw.Flush ();
+
+                       ProcessStartInfo psi = new ProcessStartInfo {
+                               FileName = "gdb",
+                               Arguments = "-batch -x \"" + filename + "\" -nx",
+                               UseShellExecute = false,
+                               RedirectStandardError = true,
+                               RedirectStandardOutput = true,
+                       };
+
+                       using (Process process = new Process { StartInfo = psi })
+                       {
+                               process.OutputDataReceived += delegate (object sender, DataReceivedEventArgs e) {
+                                       lock (data.stdoutLock) {
+                                               if (e.Data != null)
+                                                       data.stdout.AppendLine (e.Data);
+                                       }
+                               };
+
+                               process.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs e) {
+                                       lock (data.stderrLock) {
+                                               if (e.Data != null)
+                                                       data.stderr.AppendLine (e.Data);
+                                       }
+                               };
+
+                               process.Start ();
+                               process.BeginOutputReadLine ();
+                               process.BeginErrorReadLine ();
+                               if (!process.WaitForExit (60 * 1000))
+                                       process.Kill ();
+                       }
+               }
+       }
 }