/* * Copyright 2017-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.android; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeFalse; import com.facebook.buck.event.BuckEventBusFactory; import com.facebook.buck.jvm.java.KeystoreBuilder; import com.facebook.buck.jvm.java.KeystoreDescription; import com.facebook.buck.jvm.java.KeystoreDescriptionArg; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.model.BuildTargetFactory; import com.facebook.buck.rules.ActionGraphAndResolver; import com.facebook.buck.rules.ActionGraphCache; import com.facebook.buck.rules.DefaultTargetNodeToBuildRuleTransformer; import com.facebook.buck.rules.FakeBuildContext; import com.facebook.buck.rules.FakeBuildableContext; import com.facebook.buck.rules.FakeSourcePath; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.rules.SourcePathRuleFinder; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.Step; import com.facebook.buck.step.TestExecutionContext; import com.facebook.buck.testutil.FakeProjectFilesystem; import com.facebook.buck.testutil.TargetGraphFactory; import com.facebook.buck.timing.IncrementingFakeClock; import com.facebook.buck.util.MoreCollectors; import com.facebook.buck.util.RichStream; import com.facebook.buck.util.environment.Platform; import com.google.common.base.Joiner; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; public class DuplicateResourcesTest { private BuildTarget mainResTarget; private BuildTarget directDepResTarget; private BuildTarget transitiveDepResTarget; private BuildTarget transitiveDepLibTarget; private BuildTarget bottomDepResTarget; private BuildTarget androidLibraryTarget; private BuildTarget androidBinaryTarget; private BuildTarget keystoreTarget; private FakeProjectFilesystem filesystem; private TargetNode<AndroidResourceDescriptionArg, AndroidResourceDescription> mainRes; private TargetNode<AndroidResourceDescriptionArg, AndroidResourceDescription> directDepRes; private TargetNode<AndroidResourceDescriptionArg, AndroidResourceDescription> transitiveDepRes; private TargetNode<AndroidResourceDescriptionArg, AndroidResourceDescription> bottomDepRes; private TargetNode<AndroidLibraryDescriptionArg, AndroidLibraryDescription> transitiveDepLib; private TargetNode<AndroidLibraryDescriptionArg, AndroidLibraryDescription> library; private TargetNode<KeystoreDescriptionArg, KeystoreDescription> keystore; /* * Builds up the following dependency graph, which an android_binary can depend on how it likes: * * //main_app:res //direct_dep:library <--------+//direct_dep:res * ^ ^ * | | * + + * //transitive_dep:library<-----+//transitive_dep:res * ^ * | * + * //bottom_dep:res */ @Before public void makeRules() throws Exception { mainResTarget = BuildTargetFactory.newInstance("//main_app:res"); directDepResTarget = BuildTargetFactory.newInstance("//direct_dep:res"); transitiveDepResTarget = BuildTargetFactory.newInstance("//transitive_dep:res"); transitiveDepLibTarget = BuildTargetFactory.newInstance("//transitive_dep:library"); bottomDepResTarget = BuildTargetFactory.newInstance("//bottom_dep:res"); androidLibraryTarget = BuildTargetFactory.newInstance("//direct_dep:library"); androidBinaryTarget = BuildTargetFactory.newInstance("//main_app:binary"); keystoreTarget = BuildTargetFactory.newInstance("//main_app:keystore"); filesystem = new FakeProjectFilesystem(); mainRes = AndroidResourceBuilder.createBuilder(mainResTarget) .setRes(new FakeSourcePath(filesystem, "main_app/res")) .setRDotJavaPackage("package") .build(); directDepRes = AndroidResourceBuilder.createBuilder(directDepResTarget) .setRes(new FakeSourcePath(filesystem, "direct_dep/res")) .setRDotJavaPackage("package") .setDeps(ImmutableSortedSet.of(transitiveDepResTarget, transitiveDepLibTarget)) .build(); transitiveDepLib = AndroidLibraryBuilder.createBuilder(transitiveDepLibTarget) .addDep(transitiveDepResTarget) .build(); transitiveDepRes = AndroidResourceBuilder.createBuilder(transitiveDepResTarget) .setRes(new FakeSourcePath(filesystem, "transitive_dep/res")) .setRDotJavaPackage("package") .setDeps(ImmutableSortedSet.of(bottomDepResTarget)) .build(); bottomDepRes = AndroidResourceBuilder.createBuilder(bottomDepResTarget) .setRes(new FakeSourcePath(filesystem, "bottom_dep/res")) .setRDotJavaPackage("package") .build(); library = AndroidLibraryBuilder.createBuilder(androidLibraryTarget) .addDep(directDepResTarget) .addDep(transitiveDepLibTarget) .build(); keystore = KeystoreBuilder.createBuilder(keystoreTarget) .setStore(new FakeSourcePath(filesystem, "store")) .setProperties(new FakeSourcePath(filesystem, "properties")) .build(); } @Test public void testDuplicateResoucesFavorCloserDependencyWithLibraryDep() throws Exception { assumeFalse("Android SDK paths don't work on Windows", Platform.detect() == Platform.WINDOWS); TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> binary = makeBinaryWithDeps(ImmutableSortedSet.of(mainResTarget, androidLibraryTarget)); ImmutableList<String> command = getAaptStepShellCommand(binary); assertResourcePathOrdering(command, "main_app", "direct_dep", "transitive_dep", "bottom_dep"); } @Test public void testDuplicateResoucesFavorCloserDependencyWithTwoLibraryDeps() throws Exception { assumeFalse("Android SDK paths don't work on Windows", Platform.detect() == Platform.WINDOWS); TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> binary = makeBinaryWithDeps( ImmutableSortedSet.of(mainResTarget, androidLibraryTarget, transitiveDepLibTarget)); ImmutableList<String> command = getAaptStepShellCommand(binary); assertResourcePathOrdering(command, "main_app", "direct_dep", "transitive_dep", "bottom_dep"); } @Test public void testDuplicateResoucesFavorCloserDependencyWithResourceDep() throws Exception { assumeFalse("Android SDK paths don't work on Windows", Platform.detect() == Platform.WINDOWS); TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> binary = makeBinaryWithDeps(ImmutableSortedSet.of(mainResTarget, directDepResTarget)); ImmutableList<String> command = getAaptStepShellCommand(binary); assertResourcePathOrdering(command, "main_app", "direct_dep", "transitive_dep", "bottom_dep"); } @Test public void testDuplicateResoucesFavorCloserDependencyWithOnlyResourceDep() throws Exception { assumeFalse("Android SDK paths don't work on Windows", Platform.detect() == Platform.WINDOWS); TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> binary = makeBinaryWithDeps(ImmutableSortedSet.of(directDepResTarget)); ImmutableList<String> command = getAaptStepShellCommand(binary); assertResourcePathOrdering(command, "direct_dep", "transitive_dep", "bottom_dep"); } private void assertResourcePathOrdering(ImmutableList<String> command, String... paths) { String errorMessage = String.format("Full command was: %s", Joiner.on(" ").join(command)); assertThat( errorMessage, command.stream().filter(s -> "-S".equals(s)).collect(MoreCollectors.toImmutableList()), Matchers.hasSize(paths.length)); int firstResourceFolderArgument = command.indexOf("-S"); List<Matcher<? super String>> expectedSubslice = new ArrayList<>(); for (String path : paths) { expectedSubslice.add(Matchers.is("-S")); expectedSubslice.add(Matchers.is("buck-out/gen/" + path + "/res#resources-symlink-tree/res")); } assertThat( errorMessage, command.subList( firstResourceFolderArgument, firstResourceFolderArgument + expectedSubslice.size()), Matchers.contains(expectedSubslice)); } private TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> makeBinaryWithDeps( ImmutableSortedSet<BuildTarget> deps) { return AndroidBinaryBuilder.createBuilder(androidBinaryTarget) .setOriginalDeps(deps) .setKeystore(keystoreTarget) .setManifest(new FakeSourcePath(filesystem, "manifest.xml")) .build(); } private ImmutableList<String> getAaptStepShellCommand( TargetNode<AndroidBinaryDescriptionArg, AndroidBinaryDescription> binary) { TargetGraph targetGraph = TargetGraphFactory.newInstance( binary, mainRes, directDepRes, transitiveDepRes, transitiveDepLib, bottomDepRes, library, keystore); ActionGraphAndResolver actionGraphAndResolver = ActionGraphCache.getFreshActionGraph( BuckEventBusFactory.newInstance(new IncrementingFakeClock(TimeUnit.SECONDS.toNanos(1))), new DefaultTargetNodeToBuildRuleTransformer(), targetGraph); SourcePathResolver pathResolver = new SourcePathResolver(new SourcePathRuleFinder(actionGraphAndResolver.getResolver())); ImmutableSet<ImmutableList<Step>> ruleSteps = RichStream.from(actionGraphAndResolver.getActionGraph().getNodes()) .filter(AaptPackageResources.class) .filter( r -> androidBinaryTarget .getUnflavoredBuildTarget() .equals(r.getBuildTarget().getUnflavoredBuildTarget())) .map( b -> b.getBuildSteps( FakeBuildContext.withSourcePathResolver(pathResolver), new FakeBuildableContext())) .map( steps -> steps .stream() .filter(step -> step instanceof AaptStep) .collect(MoreCollectors.toImmutableList())) .filter(steps -> !steps.isEmpty()) .collect(MoreCollectors.toImmutableSet()); assertEquals(1, ruleSteps.size()); assertEquals(1, Iterables.getOnlyElement(ruleSteps).size()); AaptStep step = (AaptStep) Iterables.getOnlyElement(Iterables.getOnlyElement(ruleSteps)); AndroidDirectoryResolver androidDirectoryResolver = new FakeAndroidDirectoryResolver( Optional.of(filesystem.getPath("/android-sdk")), Optional.of(filesystem.getPath("/android-build-tools")), Optional.empty(), Optional.empty()); AndroidPlatformTarget androidPlatformTarget = AndroidPlatformTarget.createFromDefaultDirectoryStructure( "", androidDirectoryResolver, "", ImmutableSet.of(), Optional.empty(), Optional.empty()); ExecutionContext context = TestExecutionContext.newBuilder() .setAndroidPlatformTargetSupplier(Suppliers.ofInstance(androidPlatformTarget)) .build(); return step.getShellCommand(context); } }