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.

There is a newer version: 1.0.30
Show 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 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.runners.model.InitializationError;
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.pantsbuild.args4j.InvalidCmdLineArgumentException;
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() throws IOException {
      return read(outstream);
    }

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

    private byte[] read(ByteArrayOutputStream stream) throws IOException {
      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 {
      for (StreamCapture capture : suiteCaptures.values()) {
        capture.close();
      }
      caseCaptures.clear();
      super.testRunFinished(result);
    }

    @Override
    public void testStarted(Description description) throws Exception {
      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 (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();
    }
  }

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

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

    @Override
    public void testFailure(Failure failure) throws Exception {
      runNotifier.fireTestFinished(failure.getDescription());
      runNotifier.fireTestRunFinished(result);
      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));
      wrappedRunner.run(notifier);
    }
  }

  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;

  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;
  }

  void run(Collection tests) {
    System.setOut(new PrintStream(swappableOut));
    System.setErr(new PrintStream(swappableErr));

    JUnitCore core = new JUnitCore();

    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);

    if (xmlReport) {
      core.addListener(new AntJunitXmlReportListener(outdir, streamCapturingListener));
    }

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

    ShutdownListener shutdownListener = new ShutdownListener(swappableOut.getOriginal());
    core.addListener(shutdownListener);
    // Wrap test execution with registration of a shutdown hook that will ensure we
    // never exit silently if the VM does.
    final Thread unexpectedExitHook =
        createUnexpectedExitHook(shutdownListener, swappableOut.getOriginal());
    Runtime.getRuntime().addShutdownHook(unexpectedExitHook);

    int failures = 1;
    try {
      Collection parsedTests = new SpecParser(tests).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.
      Runtime.getRuntime().removeShutdownHook(unexpectedExitHook);
    }
    exit(failures == 0 ? 0 : 1);
  }

  /**
   * Returns a thread that records a system exit to the listener, and then halts(1).
   */
  private Thread createUnexpectedExitHook(final ShutdownListener listener, final PrintStream out) {
    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);
        }
        // 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 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();
    CustomAnnotationBuilder builder =
        new CustomAnnotationBuilder(numRetries, 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);
    }

    if (this.parallelThreads > 1) {
      ConcurrentCompositeRequestRunner concurrentRunner = new ConcurrentCompositeRequestRunner(
          requests, this.defaultConcurrency, this.parallelThreads);
      if (failFast) {
        return core.run(new FailFastRunner(concurrentRunner)).getFailureCount();
      } else {
        return core.run(concurrentRunner).getFailureCount();
      }
    }

    int failures = 0;
    Result result;
    for (Request request : requests) {
      if (failFast) {
        result = core.run(new FailFastRunner(request.getRunner()));
      } else {
        result = core.run(request);
      }
      failures += result.getFailureCount();
    }
    return failures;
  }

  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) {
          if (legacyShouldRunParallelMethods(clazz)) {
            if (ScalaTestUtil.isScalaTestTest(clazz)) {
              // legacy and scala doesn't work easily.  just adding the class
              requests.add(new AnnotatedClassRequest(clazz, numRetries, err));
            } else {
              testMethods.addAll(TestMethod.fromClass(clazz));
            }
          } else {
            requests.add(new AnnotatedClassRequest(clazz, numRetries, err));
          }
        }
      } 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 {
          CustomAnnotationBuilder builder =
              new CustomAnnotationBuilder(numRetries, 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(new AnnotatedClassRequest(testMethod.clazz, numRetries, err)
          .filterWith(Description.createTestDescription(testMethod.clazz, testMethod.name)));
    }
    return requests;
  }

  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()) {
          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;
  }

  /**
   * Launcher for JUnitConsoleRunner.
   *
   * @param args options from the command line
   */
  public static void main(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 "
                        + "whitespace delimited arguments found inside added to the list",
                required = true,
                metaVar = "TESTS",
                handler = StringArrayOptionHandler.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 e) {
      parser.printUsage(System.err);
      exit(1);
    } catch (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));

    List tests = Lists.newArrayList();
    for (String test : options.tests) {
      if (test.startsWith("@")) {
        try {
          String argFileContents = Files.toString(new File(test.substring(1)), Charsets.UTF_8);
          tests.addAll(Arrays.asList(argFileContents.split("\\s+")));
        } catch (IOException e) {
          System.err.printf("Failed to load args from arg file %s: %s\n", test, e.getMessage());
          exit(1);
        }
      } else {
        tests.add(test);
      }
    }
    runner.run(tests);
  }

  /**
   * 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 - 2024 Weber Informatics LLC | Privacy Policy