/* * Copyright 2015-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.d; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.BuildContext; import com.facebook.buck.rules.BuildRule; import com.facebook.buck.rules.BuildRuleParams; import com.facebook.buck.rules.BuildableContext; import com.facebook.buck.rules.BuildableProperties; import com.facebook.buck.rules.ExternalTestRunnerRule; import com.facebook.buck.rules.ExternalTestRunnerTestSpec; import com.facebook.buck.rules.ForwardingBuildTargetSourcePath; import com.facebook.buck.rules.HasRuntimeDeps; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.TestRule; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.fs.MakeCleanDirectoryStep; import com.facebook.buck.test.TestCaseSummary; import com.facebook.buck.test.TestResultSummary; import com.facebook.buck.test.TestResults; import com.facebook.buck.test.TestRunningOptions; import com.facebook.buck.test.result.type.ResultType; import com.facebook.buck.util.MoreCollectors; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @SuppressWarnings("PMD.TestClassWithoutTestCases") public class DTest extends AbstractBuildRule implements ExternalTestRunnerRule, HasRuntimeDeps, TestRule { private ImmutableSortedSet<String> contacts; private ImmutableSortedSet<String> labels; private final BuildRule testBinaryBuildRule; private final Optional<Long> testRuleTimeoutMs; public DTest( BuildRuleParams params, BuildRule testBinaryBuildRule, ImmutableSortedSet<String> contacts, ImmutableSortedSet<String> labels, Optional<Long> testRuleTimeoutMs) { super(params); this.contacts = contacts; this.labels = labels; this.testRuleTimeoutMs = testRuleTimeoutMs; this.testBinaryBuildRule = testBinaryBuildRule; } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, BuildableContext buildableContext) { return ImmutableList.of(); } @Override public ImmutableSet<String> getContacts() { return contacts; } private ImmutableList<String> getExecutableCommand(SourcePathResolver pathResolver) { return ImmutableList.of(pathResolver.getAbsolutePath(getSourcePathToOutput()).toString()); } @Override public ImmutableSet<String> getLabels() { return labels; } /** @return the path to which the test commands output is written. */ protected Path getPathToTestExitCode() { return getPathToTestOutputDirectory().resolve("exitCode"); } /** @return the path to which the test commands output is written. */ protected Path getPathToTestOutput() { return getPathToTestOutputDirectory().resolve("output"); } @Override public Path getPathToTestOutputDirectory() { return BuildTargets.getGenPath(getProjectFilesystem(), getBuildTarget(), "__test_%s_output__"); } @Override public BuildableProperties getProperties() { return new BuildableProperties(BuildableProperties.Kind.TEST); } private ImmutableList<String> getShellCommand(SourcePathResolver pathResolver) { return getExecutableCommand(pathResolver); } @Override public Callable<TestResults> interpretTestResults( final ExecutionContext executionContext, boolean isUsingTestSelectors) { return () -> { ResultType resultType = ResultType.FAILURE; // Successful exit indicates success. try (ObjectInputStream objectIn = new ObjectInputStream( new FileInputStream( getProjectFilesystem().resolve(getPathToTestExitCode()).toFile()))) { int exitCode = objectIn.readInt(); if (exitCode == 0) { resultType = ResultType.SUCCESS; } } catch (IOException e) { // Any IO error means something went awry, so it's a failure. resultType = ResultType.FAILURE; } String testOutput = getProjectFilesystem().readFileIfItExists(getPathToTestOutput()).orElse(""); String message = ""; String stackTrace = ""; String testName = ""; if (resultType == ResultType.FAILURE && !testOutput.isEmpty()) { // We don't get any information on successful runs, but failures usually come with // some information. This code parses it. int firstNewline = testOutput.indexOf('\n'); String firstLine = firstNewline == -1 ? testOutput : testOutput.substring(0, firstNewline); // First line has format <Exception name>@<location>: <message> // Use <location> as test name, and <message> as message. Pattern firstLinePattern = Pattern.compile("^[^@]*@([^:]*): (.*)"); Matcher m = firstLinePattern.matcher(firstLine); if (m.matches()) { testName = m.group(1); message = m.group(2); } // The whole output is actually a stack trace. stackTrace = testOutput; } TestResultSummary summary = new TestResultSummary( getBuildTarget().getShortName(), testName, resultType, /* time */ 0, message, stackTrace, testOutput, /* stderr */ ""); return TestResults.of( getBuildTarget(), ImmutableList.of(new TestCaseSummary("main", ImmutableList.of(summary))), contacts, labels.stream().map(Object::toString).collect(MoreCollectors.toImmutableSet())); }; } @Override public ImmutableList<Step> runTests( ExecutionContext executionContext, TestRunningOptions options, SourcePathResolver pathResolver, TestReportingCallback testReportingCallback) { return new ImmutableList.Builder<Step>() .addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), getPathToTestOutputDirectory())) .add( new DTestStep( getProjectFilesystem(), getShellCommand(pathResolver), getPathToTestExitCode(), testRuleTimeoutMs, getPathToTestOutput())) .build(); } @Override public boolean runTestSeparately() { return false; } @Override public boolean supportsStreamingTests() { return false; } @Override public ExternalTestRunnerTestSpec getExternalTestRunnerSpec( ExecutionContext executionContext, TestRunningOptions testRunningOptions, SourcePathResolver pathResolver) { return ExternalTestRunnerTestSpec.builder() .setTarget(getBuildTarget()) .setType("dunit") .setCommand(getShellCommand(pathResolver)) .setLabels(getLabels()) .setContacts(getContacts()) .build(); } @Override public SourcePath getSourcePathToOutput() { return new ForwardingBuildTargetSourcePath( getBuildTarget(), Preconditions.checkNotNull(testBinaryBuildRule.getSourcePathToOutput())); } @Override public Stream<BuildTarget> getRuntimeDeps() { // Return the actual executable as a runtime dependency. // Without this, the file is not written when we get a cache hit. return Stream.of(testBinaryBuildRule.getBuildTarget()); } }