Bug 540222 - All test projects should dump stack traces on timeout

When running tests with org.eclipse.test.EclipseTestRunner during
integration builds, stack traces and screenshots are provided if the
build times out (e.g. due to a hang in the tests). When running test
projects with maven however (e.g. when jdt.debug.tests run in a Gerrit
job), no such stack traces or screenshots are present. Debugging
sporadic hangs in Gerrit jobs is therefore very difficult.

This change is a part of moving the dump-on-timeout functionality to a
location which is accessible to test applications, namely in
org.eclipse.e4.ui.workbench3 (along with
org.eclipse.ui.testing.TestableObject).

Change-Id: I301883c95a7fe68ae8102401b6ec6cb4e9634e06
Signed-off-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/bundles/org.eclipse.test/META-INF/MANIFEST.MF b/bundles/org.eclipse.test/META-INF/MANIFEST.MF
index 679aa63..41b9562 100644
--- a/bundles/org.eclipse.test/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.test/META-INF/MANIFEST.MF
@@ -2,12 +2,12 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.test; singleton:=true
-Bundle-Version: 3.4.300.qualifier
+Bundle-Version: 3.4.400.qualifier
 Eclipse-BundleShape: dir
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Require-Bundle: org.apache.ant,
- org.eclipse.ui,
+ org.eclipse.ui;bundle-version="[3.112.0,4.0.0)",
  org.eclipse.core.runtime,
  org.eclipse.ui.ide.application,
  org.eclipse.equinox.app,
diff --git a/bundles/org.eclipse.test/pom.xml b/bundles/org.eclipse.test/pom.xml
index 998746b..6b48bdc 100644
--- a/bundles/org.eclipse.test/pom.xml
+++ b/bundles/org.eclipse.test/pom.xml
@@ -19,7 +19,7 @@
   </parent>
   <groupId>org.eclipse.test</groupId>
   <artifactId>org.eclipse.test</artifactId>
-  <version>3.4.300-SNAPSHOT</version>
+  <version>3.4.400-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
   <properties>
     <defaultSigning-excludeInnerJars>true</defaultSigning-excludeInnerJars> 
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java b/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
index 30d07ae..c300d2d 100644
--- a/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
@@ -17,51 +17,25 @@
 
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
 
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.PrintStream;
-import java.lang.reflect.Method;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Date;
 import java.util.Enumeration;
 import java.util.Hashtable;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Properties;
-import java.util.Timer;
-import java.util.TimerTask;
 
 import org.apache.tools.ant.Project;
 import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestExecutionContext;
 import org.eclipse.core.runtime.IPath;
-import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Path;
 import org.eclipse.core.runtime.Platform;
-import org.eclipse.core.runtime.Status;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.SWTException;
-import org.eclipse.swt.graphics.GC;
-import org.eclipse.swt.graphics.Image;
-import org.eclipse.swt.graphics.ImageData;
-import org.eclipse.swt.graphics.ImageLoader;
-import org.eclipse.swt.widgets.Control;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.testing.dumps.TimeoutDumpTimer;
 import org.junit.platform.engine.TestExecutionResult;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -90,29 +64,6 @@
 		}
 	}
 
-	static class StreamForwarder extends Thread {
-		private InputStream fProcessOutput;
-
-		private PrintStream fStream;
-
-		public StreamForwarder(InputStream processOutput, PrintStream stream) {
-			fProcessOutput = processOutput;
-			fStream = stream;
-		}
-
-		@Override
-		public void run() {
-			try (BufferedReader reader = new BufferedReader(new InputStreamReader(fProcessOutput))) {
-				String line = null;
-				while ((line = reader.readLine()) != null) {
-					fStream.println(line);
-				}
-			} catch (IOException e) {
-				e.printStackTrace();
-			}
-		}
-	}
-
 	/**
 	 * No problems with this test.
 	 */
@@ -127,22 +78,6 @@
 	public static final int ERRORS = 2;
 
 	/**
-	 * SECONDS_BEFORE_TIMEOUT_BUFFER is the time we allow ourselves to take stack
-	 * traces, get a screen shot, delay "SECONDS_BETWEEN_DUMPS", then do it again.
-	 * On current build machine, it takes about 30 seconds to do all that, so 2
-	 * minutes should be suffiencient time allowed for most machines. Though, should
-	 * increase, say, if we increase the "time beteen dumps" to a minute or more.
-	 */
-	private static final int SECONDS_BEFORE_TIMEOUT_BUFFER = 120;
-
-	/**
-	 * SECONDS_BETWEEN_DUMPS is the time we wait from first to second dump of stack
-	 * trace and screenshots. In most cases, this should suffice to determine if
-	 * still busy doing something, or, hung, or waiting for user input.
-	 */
-	private static final int SECONDS_BETWEEN_DUMPS = 5;
-
-	/**
 	 * The main entry point (the parameters are not yet consistent with the Ant
 	 * JUnitTestRunner, but eventually they should be). Parameters
 	 *
@@ -241,7 +176,7 @@
 			}
 			System.err.println("INFO: timeoutScreenOutputDir: " + timeoutScreenOutputDir);
 			System.err.println("INFO: timeout: " + timeoutString);
-			startStackDumpTimeoutTimer(timeoutString, new File(timeoutScreenOutputDir), className);
+			startStackDumpTimeoutTimer(timeoutString, new File(timeoutScreenOutputDir));
 		}
 
 		if (testPluginsNames != null && classesNames != null) {
@@ -389,241 +324,8 @@
 	 *
 	 * @param timeoutArg      the -timeout argument from the command line
 	 * @param outputDirectory where the test results end up
-	 * @param classname       the class that is running the tests suite
 	 */
-	private static void startStackDumpTimeoutTimer(final String timeoutArg, final File outputDirectory,
-			final String classname) {
-		try {
-			/*
-			 * The delay (in ms) is the sum of - the expected time it took for launching the
-			 * current VM and reaching this method - the time it will take to run the
-			 * garbage collection and dump all the infos (twice)
-			 */
-			int delay = SECONDS_BEFORE_TIMEOUT_BUFFER * 1000;
-
-			int timeout = Integer.parseInt(timeoutArg) - delay;
-			String time0 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(new Date());
-			System.err.println("starting EclipseTestRunner Timer with timeout=" + timeout + " at " + time0);
-			if (timeout > 0) {
-				new Timer("EclipseTestRunner Timer", true).schedule(new TimerTask() {
-
-					volatile boolean assumeUiThreadIsResponsive;
-
-					@Override
-					public void run() {
-						assumeUiThreadIsResponsive = true;
-						dump(0);
-						try {
-							Thread.sleep(SECONDS_BETWEEN_DUMPS * 1000);
-						} catch (InterruptedException e) {
-							// continue
-						}
-						dump(SECONDS_BETWEEN_DUMPS);
-					}
-
-					/**
-					 *
-					 * @param num num is purely a lable used in naming the screen capture files. By
-					 *            convention, we pass in 0 or "SECONDS_BETWEEN_DUMPS" just as a
-					 *            subtle reminder of how much time as elapsed. Thus, files end up
-					 *            with names similar to <classname>_screen0.png,
-					 *            <classname>_screem5.png in a directory named "timeoutScreens"
-					 *            under "results", such as
-					 *            .../results/linux.gtk.x86_64/timeoutScreens/
-					 */
-					private void dump(final int num) {
-						// Time elapsed time to do each dump, so we'll
-						// know if/when we get too close to the 2
-						// minutes we allow
-						long start = System.currentTimeMillis();
-
-						// Dump all stacks:
-						dumpStackTraces(num, System.err);
-						dumpStackTraces(num, System.out); // System.err could be blocked, see
-						// https://bugs.eclipse.org/506304
-						logStackTraces(num);              // make this available in the log, see bug 533367
-
-						if (!dumpSwtDisplay(num)) {
-							String screenshotFile = getScreenshotFile(num);
-							dumpAwtScreenshot(screenshotFile);
-						}
-
-						// Elapsed time in milliseconds
-						long elapsedTimeMillis = System.currentTimeMillis() - start;
-
-						// Print in seconds
-						float elapsedTimeSec = elapsedTimeMillis / 1000F;
-						System.err.println("INFO: Seconds to do dump " + num + ": " + elapsedTimeSec);
-					}
-
-					private void logStackTraces(int num) {
-						ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-						dumpStackTraces(num, new PrintStream(outputStream));
-						String symbolicName = "org.eclipse.test";
-						IStatus warningStatus = new Status(IStatus.WARNING, symbolicName, outputStream.toString());
-						Platform.getLog(Platform.getBundle(symbolicName)).log(warningStatus);
-					}
-
-					private void dumpStackTraces(int num, PrintStream out) {
-						out.println("EclipseTestRunner almost reached timeout '" + timeoutArg + "'.");
-						out.println("totalMemory:            " + Runtime.getRuntime().totalMemory());
-						out.println("freeMemory (before GC): " + Runtime.getRuntime().freeMemory());
-						out.flush(); // bug 420258: flush aggressively, we could be low on memory
-						System.gc();
-						out.println("freeMemory (after GC):  " + Runtime.getRuntime().freeMemory());
-						String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US).format(new Date());
-						out.println("Thread dump " + num + " at " + time + ":");
-						out.flush();
-						Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
-						for (Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet()) {
-							String name = entry.getKey().getName();
-							StackTraceElement[] stack = entry.getValue();
-							ThreadDump exception = new ThreadDump("for thread \"" + name + "\"");
-							exception.setStackTrace(stack);
-							exception.printStackTrace(out);
-						}
-						out.flush();
-					}
-
-					String getScreenshotFile(final int num) {
-						if (!outputDirectory.exists()) {
-							outputDirectory.mkdirs();
-						}
-						String filename = outputDirectory.getAbsolutePath() + "/" + classname + "_screen" + num
-								+ ".png";
-						return filename;
-					}
-
-					private boolean dumpSwtDisplay(final int num) {
-						try {
-							final Display display = Display.getDefault();
-
-							if (!assumeUiThreadIsResponsive) {
-								String message = "trying to make UI thread respond";
-								IllegalStateException toThrow = new IllegalStateException(message);
-								Thread t = display.getThread();
-								// Initialize the cause. Its stack trace will be that of the current thread.
-								toThrow.initCause(new RuntimeException(message));
-								// Set the stack trace to that of the target thread.
-								toThrow.setStackTrace(t.getStackTrace());
-								// Stop the thread using the specified throwable.
-								// Thread#stop(Throwable) doesn't work any more in JDK 8 and is removed in Java 11 so it's not gonna be tried at all. Try stop0:
-								try {
-									Method stop0 = Thread.class.getDeclaredMethod("stop0", Object.class);
-									stop0.setAccessible(true);
-									stop0.invoke(t, toThrow);
-								} catch (Exception e1) {
-									e1.printStackTrace();
-								}
-							}
-
-							assumeUiThreadIsResponsive = false;
-
-							display.asyncExec(new Runnable() {
-								@Override
-								public void run() {
-									assumeUiThreadIsResponsive = true;
-
-									dumpDisplayState(System.err);
-									dumpDisplayState(System.out); // System.err could be blocked, see
-									// https://bugs.eclipse.org/506304
-
-									// Take a screenshot:
-									GC gc = new GC(display);
-									final Image image = new Image(display, display.getBounds());
-									gc.copyArea(image, 0, 0);
-									gc.dispose();
-
-									ImageLoader loader = new ImageLoader();
-									loader.data = new ImageData[] { image.getImageData() };
-									String filename = getScreenshotFile(num);
-									loader.save(filename, SWT.IMAGE_PNG);
-									System.err.println("Screenshot saved to: " + filename);
-									System.out.println("Screenshot saved to: " + filename);
-									image.dispose();
-								}
-
-								private void dumpDisplayState(PrintStream out) {
-									// Dump focus control, parents, and
-									// shells:
-									Control focusControl = display.getFocusControl();
-									if (focusControl != null) {
-										out.println("FocusControl: ");
-										StringBuilder indent = new StringBuilder("  ");
-										do {
-											out.println(indent.toString() + focusControl);
-											focusControl = focusControl.getParent();
-											indent.append("  ");
-										} while (focusControl != null);
-									}
-									Shell[] shells = display.getShells();
-									if (shells.length > 0) {
-										out.println("Shells: ");
-										for (Shell shell : shells) {
-											out.println((shell.isVisible() ? "  visible: " : "  invisible: ") + shell);
-										}
-									}
-									out.flush(); // for bug 420258
-								}
-							});
-							return true;
-						} catch (SWTException e) {
-							e.printStackTrace();
-							return false;
-						}
-					}
-
-				}, timeout);
-			} else {
-				System.err.println("EclipseTestRunner argument error: '-timeout " + timeoutArg
-						+ "' was too short to accommodate time delay required (" + delay + ").");
-			}
-		} catch (NumberFormatException e) {
-			e.printStackTrace();
-		}
-	}
-
-	public static void dumpAwtScreenshot(String screenshotFile) {
-		try {
-			URL location = AwtScreenshot.class.getProtectionDomain().getCodeSource().getLocation();
-			String cp = location.toURI().getPath();
-			String javaHome = System.getProperty("java.home");
-			String javaExe = javaHome + File.separatorChar + "bin" + File.separatorChar + "java";
-			if (File.separatorChar == '\\') {
-				javaExe += ".exe"; // assume it's Windows
-			}
-			String[] args = new String[] { javaExe, "-cp", cp, AwtScreenshot.class.getName(), screenshotFile };
-			System.err.println("Start process: " + Arrays.asList(args));
-			ProcessBuilder processBuilder = new ProcessBuilder(args);
-			if ("Mac OS X".equals(System.getProperty("os.name"))) {
-				processBuilder.environment().put("AWT_TOOLKIT", "CToolkit");
-			}
-			Process process = processBuilder.start();
-			new StreamForwarder(process.getErrorStream(), System.err).start();
-			new StreamForwarder(process.getInputStream(), System.err).start();
-			int screenshotTimeout = 15;
-			long end = System.currentTimeMillis() + screenshotTimeout * 1000;
-			boolean done = false;
-			do {
-				try {
-					process.exitValue();
-					done = true;
-				} catch (IllegalThreadStateException e) {
-					try {
-						Thread.sleep(100);
-					} catch (InterruptedException e1) {
-					}
-				}
-			} while (!done && System.currentTimeMillis() < end);
-
-			if (done) {
-				System.err.println("AwtScreenshot VM finished with exit code " + process.exitValue() + ".");
-			} else {
-				process.destroy();
-				System.err.println("Killed AwtScreenshot VM after " + screenshotTimeout + " seconds.");
-			}
-		} catch (URISyntaxException | IOException e) {
-			e.printStackTrace();
-		}
+	private static void startStackDumpTimeoutTimer(final String timeoutArg, final File outputDirectory) {
+		TimeoutDumpTimer.startTimeoutDumpTimer(timeoutArg, outputDirectory);
 	}
 }