com.facebook.buck.rules.JavaTestRule.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.rules.JavaTestRule.java

Source

/*
 * 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.rules;

import com.facebook.buck.java.JUnitStep;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.Step;
import com.facebook.buck.step.fs.MakeCleanDirectoryStep;
import com.facebook.buck.util.BuckConstant;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.ZipFileTraversal;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.annotation.Nullable;

public class JavaTestRule extends DefaultJavaLibraryRule implements TestRule {

    private final ImmutableList<String> vmArgs;

    /**
     * Build rules for which this test rule will be testing.
     */
    private final ImmutableSet<JavaLibraryRule> sourceUnderTest;

    private CompiledClassFileFinder compiledClassFileFinder;

    private final ImmutableSet<String> labels;

    protected JavaTestRule(BuildRuleParams buildRuleParams, Set<String> srcs, Set<String> resources,
            Set<String> labels, @Nullable String proguardConfig, AnnotationProcessingParams annotationProcessors,
            List<String> vmArgs, ImmutableSet<JavaLibraryRule> sourceUnderTest, String sourceLevel,
            String targetLevel) {
        super(buildRuleParams, srcs, resources, proguardConfig, annotationProcessors, /* exportDeps */ false,
                sourceLevel, targetLevel);
        this.vmArgs = ImmutableList.copyOf(vmArgs);
        this.sourceUnderTest = Preconditions.checkNotNull(sourceUnderTest);
        this.labels = ImmutableSet.copyOf(labels);
    }

    @Override
    public BuildRuleType getType() {
        return BuildRuleType.JAVA_TEST;
    }

    @Override
    public ImmutableSet<String> getLabels() {
        return labels;
    }

    @Override
    protected RuleKey.Builder ruleKeyBuilder() {
        // Convert sourceUnderTest to a form that RuleKey can handle.  Ideally RuleKey would directly
        // handle sourceUnderTest, but type erasure would cause method overload ambiguity.  The other
        // alternative would be to add a converter method to JavaLibraryRule, but that will not work
        // because JavaLibraryRule is an interface.
        Comparator<BuildRule> comparator = new Comparator<BuildRule>() {
            @Override
            public int compare(BuildRule o1, BuildRule o2) {
                return o1.compareTo(o2);
            }
        };
        ImmutableSortedSet.Builder<BuildRule> builder = new ImmutableSortedSet.Builder<BuildRule>(comparator);
        for (JavaLibraryRule rule : sourceUnderTest) {
            builder.add(rule);
        }
        ImmutableSortedSet<BuildRule> srcUnderTest = builder.build();

        return super.ruleKeyBuilder().set("vmArgs", vmArgs).set("sourceUnderTest", srcUnderTest);
    }

    /**
     * @return A set of rules that this test rule will be testing.
     */
    public ImmutableSet<JavaLibraryRule> getSourceUnderTest() {
        return sourceUnderTest;
    }

    public ImmutableList<String> getVmArgs() {
        return vmArgs;
    }

    /**
     * Runs the tests specified by the "srcs" of this class. If this rule transitively depends on
     * other {@code java_test()} rules, then they will be run separately.
     */
    @Override
    public List<Step> runTests(BuildContext buildContext, ExecutionContext executionContext) {
        Preconditions.checkState(isRuleBuilt(), "%s must be built before tests can be run.", this);

        // If no classes were generated, then this is probably a java_test() that declares a number of
        // other java_test() rules as deps, functioning as a test suite. In this case, simply return an
        // empty list of commands.
        Set<String> testClassNames = getClassNamesForSources();
        if (testClassNames.isEmpty()) {
            return ImmutableList.of();
        }

        // androidResourceDeps will be null if this test is re-run without being rebuilt.
        if (androidResourceDeps == null) {
            androidResourceDeps = AndroidResourceRule.getAndroidResourceDeps(this,
                    buildContext.getDependencyGraph());
        }

        ImmutableList.Builder<Step> steps = ImmutableList.builder();

        String pathToTestOutput = getPathToTestOutput();
        MakeCleanDirectoryStep mkdirClean = new MakeCleanDirectoryStep(pathToTestOutput);
        steps.add(mkdirClean);

        // If there are android resources, then compile the uber R.java files and add them to the
        // classpath used to run the test runner.
        ImmutableSet<String> classpathEntries;
        if (isAndroidRule()) {
            BuildTarget buildTarget = getBuildTarget();
            String rDotJavaClasspathEntry;
            UberRDotJavaUtil.createDummyRDotJavaFiles(androidResourceDeps, buildTarget, steps);
            rDotJavaClasspathEntry = UberRDotJavaUtil.getRDotJavaBinFolder(buildTarget);
            ImmutableSet.Builder<String> classpathEntriesBuilder = ImmutableSet.builder();
            classpathEntriesBuilder.add(rDotJavaClasspathEntry);
            classpathEntriesBuilder.addAll(getTransitiveClasspathEntries().values());
            classpathEntries = classpathEntriesBuilder.build();
        } else {
            classpathEntries = ImmutableSet.copyOf(getTransitiveClasspathEntries().values());
        }

        Step junit = new JUnitStep(classpathEntries, testClassNames, vmArgs, pathToTestOutput,
                executionContext.isCodeCoverageEnabled, executionContext.isDebugEnabled());
        steps.add(junit);

        return steps.build();
    }

    @Override
    public boolean isTestRunRequired(BuildContext buildContext, ExecutionContext executionContext) {
        // If this rule was cached, then no commands are necessary to run the tests. The results will be
        // read from the XML files in interpretTestResults(); If debug is enabled we should run the
        // tests as the user is expecting to hook up a debugger.
        if (executionContext.isDebugEnabled() || !isRuleBuiltFromCache()) {
            return true;
        }

        // The rule has been built from the cache. However, it's possible for the results file not to
        // have been written (eg. if a user runs with the --debug flag and then aborts the build) To
        // prevent a FileNotFoundException later on, check that the output files are present.

        // It is possible that this rule was not responsible for running any tests because all tests
        // were run by its deps. In this case, return an empty TestResults.
        Set<String> testClassNames = getClassNamesForSources();
        if (testClassNames.isEmpty()) {
            return false;
        }

        File outputDirectory = new File(getPathToTestOutput());
        for (String testClass : testClassNames) {
            File testResultFile = new File(outputDirectory, testClass + ".xml");
            if (!testResultFile.exists()) {
                return true;
            }
        }

        return false;
    }

    private String getPathToTestOutput() {
        return String.format("%s/%s/__java_test_%s_output__", BuckConstant.GEN_DIR, getBuildTarget().getBasePath(),
                getBuildTarget().getShortName());
    }

    @Override
    public Callable<TestResults> interpretTestResults() {
        return new Callable<TestResults>() {

            @Override
            public TestResults call() throws Exception {
                // It is possible that this rule was not responsible for running any tests because all tests
                // were run by its deps. In this case, return an empty TestResults.
                Set<String> testClassNames = getClassNamesForSources();
                if (testClassNames.isEmpty()) {
                    return TestResults.getEmptyTestResults();
                }

                File outputDirectory = new File(getPathToTestOutput());
                List<TestCaseSummary> summaries = Lists.newArrayListWithCapacity(testClassNames.size());
                for (String testClass : testClassNames) {
                    File testResultFile = new File(outputDirectory, testClass + ".xml");
                    TestCaseSummary summary = XmlTestResultParser.parse(testResultFile);
                    summaries.add(summary);
                }

                return new TestResults(summaries);
            }

        };
    }

    private Set<String> getClassNamesForSources() {
        if (compiledClassFileFinder == null) {
            compiledClassFileFinder = new CompiledClassFileFinder(this);
        }
        return compiledClassFileFinder.getClassNamesForSources();
    }

    @VisibleForTesting
    static class CompiledClassFileFinder {

        private final Set<String> classNamesForSources;

        CompiledClassFileFinder(JavaTestRule rule) {
            Preconditions.checkState(rule.isRuleBuilt(),
                    "Rule must be built so that the classes folder is available");
            classNamesForSources = getClassNamesForSources(rule.getJavaSrcs(), rule.getOutput());
        }

        public Set<String> getClassNamesForSources() {
            return classNamesForSources;
        }

        /**
         * When a collection of .java files is compiled into a directory, that directory will have a
         * subfolder structure that matches the package structure of the input .java files. In general,
         * the .java files will be 1:1 with the .class files with two notable exceptions:
         * (1) There will be an additional .class file for each inner/anonymous class generated. These
         *     types of classes are easy to identify because they will contain a '$' in the name.
         * (2) A .java file that defines multiple top-level classes (yes, this can exist:
         *     http://stackoverflow.com/questions/2336692/java-multiple-class-declarations-in-one-file)
         *     will generate multiple .class files that do not have '$' in the name.
         * In this method, we perform a strict check for (1) and use a heuristic for (2). It is possible
         * to filter out the type (2) situation with a stricter check that aligns the package directories
         * of the .java files and the .class files, but it is a pain to implement. If this heuristic turns
         * out to be insufficient in practice, then we can fix it.
         *
         * @param sources paths to .java source files that were passed to javac
         * @param jarFile jar where the generated .class files were written
         */
        @VisibleForTesting
        static Set<String> getClassNamesForSources(Set<String> sources, @Nullable File jarFile) {
            if (jarFile == null) {
                return ImmutableSet.of();
            }

            final Set<String> sourceClassNames = Sets.newHashSetWithExpectedSize(sources.size());
            for (String source : sources) {
                int lastSlashIndex = source.lastIndexOf('/');
                if (lastSlashIndex >= 0) {
                    source = source.substring(lastSlashIndex + 1);
                }
                source = source.substring(0, source.length() - ".java".length());
                sourceClassNames.add(source);
            }

            final ImmutableSet.Builder<String> testClassNames = ImmutableSet.builder();
            ZipFileTraversal traversal = new ZipFileTraversal(jarFile) {

                @Override
                public void visit(ZipFile zipFile, ZipEntry zipEntry) {
                    final String name = new File(zipEntry.getName()).getName();

                    // Ignore non-.class files.
                    if (!name.endsWith(".class")) {
                        return;
                    }

                    // As a heuristic for case (2) as described in the Javadoc, make sure the name of the .class
                    // file matches the name of a .java file.
                    String nameWithoutDotClass = name.substring(0, name.length() - ".class".length());
                    if (!sourceClassNames.contains(nameWithoutDotClass)) {
                        return;
                    }

                    // Make sure it is a .class file that corresponds to a top-level .class file and not an
                    // inner class.
                    if (!name.contains("$")) {
                        String fullyQualifiedNameWithDotClassSuffix = zipEntry.getName().replace('/', '.');
                        String className = fullyQualifiedNameWithDotClassSuffix.substring(0,
                                fullyQualifiedNameWithDotClassSuffix.length() - ".class".length());
                        testClassNames.add(className);
                    }
                }
            };
            try {
                traversal.traverse();
            } catch (IOException e) {
                // There's nothing sane to do here. The jar file really should exist.
                throw Throwables.propagate(e);
            }

            return testClassNames.build();
        }
    }

    public static Builder newJavaTestRuleBuilder() {
        return new Builder();
    }

    public static class Builder extends DefaultJavaLibraryRule.Builder implements LabelsAttributeBuilder {

        @Nullable
        protected List<String> vmArgs = ImmutableList.of();
        protected ImmutableSet<String> sourceUnderTestNames = ImmutableSet.of();
        protected ImmutableSet<String> labels = ImmutableSet.of();

        protected Builder() {
        }

        @Override
        public JavaTestRule build(Map<String, BuildRule> buildRuleIndex) {
            ImmutableSet<JavaLibraryRule> sourceUnderTest = generateSourceUnderTest(sourceUnderTestNames,
                    buildRuleIndex);
            return new JavaTestRule(createBuildRuleParams(buildRuleIndex), srcs, resources, labels, proguardConfig,
                    getAnnotationProcessingBuilder().build(buildRuleIndex), vmArgs, sourceUnderTest, sourceLevel,
                    targetLevel);
        }

        @Override
        public Builder setBuildTarget(BuildTarget buildTarget) {
            super.setBuildTarget(buildTarget);
            return this;
        }

        @Override
        public Builder addDep(String dep) {
            super.addDep(dep);
            return this;
        }

        @Override
        public Builder addSrc(String src) {
            super.addSrc(src);
            return this;
        }

        public Builder setVmArgs(List<String> vmArgs) {
            this.vmArgs = vmArgs == null ? ImmutableList.<String>of() : ImmutableList.copyOf(vmArgs);
            return this;
        }

        public Builder setSourceUnderTest(ImmutableSet<String> sourceUnderTestNames) {
            this.sourceUnderTestNames = sourceUnderTestNames;
            return this;
        }

        @Override
        public Builder setLabels(ImmutableSet<String> labels) {
            this.labels = labels;
            return this;
        }

        /**
         * Generates the set of build rules that contain the source that will be under test.
         */
        protected ImmutableSet<JavaLibraryRule> generateSourceUnderTest(ImmutableSet<String> sourceUnderTestNames,
                Map<String, BuildRule> buildRuleIndex) {
            ImmutableSet.Builder<JavaLibraryRule> sourceUnderTest = ImmutableSet.builder();
            for (String sourceUnderTestName : sourceUnderTestNames) {
                // Generates the set by matching its path with the full path names that are passed in.
                BuildRule rule = buildRuleIndex.get(sourceUnderTestName);
                if (rule instanceof JavaLibraryRule) {
                    sourceUnderTest.add((JavaLibraryRule) rule);
                } else {
                    // In this case, the source under test specified in the build file was not a Java library
                    // rule. Since EMMA requires the sources to be in Java, we will throw this exception and
                    // not continue with the tests.
                    throw new HumanReadableException(
                            "Specified source under test for %s is not a Java library: %s.",
                            getBuildTarget().getFullyQualifiedName(), sourceUnderTestName);
                }
            }

            return sourceUnderTest.build();
        }
    }
}