| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // 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.google.devtools.build.lib.rules.objc; |
| |
| 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.Iterables; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.analysis.FileProvider; |
| import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.Runfiles.Builder; |
| import com.google.devtools.build.lib.analysis.RunfilesProvider; |
| import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction; |
| import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction.Substitution; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.packages.SkylarkClassObject; |
| import com.google.devtools.build.lib.rules.apple.AppleConfiguration; |
| import com.google.devtools.build.lib.rules.apple.DottedVersion; |
| import com.google.devtools.build.lib.rules.objc.ObjcRuleClasses.SimulatorRule; |
| import com.google.devtools.build.lib.rules.test.InstrumentedFilesProvider; |
| import com.google.devtools.build.lib.rules.test.TestEnvironmentProvider; |
| import com.google.devtools.build.lib.syntax.Type; |
| import com.google.devtools.build.lib.util.FileType; |
| import com.google.devtools.build.lib.util.Preconditions; |
| import java.util.List; |
| import javax.annotation.Nullable; |
| |
| /** |
| * Support for running XcTests. |
| */ |
| public class TestSupport { |
| private final RuleContext ruleContext; |
| |
| public TestSupport(RuleContext ruleContext) { |
| this.ruleContext = ruleContext; |
| } |
| |
| /** |
| * Registers actions to create all files needed in order to actually run the test. |
| * |
| * @throws InterruptedException |
| */ |
| public TestSupport registerTestRunnerActions() throws InterruptedException { |
| registerTestScriptSubstitutionAction(); |
| return this; |
| } |
| |
| /** |
| * Returns the script which should be run in order to actually run the tests. |
| */ |
| public Artifact generatedTestScript() { |
| return ObjcRuleClasses.artifactByAppendingToBaseName(ruleContext, "_test_script"); |
| } |
| |
| private void registerTestScriptSubstitutionAction() throws InterruptedException { |
| // testBundleIpa is the bundle actually containing the tests. |
| Artifact testBundleIpa = testBundleIpa(); |
| |
| String runMemleaks = |
| ruleContext.getFragment(ObjcConfiguration.class).runMemleaks() ? "true" : "false"; |
| |
| ImmutableMap<String, String> testEnv = ruleContext.getConfiguration().getTestEnv(); |
| |
| // The substitutions below are common for simulator and lab device. |
| ImmutableList.Builder<Substitution> substitutions = |
| new ImmutableList.Builder<Substitution>() |
| .add(Substitution.of("%(memleaks)s", runMemleaks)) |
| .add(Substitution.of("%(test_app_ipa)s", testBundleIpa.getRootRelativePathString())) |
| .add(Substitution.of("%(test_app_name)s", baseNameWithoutIpa(testBundleIpa))) |
| .add(Substitution.of("%(test_bundle_path)s", testBundleIpa.getRootRelativePathString())) |
| .add( |
| Substitution.of("%(plugin_jars)s", Artifact.joinRootRelativePaths(":", plugins()))); |
| |
| substitutions.add(Substitution.ofSpaceSeparatedMap("%(test_env)s", testEnv)); |
| |
| // testHarnessIpa is the app being tested in the case where testBundleIpa is a .xctest bundle. |
| Optional<Artifact> testHarnessIpa = testHarnessIpa(); |
| if (testHarnessIpa.isPresent()) { |
| substitutions |
| .add(Substitution.of("%(xctest_app_ipa)s", |
| testHarnessIpa.get().getRootRelativePathString())) |
| .add(Substitution.of("%(xctest_app_name)s", baseNameWithoutIpa(testHarnessIpa.get()))) |
| .add(Substitution.of("%(test_host_path)s", |
| testHarnessIpa.get().getRootRelativePathString())); |
| } else { |
| substitutions |
| .add(Substitution.of("%(xctest_app_ipa)s", "")) |
| .add(Substitution.of("%(xctest_app_name)s", "")) |
| .add(Substitution.of("%(test_host_path)s", "")); |
| } |
| |
| if (ruleContext.attributes().get(IosTest.IS_XCTEST_ATTR, Type.BOOLEAN)) { |
| substitutions.add(Substitution.of("%(test_type)s", "XCTEST")); |
| } else { |
| substitutions.add(Substitution.of("%(test_type)s", "KIF")); |
| } |
| |
| Artifact template; |
| if (!runWithLabDevice()) { |
| substitutions.addAll(substitutionsForSimulator()); |
| template = ruleContext.getPrerequisiteArtifact(IosTest.TEST_TEMPLATE_ATTR, Mode.TARGET); |
| } else { |
| substitutions.addAll(substitutionsForLabDevice()); |
| template = testTemplateForLabDevice(); |
| } |
| |
| ruleContext.registerAction(new TemplateExpansionAction(ruleContext.getActionOwner(), |
| template, generatedTestScript(), substitutions.build(), /*executable=*/true)); |
| } |
| |
| private boolean runWithLabDevice() { |
| return iosLabDeviceSubstitutions() != null; |
| } |
| |
| /** |
| * Gets the substitutions for simulator. |
| */ |
| private ImmutableList<Substitution> substitutionsForSimulator() { |
| ImmutableList.Builder<Substitution> substitutions = new ImmutableList.Builder<Substitution>() |
| .add(Substitution.of("%(std_redirect_dylib_path)s", |
| stdRedirectDylib().getRunfilesPathString())) |
| .addAll(deviceSubstitutions().getSubstitutionsForTestRunnerScript()); |
| |
| Optional<Artifact> testRunner = testRunner(); |
| if (testRunner.isPresent()) { |
| substitutions.add( |
| Substitution.of("%(testrunner_binary)s", testRunner.get().getRunfilesPathString())); |
| } |
| return substitutions.build(); |
| } |
| |
| private IosTestSubstitutionProvider deviceSubstitutions() { |
| return ruleContext.getPrerequisite( |
| IosTest.TARGET_DEVICE, Mode.TARGET, IosTestSubstitutionProvider.class); |
| } |
| |
| /* |
| * The IPA of the bundle that contains the tests. Typically will be a .xctest bundle, but in the |
| * case where the xctest attribute is false, it will be a .app bundle. |
| */ |
| private Artifact testBundleIpa() throws InterruptedException { |
| return ruleContext.getImplicitOutputArtifact(ReleaseBundlingSupport.IPA); |
| } |
| |
| /* |
| * The IPA of the testHarness in the case where the testBundleIpa is an .xctest bundle. |
| */ |
| private Optional<Artifact> testHarnessIpa() { |
| FileProvider fileProvider = |
| ruleContext.getPrerequisite(IosTest.XCTEST_APP_ATTR, Mode.TARGET, FileProvider.class); |
| if (fileProvider == null) { |
| return Optional.absent(); |
| } |
| List<Artifact> files = |
| Artifact.filterFiles(fileProvider.getFilesToBuild(), FileType.of(".ipa")); |
| if (files.size() == 0) { |
| return Optional.absent(); |
| } else if (files.size() == 1) { |
| return Optional.of(Iterables.getOnlyElement(files)); |
| } else { |
| throw new IllegalStateException("Expected 0 or 1 files in xctest_app, got: " + files); |
| } |
| } |
| |
| private Artifact stdRedirectDylib() { |
| return ruleContext.getPrerequisiteArtifact(SimulatorRule.STD_REDIRECT_DYLIB_ATTR, Mode.HOST); |
| } |
| |
| /** |
| * Gets the binary of the testrunner attribute, if there is one. |
| */ |
| private Optional<Artifact> testRunner() { |
| return Optional.fromNullable( |
| ruleContext.getPrerequisiteArtifact(IosTest.TEST_RUNNER_ATTR, Mode.TARGET)); |
| } |
| |
| /** |
| * Gets the substitutions for lab device. |
| */ |
| private ImmutableList<Substitution> substitutionsForLabDevice() { |
| return new ImmutableList.Builder<Substitution>() |
| .addAll(iosLabDeviceSubstitutions().getSubstitutionsForTestRunnerScript()) |
| .add(Substitution.of("%(ios_device_arg)s", Joiner.on(" ").join(iosDeviceArgs()))).build(); |
| } |
| |
| /** |
| * Gets the test template for lab devices. |
| */ |
| private Artifact testTemplateForLabDevice() { |
| return ruleContext |
| .getPrerequisite( |
| IosTest.TEST_TARGET_DEVICE_ATTR, Mode.TARGET, LabDeviceTemplateProvider.class) |
| .getLabDeviceTemplate(); |
| } |
| |
| @Nullable |
| private IosTestSubstitutionProvider iosLabDeviceSubstitutions() { |
| return ruleContext.getPrerequisite( |
| IosTest.TEST_TARGET_DEVICE_ATTR, Mode.TARGET, IosTestSubstitutionProvider.class); |
| } |
| |
| private List<String> iosDeviceArgs() { |
| return ruleContext.attributes().get(IosTest.DEVICE_ARG_ATTR, Type.STRING_LIST); |
| } |
| |
| /** |
| * Adds all files needed to run this test to the passed Runfiles builder. |
| */ |
| public TestSupport addRunfiles( |
| Builder runfilesBuilder, InstrumentedFilesProvider instrumentedFilesProvider) |
| throws InterruptedException { |
| runfilesBuilder |
| .addArtifact(testBundleIpa()) |
| .addArtifacts(testHarnessIpa().asSet()) |
| .addArtifact(generatedTestScript()) |
| .addTransitiveArtifacts(plugins()); |
| if (!runWithLabDevice()) { |
| runfilesBuilder |
| .addArtifact(stdRedirectDylib()) |
| .addTransitiveArtifacts(deviceRunfiles()) |
| .addArtifacts(testRunner().asSet()); |
| } else { |
| runfilesBuilder.addTransitiveArtifacts(labDeviceRunfiles()); |
| } |
| |
| if (ruleContext.getConfiguration().isCodeCoverageEnabled()) { |
| runfilesBuilder.addArtifact(ruleContext.getHostPrerequisiteArtifact(IosTest.MCOV_TOOL_ATTR)); |
| runfilesBuilder.addTransitiveArtifacts(instrumentedFilesProvider.getInstrumentedFiles()); |
| } |
| |
| return this; |
| } |
| |
| /** |
| * Returns any additional providers that need to be exported to the rule context to the passed |
| * builder. |
| */ |
| public Iterable<SkylarkClassObject> getExtraProviders() { |
| IosDeviceProvider deviceProvider = |
| (IosDeviceProvider) |
| ruleContext.getPrerequisite( |
| IosTest.TARGET_DEVICE, Mode.TARGET, IosDeviceProvider.SKYLARK_CONSTRUCTOR.getKey()); |
| DottedVersion xcodeVersion = deviceProvider.getXcodeVersion(); |
| AppleConfiguration configuration = ruleContext.getFragment(AppleConfiguration.class); |
| |
| ImmutableMap.Builder<String, String> envBuilder = ImmutableMap.builder(); |
| |
| if (xcodeVersion != null) { |
| envBuilder.putAll(configuration.getXcodeVersionEnv(xcodeVersion)); |
| } |
| |
| if (ruleContext.getConfiguration().isCodeCoverageEnabled()) { |
| envBuilder.put("COVERAGE_GCOV_PATH", |
| ruleContext.getHostPrerequisiteArtifact(IosTest.OBJC_GCOV_ATTR).getExecPathString()); |
| envBuilder.put("APPLE_COVERAGE", "1"); |
| } |
| |
| return ImmutableList.<SkylarkClassObject>of(new TestEnvironmentProvider(envBuilder.build())); |
| } |
| |
| /** |
| * Jar files for plugins to the test runner. May be empty. |
| */ |
| private NestedSet<Artifact> plugins() { |
| NestedSetBuilder<Artifact> pluginArtifacts = NestedSetBuilder.stableOrder(); |
| pluginArtifacts.addTransitive( |
| PrerequisiteArtifacts.nestedSet(ruleContext, IosTest.PLUGINS_ATTR, Mode.TARGET)); |
| if (ruleContext.getFragment(ObjcConfiguration.class).runMemleaks()) { |
| pluginArtifacts.addTransitive( |
| PrerequisiteArtifacts.nestedSet(ruleContext, IosTest.MEMLEAKS_PLUGIN_ATTR, Mode.TARGET)); |
| } |
| return pluginArtifacts.build(); |
| } |
| |
| /** |
| * Runfiles required in order to use the specified target device. |
| */ |
| private NestedSet<Artifact> deviceRunfiles() { |
| return ruleContext.getPrerequisite(IosTest.TARGET_DEVICE, Mode.TARGET, RunfilesProvider.class) |
| .getDefaultRunfiles().getAllArtifacts(); |
| } |
| |
| /** |
| * Runfiles required in order to use the specified target device. |
| */ |
| private NestedSet<Artifact> labDeviceRunfiles() { |
| return ruleContext |
| .getPrerequisite(IosTest.TEST_TARGET_DEVICE_ATTR, Mode.TARGET, RunfilesProvider.class) |
| .getDefaultRunfiles().getAllArtifacts(); |
| } |
| |
| /** |
| * Adds files which must be built in order to run this test to builder. |
| */ |
| public TestSupport addFilesToBuild(NestedSetBuilder<Artifact> builder) |
| throws InterruptedException { |
| builder.add(testBundleIpa()).addAll(testHarnessIpa().asSet()); |
| return this; |
| } |
| |
| /** |
| * Returns the base name of the artifact, with the .ipa stuffix stripped. |
| */ |
| private static String baseNameWithoutIpa(Artifact artifact) { |
| String baseName = artifact.getExecPath().getBaseName(); |
| Preconditions.checkState(baseName.endsWith(".ipa"), |
| "%s should end in .ipa but doesn't", baseName); |
| return baseName.substring(0, baseName.length() - 4); |
| } |
| } |