| /* |
| * Copyright 2012-present Facebook, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may |
| * not use this file except in compliance with the License. You may obtain |
| * a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package com.facebook.buck.java; |
| |
| import com.facebook.buck.model.BuildId; |
| import com.facebook.buck.shell.ShellStep; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.test.selectors.TestSelectorList; |
| import com.facebook.buck.util.BuckConstant; |
| import com.facebook.buck.util.ProcessExecutor; |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Lists; |
| |
| import java.io.File; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.List; |
| import java.util.Set; |
| |
| public class JUnitStep extends ShellStep { |
| |
| // Note that the default value is used when `buck test --all` is run on Buck itself. |
| @VisibleForTesting |
| static final String JUNIT_TEST_RUNNER_CLASS_NAME = |
| "com.facebook.buck.junit.JUnitMain"; |
| @VisibleForTesting |
| static final String TESTNG_TEST_RUNNER_CLASS_NAME = |
| "com.facebook.buck.junit.TestNGMain"; |
| |
| private static final Path TESTRUNNER_CLASSES = |
| Paths.get( |
| System.getProperty( |
| "buck.testrunner_classes", |
| new File("build/testrunner/classes").getAbsolutePath())); |
| |
| @VisibleForTesting |
| public static final String BUILD_ID_PROPERTY = "com.facebook.buck.buildId"; |
| |
| private final ImmutableSet<Path> classpathEntries; |
| private final Iterable<String> testClassNames; |
| private final List<String> vmArgs; |
| private final Path directoryForTestResults; |
| private final Path tmpDirectory; |
| private final Path testRunnerClasspath; |
| private final boolean isCodeCoverageEnabled; |
| private final boolean isDebugEnabled; |
| private final BuildId buildId; |
| private TestSelectorList testSelectorList; |
| private final boolean isDryRun; |
| private final TestType type; |
| private final Optional<Long> testRuleTimeoutMs; |
| |
| // Set when the junit command times out. |
| private boolean hasTimedOut = false; |
| |
| /** |
| * JaCoco is enabled for the code-coverage analysis. |
| */ |
| public static final String PATH_TO_JACOCO_AGENT_JAR = |
| System.getProperty( |
| "buck.jacoco_agent_jar", |
| "third-party/java/jacoco/jacocoagent.jar"); |
| |
| public static final String JACOCO_EXEC_COVERAGE_FILE = "jacoco.exec"; |
| |
| public static final Path JACOCO_OUTPUT_DIR = BuckConstant.GEN_PATH.resolve("jacoco"); |
| |
| /** |
| * @param classpathEntries contains the entries that will be listed first in the classpath when |
| * running JUnit. Entries for the bootclasspath for Android will be appended to this list, as |
| * well as an entry for the test runner. classpathEntries must include entries for the tests |
| * that will be run, as well as an entry for JUnit. |
| * @param testClassNames the fully qualified names of the Java tests to run |
| * @param directoryForTestResults directory where test results should be written |
| * @param tmpDirectory directory tests can use for local file scratch space. |
| */ |
| public JUnitStep( |
| Set<Path> classpathEntries, |
| Iterable<String> testClassNames, |
| List<String> vmArgs, |
| Path directoryForTestResults, |
| Path tmpDirectory, |
| boolean isCodeCoverageEnabled, |
| boolean isDebugEnabled, |
| BuildId buildId, |
| TestSelectorList testSelectorList, |
| boolean isDryRun, |
| TestType type, |
| Optional<Long> testRuleTimeoutMs) { |
| this(classpathEntries, |
| testClassNames, |
| vmArgs, |
| directoryForTestResults, |
| tmpDirectory, |
| isCodeCoverageEnabled, |
| isDebugEnabled, |
| buildId, |
| testSelectorList, |
| isDryRun, |
| type, |
| TESTRUNNER_CLASSES, |
| testRuleTimeoutMs); |
| } |
| |
| @VisibleForTesting |
| JUnitStep( |
| Set<Path> classpathEntries, |
| Iterable<String> testClassNames, |
| List<String> vmArgs, |
| Path directoryForTestResults, |
| Path tmpDirectory, |
| boolean isCodeCoverageEnabled, |
| boolean isDebugEnabled, |
| BuildId buildId, |
| TestSelectorList testSelectorList, |
| boolean isDryRun, |
| TestType type, |
| Path testRunnerClasspath, |
| Optional<Long> testRuleTimeoutMs) { |
| this.classpathEntries = ImmutableSet.copyOf(classpathEntries); |
| this.testClassNames = Iterables.unmodifiableIterable(testClassNames); |
| this.vmArgs = ImmutableList.copyOf(vmArgs); |
| this.directoryForTestResults = directoryForTestResults; |
| this.tmpDirectory = tmpDirectory; |
| this.isCodeCoverageEnabled = isCodeCoverageEnabled; |
| this.isDebugEnabled = isDebugEnabled; |
| this.buildId = buildId; |
| this.testSelectorList = testSelectorList; |
| this.isDryRun = isDryRun; |
| this.type = type; |
| this.testRunnerClasspath = testRunnerClasspath; |
| this.testRuleTimeoutMs = testRuleTimeoutMs; |
| } |
| |
| @Override |
| public String getShortName() { |
| return "junit"; |
| } |
| |
| @Override |
| protected ImmutableList<String> getShellCommandInternal(ExecutionContext context) { |
| ImmutableList.Builder<String> args = ImmutableList.builder(); |
| args.add("java"); |
| args.add(String.format("-Djava.io.tmpdir=%s", tmpDirectory)); |
| |
| // NOTE(agallagher): These propbably don't belong here, but buck integration tests need |
| // to find the test runner classes, so propagate these down via the relevant properties. |
| args.add( |
| String.format( |
| "-Dbuck.testrunner_classes=%s", |
| testRunnerClasspath)); |
| |
| if (isCodeCoverageEnabled) { |
| args.add(String.format("-javaagent:%s=destfile=%s/%s,append=true", |
| PATH_TO_JACOCO_AGENT_JAR, |
| JACOCO_OUTPUT_DIR, |
| JACOCO_EXEC_COVERAGE_FILE)); |
| } |
| |
| // Include the buildId |
| args.add(String.format("-D%s=%s", BUILD_ID_PROPERTY, buildId)); |
| |
| if (isDebugEnabled) { |
| // This is the default config used by IntelliJ. By doing this, all a user |
| // needs to do is create a new "Remote" debug config. Note that we start |
| // suspended, so tests will not run until the user connects. |
| args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005"); |
| warnUser(context, |
| "Debugging. Suspending JVM. Connect a JDWP debugger to port 5005 to proceed."); |
| } |
| |
| // User-defined VM arguments, such as -D or -X. |
| args.addAll(vmArgs); |
| |
| // verbose flag, if appropriate. |
| if (context.getVerbosity().shouldUseVerbosityFlagIfAvailable()) { |
| args.add("-verbose"); |
| } |
| |
| // Build up the -classpath argument, starting with the classpath entries the client specified. |
| List<Path> classpath = Lists.newArrayList(classpathEntries); |
| |
| // Finally, include an entry for the test runner. |
| classpath.add(testRunnerClasspath); |
| |
| // Add the -classpath argument. |
| args.add("-classpath").add(Joiner.on(File.pathSeparator).join(classpath)); |
| |
| // Specify the Java class whose main() method should be run. This is the class that is |
| // responsible for running the tests. |
| if (TestType.JUNIT == type) { |
| args.add(JUNIT_TEST_RUNNER_CLASS_NAME); |
| } else if (TestType.TESTNG == type) { |
| args.add(TESTNG_TEST_RUNNER_CLASS_NAME); |
| } else { |
| throw new IllegalArgumentException( |
| "java_test: unrecognized type " + type + ", expected eg. junit or testng"); |
| } |
| |
| // The first argument to the test runner is where the test results should be written. It is not |
| // reliable to write test results to stdout or stderr because there may be output from the unit |
| // tests written to those file descriptors, as well. |
| args.add(directoryForTestResults.toString()); |
| |
| // Add the default test timeout if --debug flag is not set |
| long timeout = isDebugEnabled ? 0 : context.getDefaultTestTimeoutMillis(); |
| args.add(String.valueOf(timeout)); |
| |
| // Add the test selectors, one per line, in a single argument. |
| StringBuilder selectorsArgBuilder = new StringBuilder(); |
| if (!testSelectorList.isEmpty()) { |
| for (String rawSelector : this.testSelectorList.getRawSelectors()) { |
| selectorsArgBuilder.append(rawSelector).append("\n"); |
| } |
| } |
| args.add(selectorsArgBuilder.toString()); |
| |
| // Dry-run flag. |
| args.add(isDryRun ? "non-empty-dry-run-flag" : ""); |
| |
| // List all of the tests to be run. |
| for (String testClassName : testClassNames) { |
| args.add(testClassName); |
| } |
| |
| return args.build(); |
| } |
| |
| @Override |
| public ImmutableMap<String, String> getEnvironmentVariables(ExecutionContext context) { |
| return ImmutableMap.of("TMP", tmpDirectory.toString()); |
| } |
| |
| private void warnUser(ExecutionContext context, String message) { |
| context.getStdErr().println(context.getAnsi().asWarningText(message)); |
| } |
| |
| @Override |
| protected Optional<Long> getTimeout() { |
| return testRuleTimeoutMs; |
| } |
| |
| @Override |
| protected int getExitCodeFromResult(ExecutionContext context, ProcessExecutor.Result result) { |
| int exitCode = result.getExitCode(); |
| |
| // If we timed out, force the exit code to 0 just so that the step itself doesn't fail, |
| // allowing us to interpret any test cases that finished before the bad test. We signify |
| // this special case by setting `hasTimedOut` which the result interpreter will query to |
| // properly format its results. |
| if (result.isTimedOut()) { |
| exitCode = 0; |
| hasTimedOut = true; |
| } |
| |
| return exitCode; |
| } |
| |
| public boolean hasTimedOut() { |
| return hasTimedOut; |
| } |
| |
| } |