All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.carrotsearch.ant.tasks.junit4.slave.SlaveMain Maven / Gradle / Ivy

package com.carrotsearch.ant.tasks.junit4.slave;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import com.carrotsearch.randomizedtesting.RandomizedRunner;
import org.junit.runner.Description;
import org.junit.runner.Request;
import org.junit.runner.Result;
import org.junit.runner.Runner;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;

import com.carrotsearch.ant.tasks.junit4.events.AppendStdErrEvent;
import com.carrotsearch.ant.tasks.junit4.events.AppendStdOutEvent;
import com.carrotsearch.ant.tasks.junit4.events.BootstrapEvent;
import com.carrotsearch.ant.tasks.junit4.events.Serializer;
import com.carrotsearch.ant.tasks.junit4.events.SuiteFailureEvent;
import com.carrotsearch.randomizedtesting.MethodGlobFilter;
import com.carrotsearch.randomizedtesting.SysGlobals;
import com.carrotsearch.randomizedtesting.annotations.SuppressForbidden;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;

/**
 * A slave process running the actual tests on the target JVM.
 */
public class SlaveMain {
  /** Runtime exception. */
  public static final int ERR_EXCEPTION = 240;

  /** No JUnit on classpath. */
  public static final int ERR_NO_JUNIT = 239;

  /** Old JUnit on classpath. */
  public static final int ERR_OLD_JUNIT = 238;

  /** OOM */
  public static final int ERR_OOM = 237;

  /**
   * Last resort memory pool released under low memory conditions.
   * This is not a solution, it's a terrible hack. I know this. Everyone knows this.
   * Even monkeys in Madagaskar know this. If you know a better solution, patches
   * welcome. 
   * 
   * 

Approximately 5mb is reserved. Really, smaller values don't make any difference * and the JVM fails to even return the status passed to Runtime.halt(). */ static volatile Object lastResortMemory = new byte [1024 * 1024 * 5]; /** * Preallocate and load in advance. */ static Class oomClass = OutOfMemoryError.class; /** * Frequent event strean flushing. */ public static final String OPTION_FREQUENT_FLUSH = "-flush"; /** * Multiplex sysout and syserr to original streams (aside from * pumping them to event stream). */ public static final String OPTION_SYSOUTS = "-sysouts"; /** * Read class names from standard input. */ public static final String OPTION_STDIN = "-stdin"; /** * Name the sink for events. If given, accepts one argument - name of a file * to which events should be dumped. The file has to be initially empty! */ public static final String OPTION_EVENTSFILE = "-eventsfile"; /** * Should the debug stream from the runner be created? It's named after the events file * with .debug suffix. */ public static final String OPTION_DEBUGSTREAM = "-debug"; /** * User-defined RunListener classes. */ public static final String OPTION_RUN_LISTENERS = "-runListeners"; /** * Fire a runner failure after startup to verify messages * are propagated properly. Not really useful in practice... */ public static final String SYSPROP_FIRERUNNERFAILURE = SlaveMain.class.getName() + ".fireRunnerFailure"; /** * Delay the initial bootstrap event from the forked JVM * (used in tests). */ public static final String SYSPROP_FORKEDJVM_DELAY_MS = "junit4.tests.internal.initialDelayMs"; /** * Event sink. */ private final Serializer serializer; /** A sink for warnings (non-event stream). */ private static PrintStream warnings; /** Flush serialization stream frequently. */ private boolean flushFrequently = false; /** Debug stream to flush progress information to. */ private File debugMessagesFile; /** List of RunListener classes */ private String runListeners; /** * Multiplex calls to System streams to both event stream * and the original streams? */ private static boolean multiplexStdStreams = false; /** * Base for redirected streams. */ private static class ChunkedStream extends OutputStream { public void write(int b) throws IOException { throw new IOException("Only buffered write(byte[],int,int) calls expected from super stream."); } @Override public void close() throws IOException { throw new IOException("Not supposed to be called on redirected streams."); } } /** * Creates a slave emitting events to the given serializer. */ public SlaveMain(Serializer serializer) { this.serializer = serializer; } /** * Execute tests. */ private void execute(Iterator classNames) throws Throwable { final RunNotifier fNotifier = new OrderedRunNotifier(); final Result result = new Result(); final Writer debug = debugMessagesFile == null ? new NullWriter() : new OutputStreamWriter(new FileOutputStream(debugMessagesFile), "UTF-8"); fNotifier.addListener(result.createListener()); fNotifier.addListener( new StreamFlusherDecorator( new NoExceptionRunListenerDecorator(new RunListenerEmitter(serializer)) { @Override protected void exception(Throwable t) { warn("Event serializer exception.", t); } })); fNotifier.addListener(new RunListener() { public void testRunFinished(Result result) throws Exception { debug(debug, "testRunFinished(T:" + result.getRunCount() + ";F:" + result.getFailureCount() + ";I:" + result.getIgnoreCount() + ")"); serializer.flush(); } @Override public void testRunStarted(Description description) throws Exception { debug(debug, "testRunStarted(" + description + ")"); serializer.flush(); } @Override public void testStarted(Description description) throws Exception { debug(debug, "testStarted(" + description + ")"); serializer.flush(); } public void testFinished(Description description) throws Exception { debug(debug, "testFinished(" + description + ")"); serializer.flush(); } @Override public void testIgnored(Description description) throws Exception { debug(debug, "testIgnored(T:" + description + ")"); } @Override public void testFailure(Failure failure) throws Exception { debug(debug, "testFailure(T:" + failure + ")"); } @Override public void testAssumptionFailure(Failure failure) { try { debug(debug, "testAssumptionFailure(T:" + failure + ")"); } catch (IOException e) { throw new RuntimeException(e); } } }); /* * Instantiate method filter if any. */ String methodFilterGlob = Strings.emptyToNull(System.getProperty(SysGlobals.SYSPROP_TESTMETHOD())); Filter methodFilter = Filter.ALL; if (methodFilterGlob != null) { methodFilter = new MethodGlobFilter(methodFilterGlob); } /* * Important. Run each class separately so that we get separate * {@link RunListener} callbacks for the top extracted description. */ debug(debug, "Entering main suite loop."); try { while (classNames.hasNext()) { final String clName = classNames.next(); debug(debug, "Instantiating: " + clName); Class clazz = instantiate(clName); if (clazz == null) continue; Request request = Request.aClass(clazz); try { Runner runner = request.getRunner(); methodFilter.apply(runner); // New RunListener instances should be added per class and then removed from the RunNotifier ArrayList runListenerInstances = instantiateRunListeners(); for (RunListener runListener : runListenerInstances) { fNotifier.addListener(runListener); } fNotifier.fireTestRunStarted(runner.getDescription()); debug(debug, "Runner.run(" + clName + ")"); runner.run(fNotifier); debug(debug, "Runner.done(" + clName + ")"); fNotifier.fireTestRunFinished(result); for (RunListener runListener : runListenerInstances) { fNotifier.removeListener(runListener); } } catch (NoTestsRemainException e) { // Don't complain if all methods have been filtered out. // I don't understand the reason why this exception has been // built in to filters at all. } } } catch (Throwable t) { debug(debug, "Main suite loop error: " + t); throw t; } finally { debug(debug, "Leaving main suite loop."); debug.close(); } } private void debug(Writer w, String msg) throws IOException { w.write(msg); w.write("\n"); w.flush(); } /** * Instantiate test classes (or try to). */ private Class instantiate(String className) { try { return Class.forName(className, false, Thread.currentThread().getContextClassLoader()); } catch (Throwable t) { try { serializer.serialize( new SuiteFailureEvent( new Failure(Description.createSuiteDescription(className), t))); if (flushFrequently) serializer.flush(); } catch (Exception e) { warn("Could not report failure back to master.", t); } return null; } } /** * Console entry point. */ @SuppressWarnings("resource") public static void main(String[] allArgs) { int exitStatus = 0; Serializer serializer = null; try { final ArrayDeque args = new ArrayDeque(Arrays.asList(allArgs)); // Options. boolean debugStream = false; boolean flushFrequently = false; File eventsFile = null; boolean suitesOnStdin = false; List testClasses = new ArrayList<>(); String runListeners = null; while (!args.isEmpty()) { String option = args.pop(); if (option.equals(OPTION_FREQUENT_FLUSH)) { flushFrequently = true; } else if (option.equals(OPTION_STDIN)) { suitesOnStdin = true; } else if (option.equals(OPTION_SYSOUTS)) { multiplexStdStreams = true; } else if (option.equals(OPTION_EVENTSFILE)) { eventsFile = new File(args.pop()); if (eventsFile.isFile() && eventsFile.length() > 0) { RandomAccessFile raf = new RandomAccessFile(eventsFile, "rw"); raf.setLength(0); raf.close(); } } else if (option.equals(OPTION_RUN_LISTENERS)) { runListeners = args.pop(); } else if (option.startsWith(OPTION_DEBUGSTREAM)) { debugStream = true; } else if (option.startsWith("@")) { // Append arguments file, one line per option. args.addAll(Arrays.asList(readArgsFile(option.substring(1)))); } else { // The default expectation is a test class. testClasses.add(option); } } // Set up events channel and events serializer. if (eventsFile == null) { throw new IOException("You must specify communication channel for events."); } // Delay the forked JVM a bit (for tests). if (System.getProperty(SYSPROP_FORKEDJVM_DELAY_MS) != null) { Thread.sleep(Integer.parseInt(System.getProperty(SYSPROP_FORKEDJVM_DELAY_MS))); } // Send bootstrap package. serializer = new Serializer(new EventsOutputStream(eventsFile)) .serialize(new BootstrapEvent()) .flush(); // Redirect original streams and start running tests. redirectStreams(serializer, flushFrequently); final SlaveMain main = new SlaveMain(serializer); main.flushFrequently = flushFrequently; main.debugMessagesFile = debugStream ? new File(eventsFile.getAbsolutePath() + ".debug"): null; main.runListeners = runListeners; final Iterator stdInput; if (suitesOnStdin) { stdInput = new StdInLineIterator(main.serializer); } else { stdInput = Collections.emptyList().iterator(); } main.execute(Iterators.concat(testClasses.iterator(), stdInput)); // For unhandled exceptions tests. if (System.getProperty(SYSPROP_FIRERUNNERFAILURE) != null) { throw new Exception(System.getProperty(SYSPROP_FIRERUNNERFAILURE)); } } catch (Throwable t) { lastResortMemory = null; tryWaitingForGC(); if (t.getClass() == oomClass) { exitStatus = ERR_OOM; warn("JVM out of memory.", t); } else { exitStatus = ERR_EXCEPTION; warn("Exception at main loop level.", t); } } try { if (serializer != null) { try { serializer.close(); } catch (Throwable t) { warn("Exception closing serializer.", t); } } } finally { JvmExit.halt(exitStatus); } } /** * Try waiting for a GC to happen. This is a dirty heuristic but if we're * here we're neck deep in sh*t anyway (OOMs all over). */ private static void tryWaitingForGC() { // We could try to preallocate memory mx bean and count collections... // there is no guarantee it doesn't allocate stuff too though. final long timeout = System.currentTimeMillis() + 2000; while (System.currentTimeMillis() < timeout) { System.gc(); try { Thread.sleep(250); } catch (InterruptedException e) { break; } } } /** * Read arguments from a file. Newline delimited, UTF-8 encoded. No fanciness to * avoid dependencies. */ private static String[] readArgsFile(String argsFile) throws IOException { final ArrayList lines = new ArrayList(); final BufferedReader reader = new BufferedReader( new InputStreamReader( new FileInputStream(argsFile), "UTF-8")); try { String line; while ((line = reader.readLine()) != null) { line = line.trim(); if (!line.isEmpty() && !line.startsWith("#")) { lines.add(line); } } } finally { reader.close(); } return lines.toArray(new String [lines.size()]); } /** * Redirect standard streams so that the output can be passed to listeners. */ @SuppressForbidden("legitimate sysstreams.") private static void redirectStreams(final Serializer serializer, final boolean flushFrequently) { final PrintStream origSysOut = System.out; final PrintStream origSysErr = System.err; // Set warnings stream to System.err. warnings = System.err; AccessController.doPrivileged(new PrivilegedAction() { @SuppressForbidden("legitimate PrintStream with default charset.") @Override public Void run() { System.setOut(new PrintStream(new BufferedOutputStream(new ChunkedStream() { @Override public void write(byte[] b, int off, int len) throws IOException { if (multiplexStdStreams) { origSysOut.write(b, off, len); } serializer.serialize(new AppendStdOutEvent(b, off, len)); if (flushFrequently) serializer.flush(); } }))); System.setErr(new PrintStream(new BufferedOutputStream(new ChunkedStream() { @Override public void write(byte[] b, int off, int len) throws IOException { if (multiplexStdStreams) { origSysErr.write(b, off, len); } serializer.serialize(new AppendStdErrEvent(b, off, len)); if (flushFrequently) serializer.flush(); } }))); return null; } }); } /** * Warning emitter. Uses whatever alternative non-event communication channel is. */ @SuppressForbidden("legitimate sysstreams.") public static void warn(String message, Throwable t) { PrintStream w = (warnings == null ? System.err : warnings); try { w.print("WARN: "); w.print(message); if (t != null) { w.print(" -> "); try { t.printStackTrace(w); } catch (OutOfMemoryError e) { // Ignore, OOM. w.print(t.getClass().getName()); w.print(": "); w.print(t.getMessage()); w.println(" (stack unavailable; OOM)"); } } else { w.println(); } w.flush(); } catch (OutOfMemoryError t2) { w.println("ERROR: Couldn't even serialize a warning (out of memory)."); } catch (Throwable t2) { // Can't do anything, really. Probably an OOM? w.println("ERROR: Couldn't even serialize a warning."); } } /** * Generates JUnit 4 RunListener instances for any user defined RunListeners */ private ArrayList instantiateRunListeners() throws Exception { ArrayList instances = new ArrayList<>(); if (runListeners != null) { for (String className : Arrays.asList(runListeners.split(","))) { instances.add((RunListener) this.instantiate(className).newInstance()); } } return instances; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy