// 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 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 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 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 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 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 expectedHeaders = ImmutableList.of( getSourceArtifact("objc/a.pch"), getSourceArtifact("objc/b.h"), getSourceArtifact("objc/c"), getSourceArtifact("objc/d.hpp")); Iterable 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 getPotentialHeaders(List 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 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().getRoot()); } private static Map createHeaderFilesMap(Iterable artifacts) { ImmutableMap.Builder 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.getRoot(), treeArtifactBase.getExecPath(), treeArtifactBase.getArtifactOwner(), SpecialArtifactType.TREE); } @Test public void generatesHeaderScanningAction() throws Exception { Set scanningActions = createTargetAndGetHeaderScanningActions(ImmutableList.of("one.m", "two.m")); assertThat(scanningActions).hasSize(1); } @Test public void generates2HeaderScanningActionsWhenObjcAndCppSources() throws Exception { Set 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 sourcesBuilder = ImmutableList.builder(); for (int i = 0; i < partitionSize * actionCount; ++i) { sourcesBuilder.add(String.format("source_%d.m", i)); } Set scanningActions = createTargetAndGetHeaderScanningActions(sourcesBuilder.build()); assertThat(scanningActions).hasSize(actionCount); } private Set createTargetAndGetHeaderScanningActions(Iterable 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 spawnActions = actionsTestUtil() .findTransitivePrerequisitesOf( ActionsTestUtil.getFirstArtifactEndingWith(getFilesToBuild(target), ".a"), SpawnAction.class); return Sets.newHashSet( Iterables.filter(spawnActions, a -> a.getMnemonic().equals("ObjcHeaderScanning"))); } }