| // Copyright 2017 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 static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; |
| import com.google.devtools.build.lib.actions.Artifact.SpecialArtifactType; |
| import com.google.devtools.build.lib.actions.ExecException; |
| import com.google.devtools.build.lib.actions.UserExecException; |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.actions.SpawnAction; |
| import com.google.devtools.build.lib.analysis.util.ScratchAttributeWriter; |
| import com.google.devtools.build.lib.packages.util.MockObjcSupport; |
| import com.google.devtools.build.lib.rules.cpp.CppCompileAction; |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; |
| import com.google.devtools.build.lib.vfs.PathFragment; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Tests for header thinning. */ |
| @RunWith(JUnit4.class) |
| public class HeaderThinningTest extends ObjcRuleTestCase { |
| |
| private static final String CPP_COMPILE_ACTION_RULE_TYPE = "objc_library"; |
| |
| @Before |
| public void beforeEach() throws Exception { |
| MockObjcSupport.createCrosstoolPackage(mockToolsConfig); |
| MockObjcSupport.setupAppleSdks(mockToolsConfig); |
| useConfiguration( |
| "--crosstool_top=" + MockObjcSupport.DEFAULT_OSX_CROSSTOOL, |
| "--experimental_objc_header_thinning", |
| "--objc_use_dotd_pruning", |
| "--xcode_version=" + MockObjcSupport.DEFAULT_XCODE_VERSION, |
| "--ios_sdk_version=" + MockObjcSupport.DEFAULT_IOS_SDK_VERSION); |
| } |
| |
| @Test |
| public void testCppCompileActionHeaderThinningCanDetermineAdditionalInputs() throws Exception { |
| Artifact sourceFile = getSourceArtifact("objc/a.m"); |
| CppCompileAction action = createCppCompileAction(sourceFile); |
| List<Artifact> expectedHeaders = |
| ImmutableList.of( |
| getSourceArtifact("objc/a.pch"), |
| getSourceArtifact("objc/b.h"), |
| getSourceArtifact("objc/c"), |
| getSourceArtifact("objc/d.hpp")); |
| HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders)); |
| writeToHeadersListFile(action, "objc/a.pch", "objc/b.h", "objc/c", "objc/d.hpp"); |
| |
| Iterable<Artifact> headersFound = headerThinning.determineAdditionalInputs(null, action, null); |
| assertThat(headersFound).containsExactlyElementsIn(expectedHeaders); |
| } |
| |
| @Test |
| public void testCppCompileActionHeaderThinningThrowsWhenUnknownHeaderFound() throws Exception { |
| Artifact sourceFile = getSourceArtifact("objc/a.m"); |
| CppCompileAction action = createCppCompileAction(sourceFile); |
| List<Artifact> expectedHeaders = |
| ImmutableList.of(getSourceArtifact("objc/a.h"), getSourceArtifact("objc/b.h")); |
| HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders)); |
| writeToHeadersListFile(action, "objc/a.h", "objc/b.h", "objc/c.h"); |
| |
| try { |
| headerThinning.determineAdditionalInputs(null, action, null); |
| fail("Exception was not thrown"); |
| } catch (ExecException e) { |
| assertThat(e).hasMessageThat().containsMatch("(objc/c.h)"); |
| assertThat(e).isInstanceOf(UserExecException.class); |
| } |
| } |
| |
| @Test |
| public void testCppCompileActionHeaderThinningFindsHeadersInTreeArtifacts() throws Exception { |
| Artifact sourceFile = getSourceArtifact("objc/a.m"); |
| CppCompileAction action = createCppCompileAction(sourceFile); |
| List<Artifact> expectedHeaders = |
| ImmutableList.of(getSourceArtifact("objc/a.h"), getTreeArtifact("tree/dir")); |
| HeaderThinning headerThinning = new HeaderThinning(getPotentialHeaders(expectedHeaders)); |
| writeToHeadersListFile(action, "objc/a.h", "tree/dir/c.h"); |
| |
| Iterable<Artifact> headersFound = headerThinning.determineAdditionalInputs(null, action, null); |
| assertThat(headersFound).containsExactlyElementsIn(expectedHeaders); |
| } |
| |
| @Test |
| public void testObjcCompileActionHeaderThinningCanFindRequiredHeaderInputs() throws Exception { |
| Artifact sourceFile = getSourceArtifact("objc/a.m"); |
| Artifact headersListFile = getHeadersListArtifact(sourceFile); |
| scratch.file( |
| headersListFile.getExecPathString(), "objc/a.pch", "objc/b.h", "objc/c", "objc/d.hpp"); |
| List<Artifact> expectedHeaders = |
| ImmutableList.of( |
| getSourceArtifact("objc/a.pch"), |
| getSourceArtifact("objc/b.h"), |
| getSourceArtifact("objc/c"), |
| getSourceArtifact("objc/d.hpp")); |
| |
| Iterable<Artifact> headersFound = |
| HeaderThinning.findRequiredHeaderInputs( |
| sourceFile, |
| headersListFile, |
| createHeaderFilesMap(getPotentialHeaders(expectedHeaders))); |
| assertThat(headersFound).containsExactlyElementsIn(expectedHeaders); |
| } |
| |
| private void writeToHeadersListFile(CppCompileAction action, String... lines) throws Exception { |
| Artifact headersListFile = null; |
| for (Artifact input : action.getMandatoryInputs()) { |
| if (input.getExtension().equals("headers_list")) { |
| headersListFile = input; |
| break; |
| } |
| } |
| assertThat(headersListFile).isNotNull(); |
| scratch.file(headersListFile.getPath().getPathString(), lines); |
| } |
| |
| /** |
| * Simple utility to populate some unused header Artifacts along with the ones we expect to find. |
| */ |
| private Iterable<Artifact> getPotentialHeaders(List<Artifact> expectedHeaders) { |
| return Iterables.concat( |
| ImmutableList.of( |
| getSourceArtifact("objc/unused.h"), |
| getSourceArtifact("some/other.h"), |
| getSourceArtifact("objc/foo.pch")), |
| expectedHeaders); |
| } |
| |
| private CppCompileAction createCppCompileAction(Artifact sourceFile) throws Exception { |
| String ownerLabel = "//objc:lib"; |
| ScratchAttributeWriter.fromLabelString(this, CPP_COMPILE_ACTION_RULE_TYPE, ownerLabel) |
| .setList("srcs", sourceFile.getFilename()) |
| .write(); |
| ConfiguredTarget target = getConfiguredTarget(ownerLabel); |
| List<CppCompileAction> allActions = |
| actionsTestUtil() |
| .findTransitivePrerequisitesOf( |
| ActionsTestUtil.getFirstArtifactEndingWith(getFilesToBuild(target), ".a"), |
| CppCompileAction.class); |
| for (CppCompileAction action : allActions) { |
| if (action.getSourceFile().getExecPath().equals(sourceFile.getExecPath())) { |
| return action; |
| } |
| } |
| return null; |
| } |
| |
| private Artifact getHeadersListArtifact(Artifact sourceFile) { |
| return getSourceArtifact( |
| FileSystemUtils.replaceExtension(sourceFile.getExecPath(), ".headers_list"), |
| sourceFile.getRoot()); |
| } |
| |
| private static Map<PathFragment, Artifact> createHeaderFilesMap(Iterable<Artifact> artifacts) { |
| ImmutableMap.Builder<PathFragment, Artifact> headerFilesMapBuilder = ImmutableMap.builder(); |
| for (Artifact artifact : artifacts) { |
| headerFilesMapBuilder.put(artifact.getExecPath(), artifact); |
| } |
| return headerFilesMapBuilder.build(); |
| } |
| |
| private Artifact getTreeArtifact(String name) { |
| Artifact treeArtifactBase = getSourceArtifact(name); |
| return new SpecialArtifact( |
| treeArtifactBase.getPath(), |
| treeArtifactBase.getRoot(), |
| treeArtifactBase.getExecPath(), |
| treeArtifactBase.getArtifactOwner(), |
| SpecialArtifactType.TREE); |
| } |
| |
| @Test |
| public void generatesHeaderScanningAction() throws Exception { |
| Set<SpawnAction> scanningActions = |
| createTargetAndGetHeaderScanningActions(ImmutableList.of("one.m", "two.m")); |
| assertThat(scanningActions).hasSize(1); |
| } |
| |
| @Test |
| public void generates2HeaderScanningActionsWhenObjcAndCppSources() throws Exception { |
| Set<SpawnAction> scanningActions = |
| createTargetAndGetHeaderScanningActions(ImmutableList.of("one.m", "two.cc")); |
| assertThat(scanningActions).hasSize(2); |
| } |
| |
| @Test |
| public void generatesMultipleHeaderScanningActionsForLargeTargets2() throws Exception { |
| validateGeneratesMultipleHeaderScanningActionsForLargeTargets( |
| 2, targetConfig.getFragment(ObjcConfiguration.class).objcHeaderThinningPartitionSize()); |
| } |
| |
| @Test |
| public void generatesMultipleHeaderScanningActionsForLargeTargets4() throws Exception { |
| validateGeneratesMultipleHeaderScanningActionsForLargeTargets( |
| 4, targetConfig.getFragment(ObjcConfiguration.class).objcHeaderThinningPartitionSize()); |
| } |
| |
| @Test |
| public void generatesMultipleHeaderScanningActionsForLargeTargets8() throws Exception { |
| validateGeneratesMultipleHeaderScanningActionsForLargeTargets( |
| 8, targetConfig.getFragment(ObjcConfiguration.class).objcHeaderThinningPartitionSize()); |
| } |
| |
| @Test |
| public void generatesMultipleHeaderScanningActionsForLargeTargetsCustomPartition() |
| throws Exception { |
| int partitionSize = 5; |
| useConfiguration( |
| "--crosstool_top=" + MockObjcSupport.DEFAULT_OSX_CROSSTOOL, |
| "--experimental_objc_header_thinning", |
| "--objc_header_thinning_partition_size=" + 5, |
| "--objc_use_dotd_pruning", |
| "--xcode_version=" + MockObjcSupport.DEFAULT_XCODE_VERSION, |
| "--ios_sdk_version=" + MockObjcSupport.DEFAULT_IOS_SDK_VERSION); |
| validateGeneratesMultipleHeaderScanningActionsForLargeTargets(12, partitionSize); |
| } |
| |
| private void validateGeneratesMultipleHeaderScanningActionsForLargeTargets( |
| int actionCount, int partitionSize) throws Exception { |
| ImmutableList.Builder<String> sourcesBuilder = ImmutableList.builder(); |
| for (int i = 0; i < partitionSize * actionCount; ++i) { |
| sourcesBuilder.add(String.format("source_%d.m", i)); |
| } |
| Set<SpawnAction> scanningActions = |
| createTargetAndGetHeaderScanningActions(sourcesBuilder.build()); |
| assertThat(scanningActions).hasSize(actionCount); |
| } |
| |
| private Set<SpawnAction> createTargetAndGetHeaderScanningActions(Iterable<String> sources) |
| throws Exception { |
| String ownerLabel = "//objc:lib"; |
| ScratchAttributeWriter.fromLabelString(this, CPP_COMPILE_ACTION_RULE_TYPE, ownerLabel) |
| .setList("srcs", sources) |
| .write(); |
| ConfiguredTarget target = getConfiguredTarget(ownerLabel); |
| List<SpawnAction> spawnActions = |
| actionsTestUtil() |
| .findTransitivePrerequisitesOf( |
| ActionsTestUtil.getFirstArtifactEndingWith(getFilesToBuild(target), ".a"), |
| SpawnAction.class); |
| return Sets.newHashSet( |
| Iterables.filter(spawnActions, a -> a.getMnemonic().equals("ObjcHeaderScanning"))); |
| } |
| } |