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

org.pantsbuild.tools.junit.impl.ConsoleRunnerImpl Maven / Gradle / Ivy

Go to download

A command line tool for running junit tests that provides functionality above and beyond that provided by org.junit.runner.JUnitCore.

The newest version!
// Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
// Licensed under the Apache License, Version 2.0 (see LICENSE).

package org.pantsbuild.tools.junit.impl;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.output.TeeOutputStream;
import org.junit.runner.Computer;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
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.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.kohsuke.args4j.spi.StringArrayOptionHandler;
import org.kohsuke.args4j.spi.StringOptionHandler;
import org.pantsbuild.args4j.InvalidCmdLineArgumentException;
import org.pantsbuild.args4j.StringArgumentsHandler;
import org.pantsbuild.junit.annotations.TestParallel;
import org.pantsbuild.junit.annotations.TestSerial;
import org.pantsbuild.tools.junit.impl.experimental.ConcurrentComputer;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * An alternative to {@link JUnitCore} with stream capture and junit-report xml output capabilities.
 */
public class ConsoleRunnerImpl {
  /** Should be set to false for unit testing via {@link #setCallSystemExitOnFinish} */
  private static boolean callSystemExitOnFinish = true;
  /** Intended to be used in unit testing this class */
  private static RunListener testListener = null;

  /**
   * A stream that allows its underlying output to be swapped.
   */
  static class SwappableStream extends FilterOutputStream {
    private final T original;

    SwappableStream(T out) {
      super(out);
      this.original = out;
    }

    void swap(OutputStream out) {
      this.out = out;
    }

    /**
     * Returns the original stream this swappable stream was created with.
     */
    public T getOriginal() {
      return original;
    }
  }

  /**
   * Holder for a tests stderr and stdout streams.
   */
  static class StreamCapture {
    private final File out;
    private OutputStream outstream;

    private final File err;
    private OutputStream errstream;

    private boolean closed;

    StreamCapture(File out, File err) {
      this.out = out;
      this.err = err;
    }

    OutputStream getOutputStream() throws FileNotFoundException {
      if (outstream == null) {
        outstream = new FileOutputStream(out);
      }
      return outstream;
    }

    OutputStream getErrorStream() throws FileNotFoundException {
      if (errstream == null) {
        errstream = new FileOutputStream(err);
      }
      return errstream;
    }

    void close() throws IOException {
      if (!closed) {
        if (outstream != null) {
          Closeables.close(outstream, /* swallowIOException */ true);
        }
        if (errstream != null) {
          Closeables.close(errstream, /* swallowIOException */ true);
        }
        closed = true;
      }
    }

    byte[] readOut() throws IOException {
      return read(out);
    }

    byte[] readErr() throws IOException {
      return read(err);
    }

    private byte[] read(File file) throws IOException {
      Preconditions.checkState(closed, "Capture must be closed by all users before it can be read");
      return Files.toByteArray(file);
    }
  }

  static class InMemoryStreamCapture {
    private ByteArrayOutputStream outstream;
    private ByteArrayOutputStream errstream;

    private boolean closed;

    OutputStream getOutputStream() {
      if (outstream == null) {
        outstream = new ByteArrayOutputStream();
      }
      return outstream;
    }

    OutputStream getErrorStream() {
      if (errstream == null) {
        errstream = new ByteArrayOutputStream();
      }
      return errstream;
    }

    void close() throws IOException {
      if (!closed) {
        if (outstream != null) {
          Closeables.close(outstream, /* swallowIOException */ true);
        }
        if (errstream != null) {
          Closeables.close(errstream, /* swallowIOException */ true);
        }
        closed = true;
      }
    }

    byte[] readOut() {
      return read(outstream);
    }

    byte[] readErr() {
      return read(errstream);
    }

    private byte[] read(ByteArrayOutputStream stream) {
      Preconditions.checkState(closed, "Capture must be closed by all users before it can be read");
      return stream.toByteArray();
    }
  }

  /**
   * A run listener that suiteCaptures the output and error streams for each test class
   * and makes the content of these available.
   */
  static class StreamCapturingListener extends RunListener implements StreamSource {
    private final Map, StreamCapture> suiteCaptures = Maps.newHashMap();
    private final Map caseCaptures = Maps.newHashMap();

    private final File outdir;
    private final OutputMode outputMode;
    private final SwappableStream swappableOut;
    private final SwappableStream swappableErr;

    StreamCapturingListener(File outdir, OutputMode outputMode,
        SwappableStream swappableOut,
        SwappableStream swappableErr) {
      this.outdir = outdir;
      this.outputMode = outputMode;
      this.swappableOut = swappableOut;
      this.swappableErr = swappableErr;
    }

    @Override
    public void testRunStarted(Description description) throws Exception {
      registerTests(description.getChildren());
      super.testRunStarted(description);
    }

    private void registerTests(Iterable tests) throws IOException {
      for (Description test : tests) {
        registerTests(test.getChildren());
        if (Util.isRunnable(test)) {
          StreamCapture suiteCapture = suiteCaptures.get(test.getTestClass());
          if (suiteCapture == null) {
            String prefix = test.getClassName();

            File out = new File(outdir, prefix + ".out.txt");
            Files.createParentDirs(out);

            File err = new File(outdir, prefix + ".err.txt");
            Files.createParentDirs(err);
            suiteCapture = new StreamCapture(out, err);
            suiteCaptures.put(test.getTestClass(), suiteCapture);
          }
        }
      }
    }

    @Override
    public void testRunFinished(Result result) throws Exception {
      swappableOut.swap(swappableOut.getOriginal());
      swappableErr.swap(swappableErr.getOriginal());
      for (StreamCapture capture : suiteCaptures.values()) {
        capture.close();
      }
      caseCaptures.clear();
      super.testRunFinished(result);
    }

    @Override
    public void testStarted(Description description) throws Exception {
      if (!Util.isRunnable(description)) {
        return;
      }
      StreamCapture suiteCapture = suiteCaptures.get(description.getTestClass());
      OutputStream suiteOut = suiteCapture.getOutputStream();
      OutputStream suiteErr = suiteCapture.getErrorStream();

      switch (outputMode) {
        case ALL:
          swappableOut.swap(new TeeOutputStream(swappableOut.getOriginal(), suiteOut));
          swappableErr.swap(new TeeOutputStream(swappableErr.getOriginal(), suiteErr));
          break;
        case FAILURE_ONLY:
          InMemoryStreamCapture caseCapture = new InMemoryStreamCapture();
          caseCaptures.put(description, caseCapture);
          swappableOut.swap(new TeeOutputStream(caseCapture.getOutputStream(), suiteOut));
          swappableErr.swap(new TeeOutputStream(caseCapture.getErrorStream(), suiteErr));
          break;
        case NONE:
          swappableOut.swap(suiteOut);
          swappableErr.swap(suiteErr);
          break;
        default:
          throw new IllegalStateException();
      }

      super.testStarted(description);
    }

    @Override
    public void testFailure(Failure failure) throws Exception {
      if (outputMode == OutputMode.FAILURE_ONLY) {
        if (caseCaptures.containsKey(failure.getDescription())) {
          InMemoryStreamCapture capture = caseCaptures.remove(failure.getDescription());
          capture.close();
          swappableOut.getOriginal().append(new String(capture.readOut(), UTF_8));
          swappableErr.getOriginal().append(new String(capture.readErr(), UTF_8));
        } else {
          // Do nothing.
          // When there is an exception in a @BeforeClass method the testStarted callback is not
          // called before the testFailure callback so there will be no caseCapture for the test.
        }
      }
      super.testFailure(failure);
    }

    @Override
    public void testFinished(Description description) throws Exception {
      if (!Util.isRunnable(description)) {
        return;
      }
      if (caseCaptures.containsKey(description)) {
        caseCaptures.remove(description).close();
      }
      super.testFinished(description);
    }

    @Override
    public byte[] readOut(Class testClass) throws IOException {
      return suiteCaptures.get(testClass).readOut();
    }

    @Override
    public byte[] readErr(Class testClass) throws IOException {
      return suiteCaptures.get(testClass).readErr();
    }

    @Override
    public void close(Class testClass) throws IOException {
      suiteCaptures.get(testClass).close();
    }
  }

  /**
   * A run listener that will stop the test run after the first test failure.
   */
  public static class FailFastListener extends RunListener {
    private final RunNotifier runNotifier;

    public FailFastListener(RunNotifier runNotifier) {
      this.runNotifier = runNotifier;
    }

    @Override
    public void testFailure(Failure failure) {
      runNotifier.pleaseStop();
    }
  }

  /**
   * A runner that wraps the original test runner so we can add a listener
   * to stop the tests after the first test failure.
   */
  public static class FailFastRunner extends Runner {
    private final Runner wrappedRunner;

    public FailFastRunner(Runner wrappedRunner) {
      this.wrappedRunner = wrappedRunner;
    }

    @Override public Description getDescription() {
      return wrappedRunner.getDescription();
    }

    @Override public void run(RunNotifier notifier) {
      notifier.addListener(new FailFastListener(notifier));
      try {
        wrappedRunner.run(notifier);
      } catch (StoppedByUserException ignore) {
        // NB: By swallowing StoppedByUserException here, we ensure that the RunFinished callback is
        // fired once, by JUnitCore.
      }
    }
  }

  enum OutputMode {
    ALL, FAILURE_ONLY, NONE
  }

  private final boolean failFast;
  private final OutputMode outputMode;
  private final boolean xmlReport;
  private final File outdir;
  private final boolean perTestTimer;
  private final Concurrency defaultConcurrency;
  private final int parallelThreads;
  private final int testShard;
  private final int numTestShards;
  private final int numRetries;
  private final boolean useExperimentalRunner;
  private final SwappableStream swappableOut;
  private final SwappableStream swappableErr;
  private final Set shutdownHooks = Sets.newHashSet(); // for use in tests
  private Collection testsToRun;

  ConsoleRunnerImpl(
      boolean failFast,
      OutputMode outputMode,
      boolean xmlReport,
      boolean perTestTimer,
      File outdir,
      Concurrency defaultConcurrency,
      int parallelThreads,
      int testShard,
      int numTestShards,
      int numRetries,
      boolean useExperimentalRunner,
      PrintStream out,
      PrintStream err) {

    Preconditions.checkNotNull(outputMode);
    Preconditions.checkNotNull(defaultConcurrency);
    Preconditions.checkNotNull(out);
    Preconditions.checkNotNull(err);

    this.failFast = failFast;
    this.outputMode = outputMode;
    this.xmlReport = xmlReport;
    this.perTestTimer = perTestTimer;
    this.outdir = outdir;
    this.defaultConcurrency = defaultConcurrency;
    this.parallelThreads = parallelThreads;
    this.testShard = testShard;
    this.numTestShards = numTestShards;
    this.numRetries = numRetries;
    this.swappableOut = new SwappableStream(out);
    this.swappableErr = new SwappableStream(err);
    this.useExperimentalRunner = useExperimentalRunner;
  }

  // by holding on to the tests to run, we can create a runner that is ready to go
  // without beginning the run (which is useful in the tests for the runner itself)
  private void setTestsToRun(Collection tests) {
    this.testsToRun = tests;
  }

  void run(Collection tests) {
    setTestsToRun(tests);
    run();
  }

  @VisibleForTesting
  public void run() {
    System.setOut(new PrintStream(swappableOut));
    System.setErr(new PrintStream(swappableErr));

    JUnitCore core = new JUnitCore();

    int numShutdownHooks = 0;

    if (testListener != null) {
      core.addListener(testListener);
    }

    if (!outdir.exists() && !outdir.mkdirs()) {
      throw new IllegalStateException("Failed to create output directory: " + outdir);
    }

    StreamCapturingListener streamCapturingListener =
        new StreamCapturingListener(outdir, outputMode, swappableOut, swappableErr);
    core.addListener(streamCapturingListener);

    RunListener xmlListener = null;
    if (xmlReport) {
      xmlListener = new AntJunitXmlReportListener(outdir, streamCapturingListener);
      core.addListener(xmlListener);
      numShutdownHooks++; // later we will register a shutdown hook for writing XML output
    }

    if (perTestTimer) {
      core.addListener(new PerTestConsoleListener(swappableOut.getOriginal()));
    } else {
      core.addListener(new ConsoleListener(swappableOut.getOriginal()));
    }

    ShutdownListener consoleShutdownListener =
      new ShutdownListener(new ConsoleListener(swappableOut.getOriginal()));
    core.addListener(consoleShutdownListener);

    // Wrap test execution with registration of a shutdown hook that will ensure we
    // never exit silently if the VM does.
    numShutdownHooks++;
    final CountDownLatch haltAfterUnexpectedShutdown = new CountDownLatch(numShutdownHooks);
    final Thread unexpectedExitHook = createUnexpectedExitHook(
      consoleShutdownListener,
      swappableOut.getOriginal(),
      haltAfterUnexpectedShutdown);
    addShutdownHook(unexpectedExitHook);

    // handle writing XML output when the tests time out and are terminated by pants
    if (xmlListener != null) {
      final ShutdownListener xmlShutdownListener = new ShutdownListener(xmlListener);
      core.addListener(xmlShutdownListener);

      Thread xmlShutdownHook = new Thread() {
        @Override
        public void run() {
          xmlShutdownListener.unexpectedShutdown();
          haltAfterUnexpectedShutdown.countDown();
        }
      };
      addShutdownHook(xmlShutdownHook);
    }

    int failures = 1;
    try {
      Collection parsedTests = new SpecParser(testsToRun).parse();
      if (useExperimentalRunner) {
        failures = runExperimental(parsedTests, core);
      } else {
        failures = runLegacy(parsedTests, core);
      }
    } catch (SpecException e) {
      swappableErr.getOriginal().println("Error parsing specs: " + e.getMessage());
    } catch (InitializationError e) {
      swappableErr.getOriginal().println("Error initializing JUnit: " + e.getMessage());
    } finally {
      // If we're exiting via a thrown exception, we'll get a better message by letting it
      // propagate than by halt()ing.
      removeShutdownHook(unexpectedExitHook);
    }
    exit(failures == 0 ? 0 : 1);
  }

  /**
   * Returns a thread that records a system exit to the listener,
   * and then halts(1) after other unexpected exit hooks that use the given CountDownLatch.
   */
  private Thread createUnexpectedExitHook(
    final ShutdownListener listener,
    final PrintStream out,
    final CountDownLatch haltAfter
  ) {
    return new Thread() {
      @Override public void run() {
        try {
          listener.unexpectedShutdown();
          // We want to trap and log no matter why abort failed for a better end user message.
        } catch (Exception e) {
          out.println(e);
          e.printStackTrace(out);
        }
        try {
          // Other shutdown hooks might still need to write test results,
          // so wait for them (up to 15 seconds) to finish before halting.
          haltAfter.countDown();
          long awaiting = haltAfter.getCount();
          if (awaiting > 0) out.println("Waiting for " + awaiting + "shutdown hooks to complete");
          haltAfter.await(15, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
          out.println(e);
          e.printStackTrace(out);
        } finally {
          if (callSystemExitOnFinish) {
            // This error might be a call to `System.exit(0)` in a test, which we definitely do
            // not want to go unnoticed.
            out.println("FATAL: VM exiting unexpectedly.");
            out.flush();
            Runtime.getRuntime().halt(1);
          }
        }
      }
    };
  }

  private void addShutdownHook(Thread hook) {
    shutdownHooks.add(hook);
    Runtime.getRuntime().addShutdownHook(hook);
  }

  private void removeShutdownHook(Thread hook) {
    shutdownHooks.remove(hook);
    Runtime.getRuntime().removeShutdownHook(hook);
  }

  private int runExperimental(Collection parsedTests, JUnitCore core)
      throws InitializationError {
    Preconditions.checkNotNull(core);

    int failures = 0;
    SpecSet filter = new SpecSet(parsedTests, defaultConcurrency);

    // TODO(zundel): Test sharding currently isn't compatible with the parallel computer runner
    // since the Computer only accepts Class objects.
    if (numTestShards == 0) {
      // Run all of the parallel tests using the ConcurrentComputer
      // NB(zundel): This runs these test of each concurrency setting together and waits for them
      // to finish.  This introduces a bottleneck after each class of test.
      failures += runConcurrentTests(core, filter, Concurrency.PARALLEL_CLASSES_AND_METHODS);
      failures += runConcurrentTests(core, filter, Concurrency.PARALLEL_CLASSES);
      failures += runConcurrentTests(core, filter, Concurrency.PARALLEL_METHODS);
    }

    // Everything else has to run serially or with the legacy runner
    // TODO(zundel): Attempt to refactor so we can dump runLegacy all together.
    List legacySpecs = ImmutableList.copyOf(filter.specs());
    failures += runLegacy(legacySpecs, core);

    return failures;
  }

  private int runConcurrentTests(JUnitCore core, SpecSet specSet, Concurrency concurrency)
      throws InitializationError {
    Computer junitComputer = new ConcurrentComputer(concurrency, parallelThreads);
    Class[] classes = specSet.extract(concurrency).classes();
    RunnerBuilder builder = createCustomBuilder(swappableErr.getOriginal());
    Runner suite = junitComputer.getSuite(builder, classes);
    return core.run(Request.runner(suite)).getFailureCount();
  }

  private int runLegacy(Collection parsedTests, JUnitCore core) throws InitializationError {
    List requests = legacyParseRequests(swappableErr.getOriginal(), parsedTests);
    if (numTestShards > 0) {
      requests = setFilterForTestShard(requests);
    }

    Runner requestRunner;
    if (this.parallelThreads > 1) {
      requestRunner = new ConcurrentCompositeRequestRunner(
          requests, this.defaultConcurrency, this.parallelThreads);
    } else {
      requestRunner = new CompositeRequestRunner(requests);
    }
    requestRunner = maybeWithFailFastRunner(requestRunner);

    return core.run(requestRunner).getFailureCount();
  }

  private Runner maybeWithFailFastRunner(Runner runner) {
    if (failFast) {
      return new FailFastRunner(runner);
    } else {
      return runner;
    }
  }

  private List legacyParseRequests(PrintStream err, Collection specs) {
    Set testMethods = Sets.newLinkedHashSet();
    Set> classes = Sets.newLinkedHashSet();
    for (Spec spec: specs) {
      if (spec.getMethods().isEmpty()) {
        classes.add(spec.getSpecClass());
      } else {
        for (String method : spec.getMethods()) {
          testMethods.add(new TestMethod(spec.getSpecClass(), method));
        }
      }
    }

    List requests = Lists.newArrayList();
    if (!classes.isEmpty()) {
      if (this.perTestTimer || this.parallelThreads > 1) {
        for (Class clazz : classes) {
          // legacy doesn't support scala test tests, so those are run as classes.
          if (legacyShouldRunParallelMethods(clazz) && !ScalaTestUtil.isScalaTestTest(clazz)) {
            testMethods.addAll(TestMethod.fromClass(clazz));
          } else {
            requests.add(createAnnotatedClassRequest(err, clazz));
          }
        }
      } else {
        // The code below does what the original call
        // requests.add(Request.classes(classes.toArray(new Class[classes.size()])));
        // does, except that it instantiates our own builder, needed to support retries.
        try {
          RunnerBuilder builder = createCustomBuilder(err);
          Runner suite = new Computer().getSuite(
              builder, classes.toArray(new Class[classes.size()]));
          requests.add(Request.runner(suite));
        } catch (InitializationError e) {
          throw new RuntimeException(
              "Internal error: Suite constructor, called as above, should always complete");
        }
      }
    }
    for (TestMethod testMethod : testMethods) {
      requests.add(createAnnotatedClassRequest(err, testMethod.clazz)
          .filterWith(Description.createTestDescription(testMethod.clazz, testMethod.name)));
    }
    return requests;
  }

  private AnnotatedClassRequest createAnnotatedClassRequest(PrintStream err, Class clazz) {
    return new AnnotatedClassRequest(clazz, numRetries, err);
  }

  private RunnerBuilder createCustomBuilder(PrintStream original) {
    return new CustomAnnotationBuilder(numRetries, original);
  }

  private boolean legacyShouldRunParallelMethods(Class clazz) {
    if (!Util.isRunnable(clazz)) {
      return false;
    }
    // The legacy runner makes Requests out of each individual method in a class. This isn't
    // designed to work for JUnit3 and isn't appropriate for custom runners.
    if (Util.isJunit3Test(clazz) || Util.isUsingCustomRunner(clazz)) {
      return false;
    }

    // TestSerial and TestParallel take precedence over the default concurrency command
    // line parameter
    if (clazz.isAnnotationPresent(TestSerial.class)
        || clazz.isAnnotationPresent(TestParallel.class)) {
      return false;
    }

    return this.defaultConcurrency.shouldRunMethodsParallel();
  }

  /**
   * Using JUnit4 test filtering mechanism, replaces the provided list of requests with
   * the one where each request has a filter attached. The filters are used to run only
   * one test shard, i.e. every Mth test out of N (testShard and numTestShards fields).
   */
  private List setFilterForTestShard(List requests) {
    // The filter below can be called multiple times for the same test, at least
    // when parallelThreads is true. To maintain the stable "run - not run" test status,
    // we determine it once, when the test is seen for the first time (always in serial
    // order), and save it in testToRunStatus table.
    class TestFilter extends Filter {
      private int testIdx;
      private Map testToRunStatus = Maps.newHashMap();

      @Override
      public boolean shouldRun(Description desc) {
        if (desc.isSuite() && !ScalaTestUtil.isScalaTestTest(desc.getTestClass())) {
          return true;
        }
        String descString = Util.getPantsFriendlyDisplayName(desc);
        // Note that currently even when parallelThreads is true, the first time this
        // is called in serial order, by our own iterator below.
        synchronized (this) {
          Boolean shouldRun = testToRunStatus.get(descString);
          if (shouldRun != null) {
            return shouldRun;
          } else {
            shouldRun = testIdx % numTestShards == testShard;
            testIdx++;
            testToRunStatus.put(descString, shouldRun);
            return shouldRun;
          }
        }
      }

      @Override
      public String describe() {
        return "Filters a static subset of test methods";
      }
    }

    class AlphabeticComparator implements Comparator {
      @Override
      public int compare(Description o1, Description o2) {
        return Util.getPantsFriendlyDisplayName(o1).compareTo(Util.getPantsFriendlyDisplayName(o2));
      }
    }

    TestFilter testFilter = new TestFilter();
    AlphabeticComparator alphaComp = new AlphabeticComparator();
    ArrayList filteredRequests = new ArrayList(requests.size());
    for (Request request : requests) {
      filteredRequests.add(request.sortWith(alphaComp).filterWith(testFilter));
    }
    // This will iterate over all of the test serially, calling shouldRun() above.
    // It's needed to guarantee stable sharding in all situations.
    for (Request request : filteredRequests) {
      request.getRunner().getDescription();
    }
    return filteredRequests;
  }

  @VisibleForTesting
  void runShutdownHooks() throws InterruptedException {
    for(Thread hook: shutdownHooks) {
      hook.start();
    }
    for(Thread hook: shutdownHooks) {
      hook.join(10000); // wait for all hooks to complete, up to 10 seconds each
    }
  }

  /**
   * Launcher for JUnitConsoleRunner.
   *
   * @param args options from the command line
   */
  public static void main(String[] args) {
    mainImpl(args).run();
  }

  /**
   * As main, but returns the ConsoleRunnerImpl instance and doesn't begin the test run.
   * For use in tests.
   *
   * @param args options from the command line
   */
  public static ConsoleRunnerImpl mainImpl(String[] args) {
    /**
     * Command line option bean.
     */
    class Options {
      @Option(name = "-fail-fast", usage = "Causes the test suite run to fail fast.")
      private boolean failFast;

      @Option(name = "-output-mode", usage = "Specify what part of output should be passed " +
          "to stdout. In case of FAILURE_ONLY and parallel tests execution " +
          "output can be partial or even wrong. (default: ALL)")
      private OutputMode outputMode = OutputMode.ALL;

      @Option(name = "-xmlreport",
              usage = "Create ant compatible junit xml report files in -outdir.")
      private boolean xmlReport;

      @Option(name = "-outdir",
              usage = "Directory to output test captures too.")
      private File outdir = new File(System.getProperty("java.io.tmpdir"));

      @Option(name = "-per-test-timer",
          usage = "Show a description of each test and timer for each test class.")
      private boolean perTestTimer;

      // TODO(zundel): This argument is deprecated, remove in a future release
      @Option(name = "-default-parallel",
          usage = "DEPRECATED: use -default-concurrency instead.\n"
              + "Whether to run test classes without @TestParallel or @TestSerial in parallel.")
      private boolean defaultParallel;

      @Option(name = "-default-concurrency",
          usage = "Specify how to parallelize running tests.\n"
          + "Use -use-experimental-runner for PARALLEL_METHODS and PARALLEL_CLASSES_AND_METHODS")
      private Concurrency defaultConcurrency;

      private int parallelThreads = 0;

      @Option(name = "-parallel-threads",
          usage = "Number of threads to execute tests in parallel. Must be positive, "
              + "or 0 to set automatically.")
      public void setParallelThreads(int parallelThreads) {
        if (parallelThreads < 0) {
          throw new InvalidCmdLineArgumentException(
              "-parallel-threads", parallelThreads, "-parallel-threads cannot be negative");
        }
        this.parallelThreads = parallelThreads;
        if (parallelThreads == 0) {
          int availableProcessors = Runtime.getRuntime().availableProcessors();
          this.parallelThreads = availableProcessors;
          System.err.printf("Auto-detected %d processors, using -parallel-threads=%d\n",
              availableProcessors, this.parallelThreads);
        }
      }

      private int testShard;
      private int numTestShards;

      @Option(name = "-test-shard",
          usage = "Subset of tests to run, in the form M/N, 0 <= M < N. For example, 1/3 means "
                  + "run tests number 2, 5, 8, 11, ...")
      public void setTestShard(String shard) {
        String errorMsg = "-test-shard should be in the form M/N";
        int slashIdx = shard.indexOf('/');
        if (slashIdx < 0) {
          throw new InvalidCmdLineArgumentException("-test-shard", shard, errorMsg);
        }
        try {
          this.testShard = Integer.parseInt(shard.substring(0, slashIdx));
          this.numTestShards = Integer.parseInt(shard.substring(slashIdx + 1));
        } catch (NumberFormatException ex) {
          throw new InvalidCmdLineArgumentException("-test-shard", shard, errorMsg);
        }
        if (testShard < 0 || numTestShards <= 0 || testShard >= numTestShards) {
          throw new InvalidCmdLineArgumentException(
              "-test-shard", shard, "0 <= M < N is required in -test-shard M/N");
        }
      }

      private int numRetries;

      @Option(name = "-num-retries",
          usage = "Number of attempts to retry each failing test, 0 by default")
      public void setNumRetries(int numRetries) {
        if (numRetries < 0) {
          throw new InvalidCmdLineArgumentException(
              "-num-retries", numRetries, "-num-retries cannot be negative");
        }
        this.numRetries = numRetries;
      }

      @Argument(usage = "Names of junit test classes or test methods to run.  Names prefixed "
                        + "with @ are considered arg file paths and these will be loaded and the "
                        + "newline delimited arguments found inside added to the list.",
                required = true,
                metaVar = "TESTS",
                handler = StringArgumentsHandler.class)
      private String[] tests = {};

      @Option(name="-use-experimental-runner",
          usage="Use the experimental runner that has support for parallel methods")
      private boolean useExperimentalRunner = false;
    }

    Options options = new Options();
    CmdLineParser parser = new CmdLineParser(options);
    try {
      parser.parseArgument(args);
    } catch (CmdLineException | InvalidCmdLineArgumentException e) {
      parser.printUsage(System.err);
      exit(1);
    }

    options.defaultConcurrency = computeConcurrencyOption(options.defaultConcurrency,
        options.defaultParallel);

    ConsoleRunnerImpl runner =
        new ConsoleRunnerImpl(options.failFast,
            options.outputMode,
            options.xmlReport,
            options.perTestTimer,
            options.outdir,
            options.defaultConcurrency,
            options.parallelThreads,
            options.testShard,
            options.numTestShards,
            options.numRetries,
            options.useExperimentalRunner,
            // NB: Buffering helps speedup output-heavy tests.
            new PrintStream(new BufferedOutputStream(System.out), true),
            new PrintStream(new BufferedOutputStream(System.err), true));

    runner.setTestsToRun(Lists.newArrayList(options.tests));
    return runner;
  }

  /**
   * Used to convert the legacy -default-parallel option to the new
   * style -default-concurrency values
   */
  @VisibleForTesting
  static Concurrency computeConcurrencyOption(Concurrency defaultConcurrency,
      boolean defaultParallel) {

    if (defaultConcurrency != null) {
      // -default-concurrency option present - use it.
      return defaultConcurrency;
    }

    // Fall Back to using -default-parallel
    if (!defaultParallel) {
      return Concurrency.SERIAL;
    }
    return Concurrency.PARALLEL_CLASSES;
  }

  private static void exit(int code) {
    if (callSystemExitOnFinish) {
      // We're a main - its fine to exit.
      System.exit(code);
    } else {
      if (code != 0) {
        throw new RuntimeException("ConsoleRunner exited with status " + code);
      }
    }
  }

  // ---------------------------- For testing only ---------------------------------

  public static void setCallSystemExitOnFinish(boolean exitOnFinish) {
    callSystemExitOnFinish = exitOnFinish;
  }

  public static void addTestListener(RunListener listener) {
    testListener = listener;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy