/* * Copyright 2014-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.apple; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargets; import com.facebook.buck.model.Either; import com.facebook.buck.model.Pair; import com.facebook.buck.rules.AbstractBuildRule; import com.facebook.buck.rules.AddToRuleKey; 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.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.SourcePathRuleFinder; import com.facebook.buck.rules.TestRule; import com.facebook.buck.rules.Tool; 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.TestResults; import com.facebook.buck.test.TestRunningOptions; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.OptionalCompat; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; 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 java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.concurrent.Callable; import java.util.stream.Stream; @SuppressWarnings("PMD.TestClassWithoutTestCases") public class AppleTest extends AbstractBuildRule implements TestRule, HasRuntimeDeps, ExternalTestRunnerRule { @AddToRuleKey private final Optional<SourcePath> xctool; @AddToRuleKey private Optional<Long> xctoolStutterTimeout; @AddToRuleKey private final Tool xctest; @AddToRuleKey private final boolean useXctest; @AddToRuleKey private final String platformName; private final Optional<String> defaultDestinationSpecifier; private final Optional<ImmutableMap<String, String>> destinationSpecifier; @AddToRuleKey private final AppleBundle testBundle; @AddToRuleKey private final Optional<AppleBundle> testHostApp; private final ImmutableSet<String> contacts; private final ImmutableSet<String> labels; @AddToRuleKey private final boolean runTestSeparately; @AddToRuleKey private final boolean isUiTest; private final Path testOutputPath; private final Path testLogsPath; @AddToRuleKey private final Optional<Either<SourcePath, String>> snapshotReferenceImagesPath; private Optional<Long> testRuleTimeoutMs; private final SourcePathRuleFinder ruleFinder; private Optional<AppleTestXctoolStdoutReader> xctoolStdoutReader; private Optional<AppleTestXctestOutputReader> xctestOutputReader; private final String testLogDirectoryEnvironmentVariable; private final String testLogLevelEnvironmentVariable; private final String testLogLevel; /** * Absolute path to xcode developer dir. * * <p>Should not be added to rule key. */ private final Supplier<Optional<Path>> xcodeDeveloperDirSupplier; private static class AppleTestXctoolStdoutReader implements XctoolRunTestsStep.StdoutReadingCallback { private final TestCaseSummariesBuildingXctoolEventHandler xctoolEventHandler; public AppleTestXctoolStdoutReader(TestRule.TestReportingCallback testReportingCallback) { this.xctoolEventHandler = new TestCaseSummariesBuildingXctoolEventHandler(testReportingCallback); } @Override public void readStdout(InputStream stdout) throws IOException { try (InputStreamReader stdoutReader = new InputStreamReader(stdout, StandardCharsets.UTF_8); BufferedReader bufferedReader = new BufferedReader(stdoutReader)) { XctoolOutputParsing.streamOutputFromReader(bufferedReader, xctoolEventHandler); } } public ImmutableList<TestCaseSummary> getTestCaseSummaries() { return xctoolEventHandler.getTestCaseSummaries(); } } private static class AppleTestXctestOutputReader implements XctestRunTestsStep.OutputReadingCallback { private final TestCaseSummariesBuildingXctestEventHandler xctestEventHandler; public AppleTestXctestOutputReader(TestRule.TestReportingCallback testReportingCallback) { this.xctestEventHandler = new TestCaseSummariesBuildingXctestEventHandler(testReportingCallback); } @Override public void readOutput(InputStream output) throws IOException { try (InputStreamReader outputReader = new InputStreamReader(output, StandardCharsets.UTF_8); BufferedReader outputBufferedReader = new BufferedReader(outputReader)) { XctestOutputParsing.streamOutput(outputBufferedReader, xctestEventHandler); } } public ImmutableList<TestCaseSummary> getTestCaseSummaries() { return xctestEventHandler.getTestCaseSummaries(); } } AppleTest( Optional<SourcePath> xctool, Optional<Long> xctoolStutterTimeout, Tool xctest, boolean useXctest, String platformName, Optional<String> defaultDestinationSpecifier, Optional<ImmutableMap<String, String>> destinationSpecifier, BuildRuleParams params, AppleBundle testBundle, Optional<AppleBundle> testHostApp, ImmutableSet<String> contacts, ImmutableSet<String> labels, boolean runTestSeparately, Supplier<Optional<Path>> xcodeDeveloperDirSupplier, String testLogDirectoryEnvironmentVariable, String testLogLevelEnvironmentVariable, String testLogLevel, Optional<Long> testRuleTimeoutMs, boolean isUiTest, Optional<Either<SourcePath, String>> snapshotReferenceImagesPath, SourcePathRuleFinder ruleFinder) { super(params); this.xctool = xctool; this.xctoolStutterTimeout = xctoolStutterTimeout; this.useXctest = useXctest; this.xctest = xctest; this.platformName = platformName; this.defaultDestinationSpecifier = defaultDestinationSpecifier; this.destinationSpecifier = destinationSpecifier; this.testBundle = testBundle; this.testHostApp = testHostApp; this.contacts = contacts; this.labels = labels; this.runTestSeparately = runTestSeparately; this.testRuleTimeoutMs = testRuleTimeoutMs; this.ruleFinder = ruleFinder; this.testOutputPath = getPathToTestOutputDirectory().resolve("test-output.json"); this.testLogsPath = getPathToTestOutputDirectory().resolve("logs"); this.xctoolStdoutReader = Optional.empty(); this.xctestOutputReader = Optional.empty(); this.xcodeDeveloperDirSupplier = xcodeDeveloperDirSupplier; this.testLogDirectoryEnvironmentVariable = testLogDirectoryEnvironmentVariable; this.testLogLevelEnvironmentVariable = testLogLevelEnvironmentVariable; this.testLogLevel = testLogLevel; this.isUiTest = isUiTest; this.snapshotReferenceImagesPath = snapshotReferenceImagesPath; } @Override public ImmutableSet<String> getLabels() { return labels; } @Override public ImmutableSet<String> getContacts() { return contacts; } public Pair<ImmutableList<Step>, ExternalTestRunnerTestSpec> getTestCommand( ExecutionContext context, TestRunningOptions options, SourcePathResolver pathResolver, TestRule.TestReportingCallback testReportingCallback) { ImmutableList.Builder<Step> steps = ImmutableList.builder(); ExternalTestRunnerTestSpec.Builder externalSpec = ExternalTestRunnerTestSpec.builder() .setTarget(getBuildTarget()) .setLabels(getLabels()) .setContacts(getContacts()); Path resolvedTestBundleDirectory = pathResolver.getAbsolutePath( Preconditions.checkNotNull(testBundle.getSourcePathToOutput())); Path pathToTestOutput = getProjectFilesystem().resolve(getPathToTestOutputDirectory()); steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), pathToTestOutput)); Path resolvedTestLogsPath = getProjectFilesystem().resolve(testLogsPath); steps.addAll(MakeCleanDirectoryStep.of(getProjectFilesystem(), resolvedTestLogsPath)); Path resolvedTestOutputPath = getProjectFilesystem().resolve(testOutputPath); Optional<Path> testHostAppPath = Optional.empty(); if (testHostApp.isPresent()) { Path resolvedTestHostAppDirectory = pathResolver.getAbsolutePath( Preconditions.checkNotNull(testHostApp.get().getSourcePathToOutput())); testHostAppPath = Optional.of( resolvedTestHostAppDirectory.resolve( testHostApp.get().getUnzippedOutputFilePathToBinary())); } if (!useXctest) { if (!xctool.isPresent()) { throw new HumanReadableException( "Set xctool_path = /path/to/xctool or xctool_zip_target = //path/to:xctool-zip " + "in the [apple] section of .buckconfig to run this test"); } ImmutableSet.Builder<Path> logicTestPathsBuilder = ImmutableSet.builder(); ImmutableMap.Builder<Path, Path> appTestPathsToHostAppsBuilder = ImmutableMap.builder(); if (testHostAppPath.isPresent()) { appTestPathsToHostAppsBuilder.put(resolvedTestBundleDirectory, testHostAppPath.get()); } else { logicTestPathsBuilder.add(resolvedTestBundleDirectory); } xctoolStdoutReader = Optional.of(new AppleTestXctoolStdoutReader(testReportingCallback)); Optional<String> destinationSpecifierArg; if (!destinationSpecifier.get().isEmpty()) { destinationSpecifierArg = Optional.of( Joiner.on(',') .join( Iterables.transform( destinationSpecifier.get().entrySet(), input -> input.getKey() + "=" + input.getValue()))); } else { destinationSpecifierArg = defaultDestinationSpecifier; } Optional<String> snapshotReferenceImagesPath = Optional.empty(); if (this.snapshotReferenceImagesPath.isPresent()) { if (this.snapshotReferenceImagesPath.get().isLeft()) { snapshotReferenceImagesPath = Optional.of( pathResolver .getAbsolutePath(this.snapshotReferenceImagesPath.get().getLeft()) .toString()); } else if (this.snapshotReferenceImagesPath.get().isRight()) { snapshotReferenceImagesPath = Optional.of( getProjectFilesystem() .getPathForRelativePath(this.snapshotReferenceImagesPath.get().getRight()) .toString()); } } XctoolRunTestsStep xctoolStep = new XctoolRunTestsStep( getProjectFilesystem(), pathResolver.getAbsolutePath(xctool.get()), options.getEnvironmentOverrides(), xctoolStutterTimeout, platformName, destinationSpecifierArg, logicTestPathsBuilder.build(), appTestPathsToHostAppsBuilder.build(), resolvedTestOutputPath, xctoolStdoutReader, xcodeDeveloperDirSupplier, options.getTestSelectorList(), context.isDebugEnabled(), Optional.of(testLogDirectoryEnvironmentVariable), Optional.of(resolvedTestLogsPath), Optional.of(testLogLevelEnvironmentVariable), Optional.of(testLogLevel), testRuleTimeoutMs, snapshotReferenceImagesPath); steps.add(xctoolStep); externalSpec.setType("xctool-" + (testHostApp.isPresent() ? "application" : "logic")); externalSpec.setCommand(xctoolStep.getCommand()); externalSpec.setEnv(xctoolStep.getEnv(context)); } else { xctestOutputReader = Optional.of(new AppleTestXctestOutputReader(testReportingCallback)); HashMap<String, String> environment = new HashMap<>(); environment.putAll(xctest.getEnvironment(pathResolver)); environment.putAll(options.getEnvironmentOverrides()); if (testHostAppPath.isPresent()) { environment.put("XCInjectBundleInto", testHostAppPath.get().toString()); } XctestRunTestsStep xctestStep = new XctestRunTestsStep( getProjectFilesystem(), ImmutableMap.copyOf(environment), xctest.getCommandPrefix(pathResolver), resolvedTestBundleDirectory, resolvedTestOutputPath, xctestOutputReader, xcodeDeveloperDirSupplier); steps.add(xctestStep); externalSpec.setType("xctest"); externalSpec.setCommand(xctestStep.getCommand()); externalSpec.setEnv(xctestStep.getEnv(context)); } return new Pair<>(steps.build(), externalSpec.build()); } @Override public ImmutableList<Step> runTests( ExecutionContext executionContext, TestRunningOptions options, SourcePathResolver pathResolver, TestReportingCallback testReportingCallback) { if (isUiTest()) { return ImmutableList.of(); } else { return getTestCommand(executionContext, options, pathResolver, testReportingCallback) .getFirst(); } } @Override public Callable<TestResults> interpretTestResults( final ExecutionContext executionContext, boolean isUsingTestSelectors) { return () -> { List<TestCaseSummary> testCaseSummaries; if (xctoolStdoutReader.isPresent()) { // We've already run the tests with 'xctool' and parsed // their output; no need to parse the same output again. testCaseSummaries = xctoolStdoutReader.get().getTestCaseSummaries(); } else if (xctestOutputReader.isPresent()) { // We've already run the tests with 'xctest' and parsed // their output; no need to parse the same output again. testCaseSummaries = xctestOutputReader.get().getTestCaseSummaries(); } else if (isUiTest()) { TestCaseSummary noTestsSummary = new TestCaseSummary( "XCUITest runs not supported with buck test", Collections.emptyList()); testCaseSummaries = Collections.singletonList(noTestsSummary); } else { Path resolvedOutputPath = getProjectFilesystem().resolve(testOutputPath); try (BufferedReader reader = Files.newBufferedReader(resolvedOutputPath, StandardCharsets.UTF_8)) { if (useXctest) { TestCaseSummariesBuildingXctestEventHandler xctestEventHandler = new TestCaseSummariesBuildingXctestEventHandler(NOOP_REPORTING_CALLBACK); XctestOutputParsing.streamOutput(reader, xctestEventHandler); testCaseSummaries = xctestEventHandler.getTestCaseSummaries(); } else { TestCaseSummariesBuildingXctoolEventHandler xctoolEventHandler = new TestCaseSummariesBuildingXctoolEventHandler(NOOP_REPORTING_CALLBACK); XctoolOutputParsing.streamOutputFromReader(reader, xctoolEventHandler); testCaseSummaries = xctoolEventHandler.getTestCaseSummaries(); } } } TestResults.Builder testResultsBuilder = TestResults.builder() .setBuildTarget(getBuildTarget()) .setTestCases(testCaseSummaries) .setContacts(contacts) .setLabels( labels.stream().map(Object::toString).collect(MoreCollectors.toImmutableSet())); if (getProjectFilesystem().isDirectory(testLogsPath)) { for (Path testLogPath : getProjectFilesystem().getDirectoryContents(testLogsPath)) { testResultsBuilder.addTestLogPaths(testLogPath); } } return testResultsBuilder.build(); }; } @Override public Path getPathToTestOutputDirectory() { // TODO(beng): Refactor the JavaTest implementation; this is identical. return BuildTargets.getGenPath( getProjectFilesystem(), getBuildTarget(), "__apple_test_%s_output__"); } @Override public boolean runTestSeparately() { // Tests which run in the simulator must run separately from all other tests; // there's a 20 second timeout hard-coded in the iOS Simulator SpringBoard which // is hit any time the host is overloaded. return runTestSeparately || testHostApp.isPresent(); } // This test rule just executes the test bundle, so we need it available locally. @Override public Stream<BuildTarget> getRuntimeDeps() { return Stream.concat( Stream.concat(Stream.of(testBundle), OptionalCompat.asSet(testHostApp).stream()) .map(BuildRule::getBuildTarget), OptionalCompat.asSet(xctool) .stream() .map(ruleFinder::filterBuildRuleInputs) .flatMap(ImmutableSet::stream) .map(BuildRule::getBuildTarget)); } @Override public boolean supportsStreamingTests() { return true; } @Override public ExternalTestRunnerTestSpec getExternalTestRunnerSpec( ExecutionContext executionContext, TestRunningOptions testRunningOptions, SourcePathResolver pathResolver) { return getTestCommand( executionContext, testRunningOptions, pathResolver, NOOP_REPORTING_CALLBACK) .getSecond(); } @Override public ImmutableList<Step> getBuildSteps( BuildContext context, BuildableContext buildableContext) { return ImmutableList.of(); } @Override public SourcePath getSourcePathToOutput() { return new ForwardingBuildTargetSourcePath( getBuildTarget(), testBundle.getSourcePathToOutput()); } public boolean isUiTest() { return isUiTest; } }