|  | // 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.android; | 
|  |  | 
|  | import static com.google.common.truth.Truth.assertThat; | 
|  | import static java.nio.charset.StandardCharsets.UTF_8; | 
|  | import static org.junit.Assert.assertThrows; | 
|  |  | 
|  | import com.google.common.base.Joiner; | 
|  | import com.google.common.base.Splitter; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.io.PatternFilenameFilter; | 
|  | import java.io.BufferedReader; | 
|  | import java.io.File; | 
|  | import java.io.IOException; | 
|  | import java.nio.charset.Charset; | 
|  | import java.nio.file.FileSystem; | 
|  | import java.nio.file.FileSystems; | 
|  | import java.nio.file.Files; | 
|  | import java.nio.file.Path; | 
|  | import java.nio.file.Paths; | 
|  | import java.util.Iterator; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import javax.annotation.Nullable; | 
|  | import org.junit.Before; | 
|  | import org.junit.Test; | 
|  | import org.junit.runner.RunWith; | 
|  | import org.junit.runners.JUnit4; | 
|  |  | 
|  | /** Tests for {@link ManifestMergerAction}. */ | 
|  | @RunWith(JUnit4.class) | 
|  | public class ManifestMergerActionTest { | 
|  |  | 
|  | private Path working; | 
|  |  | 
|  | /** | 
|  | * Returns a runfile's path. | 
|  | * | 
|  | * <p>The `path` must specify a valid runfile, meaning on Windows (where $RUNFILES_MANIFEST_ONLY | 
|  | * is 1) the `path` must exactly match a runfiles manifest entry, and on Linux/MacOS `path` must | 
|  | * point to a valid file in the runfiles directory. | 
|  | */ | 
|  | @Nullable | 
|  | private static Path rlocation(String path) throws IOException { | 
|  | FileSystem fs = FileSystems.getDefault(); | 
|  | if (fs.getPath(path).isAbsolute()) { | 
|  | return fs.getPath(path); | 
|  | } | 
|  | if ("1".equals(System.getenv("RUNFILES_MANIFEST_ONLY"))) { | 
|  | String manifest = System.getenv("RUNFILES_MANIFEST_FILE"); | 
|  | assertThat(manifest).isNotNull(); | 
|  | try (BufferedReader r = | 
|  | Files.newBufferedReader(Paths.get(manifest), Charset.defaultCharset())) { | 
|  | Splitter splitter = Splitter.on(' ').limit(2); | 
|  | String line = null; | 
|  | while ((line = r.readLine()) != null) { | 
|  | List<String> tokens = splitter.splitToList(line); | 
|  | if (tokens.size() == 2) { | 
|  | if (tokens.get(0).equals(path)) { | 
|  | return fs.getPath(tokens.get(1)); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } else { | 
|  | String runfiles = System.getenv("RUNFILES_DIR"); | 
|  | if (runfiles == null) { | 
|  | runfiles = System.getenv("JAVA_RUNFILES"); | 
|  | assertThat(runfiles).isNotNull(); | 
|  | } | 
|  | Path result = fs.getPath(runfiles).resolve(path); | 
|  | assertThat(result.toFile().exists()).isTrue(); // comply with function's contract | 
|  | return result; | 
|  | } | 
|  | } | 
|  |  | 
|  | @Before public void setup() throws Exception { | 
|  | working = Files.createTempDirectory(toString()); | 
|  | working.toFile().deleteOnExit(); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testMergeManifestWithBrokenManifestSyntax() throws Exception { | 
|  | String dataDir = | 
|  | Paths.get(System.getenv("TEST_WORKSPACE"), System.getenv("TEST_BINARY")) | 
|  | .resolveSibling("testing/manifestmerge") | 
|  | .toString() | 
|  | .replace('\\', '/'); | 
|  | Files.createDirectories(working.resolve("output")); | 
|  | final Path mergedManifest = working.resolve("output/mergedManifest.xml"); | 
|  | final Path brokenMergerManifest = rlocation(dataDir + "/brokenManifest/AndroidManifest.xml"); | 
|  | assertThat(brokenMergerManifest.toFile().exists()).isTrue(); | 
|  |  | 
|  | AndroidManifestProcessor.ManifestProcessingException e = | 
|  | assertThrows( | 
|  | AndroidManifestProcessor.ManifestProcessingException.class, | 
|  | () -> { | 
|  | ManifestMergerAction.main( | 
|  | generateArgs( | 
|  | brokenMergerManifest, | 
|  | ImmutableMap.of(), | 
|  | false, /* isLibrary */ | 
|  | ImmutableMap.of("applicationId", "com.google.android.apps.testapp"), | 
|  | "", /* custom_package */ | 
|  | mergedManifest, | 
|  | false /* mergeManifestPermissions */) | 
|  | .toArray(new String[0])); | 
|  | }); | 
|  | assertThat(e) | 
|  | .hasMessageThat() | 
|  | .contains( | 
|  | "com.android.manifmerger.ManifestMerger2$MergeFailureException: " | 
|  | + "org.xml.sax.SAXParseException; lineNumber: 6; columnNumber: 6; " | 
|  | + "The markup in the document following the root element must be well-formed."); | 
|  | assertThat(mergedManifest.toFile().exists()).isFalse(); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testMerge_GenerateDummyManifest() throws Exception { | 
|  | Files.createDirectories(working.resolve("output")); | 
|  | Path mergedManifest = working.resolve("output/mergedManifest.xml"); | 
|  |  | 
|  | ManifestMergerAction.main( | 
|  | new String[] { | 
|  | "--customPackage", | 
|  | "foo.bar.baz", | 
|  | "--mergeType", | 
|  | "LIBRARY", | 
|  | "--manifestOutput", | 
|  | mergedManifest.toString() | 
|  | }); | 
|  |  | 
|  | assertThat( | 
|  | Joiner.on(" ") | 
|  | .join(Files.readAllLines(mergedManifest, UTF_8)) | 
|  | .replaceAll("\\s+", " ") | 
|  | .trim()) | 
|  | .isEqualTo( | 
|  | "<?xml version=\"1.0\" encoding=\"utf-8\"?> " | 
|  | + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" " | 
|  | + "package=\"foo.bar.baz\" > " | 
|  | + "<uses-sdk android:minSdkVersion=\"1\" /> " | 
|  | + "<application /> " | 
|  | + "</manifest>"); | 
|  | } | 
|  |  | 
|  | @Test public void testMerge() throws Exception { | 
|  | String dataDir = | 
|  | Paths.get(System.getenv("TEST_WORKSPACE"), System.getenv("TEST_BINARY")) | 
|  | .resolveSibling("testing/manifestmerge") | 
|  | .toString() | 
|  | .replace("\\", "/"); | 
|  |  | 
|  | final Path mergerManifest = rlocation(dataDir + "/merger/AndroidManifest.xml"); | 
|  | final Path mergeeManifestOne = rlocation(dataDir + "/mergeeOne/AndroidManifest.xml"); | 
|  | final Path mergeeManifestTwo = rlocation(dataDir + "/mergeeTwo/AndroidManifest.xml"); | 
|  | assertThat(mergerManifest.toFile().exists()).isTrue(); | 
|  | assertThat(mergeeManifestOne.toFile().exists()).isTrue(); | 
|  | assertThat(mergeeManifestTwo.toFile().exists()).isTrue(); | 
|  |  | 
|  | // The following code retrieves the path of the only AndroidManifest.xml in the expected/ | 
|  | // manifests directory. Unfortunately, this test runs internally and externally and the files | 
|  | // have different names. | 
|  | final File expectedManifestDirectory = | 
|  | mergerManifest.getParent().resolveSibling("expected").toFile(); | 
|  | final String[] debug = | 
|  | expectedManifestDirectory.list(new PatternFilenameFilter(".*AndroidManifest\\.xml$")); | 
|  | assertThat(debug).isNotNull(); | 
|  | final File[] expectedManifestDirectoryManifests = | 
|  | expectedManifestDirectory.listFiles((File dir, String name) -> true); | 
|  | assertThat(expectedManifestDirectoryManifests).isNotNull(); | 
|  | assertThat(expectedManifestDirectoryManifests).hasLength(1); | 
|  | final Path expectedManifest = expectedManifestDirectoryManifests[0].toPath(); | 
|  |  | 
|  | Files.createDirectories(working.resolve("output")); | 
|  | final Path mergedManifest = working.resolve("output/mergedManifest.xml"); | 
|  |  | 
|  | List<String> args = | 
|  | generateArgs( | 
|  | mergerManifest, | 
|  | ImmutableMap.of(mergeeManifestOne, "mergeeOne", mergeeManifestTwo, "mergeeTwo"), | 
|  | false, /* isLibrary */ | 
|  | ImmutableMap.of("applicationId", "com.google.android.apps.testapp"), | 
|  | "", /* custom_package */ | 
|  | mergedManifest, | 
|  | /* mergeManifestPermissions */ false); | 
|  | ManifestMergerAction.main(args.toArray(new String[0])); | 
|  |  | 
|  | assertThat( | 
|  | Joiner.on(" ") | 
|  | .join(Files.readAllLines(mergedManifest, UTF_8)) | 
|  | .replaceAll("\\s+", " ") | 
|  | .trim()) | 
|  | .isEqualTo( | 
|  | Joiner.on(" ") | 
|  | .join(Files.readAllLines(expectedManifest, UTF_8)) | 
|  | .replaceAll("\\s+", " ") | 
|  | .trim()); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void testMergeWithMergePermissionsEnabled() throws Exception { | 
|  | // Largely copied from testMerge() above. Perhaps worth combining the two test methods into one | 
|  | // method in the future? | 
|  | String dataDir = | 
|  | Paths.get(System.getenv("TEST_WORKSPACE"), System.getenv("TEST_BINARY")) | 
|  | .resolveSibling("testing/manifestmerge") | 
|  | .toString() | 
|  | .replace("\\", "/"); | 
|  |  | 
|  | final Path mergerManifest = rlocation(dataDir + "/merger/AndroidManifest.xml"); | 
|  | final Path mergeeManifestOne = rlocation(dataDir + "/mergeeOne/AndroidManifest.xml"); | 
|  | final Path mergeeManifestTwo = rlocation(dataDir + "/mergeeTwo/AndroidManifest.xml"); | 
|  | assertThat(mergerManifest.toFile().exists()).isTrue(); | 
|  | assertThat(mergeeManifestOne.toFile().exists()).isTrue(); | 
|  | assertThat(mergeeManifestTwo.toFile().exists()).isTrue(); | 
|  |  | 
|  | // The following code retrieves the path of the only AndroidManifest.xml in the | 
|  | // expected-merged-permission/manifests directory. Unfortunately, this test runs | 
|  | // internally and externally and the files have different names. | 
|  | final File expectedManifestDirectory = | 
|  | mergerManifest.getParent().resolveSibling("expected-merged-permissions").toFile(); | 
|  | assertThat(expectedManifestDirectory.exists()).isTrue(); | 
|  | final String[] debug = | 
|  | expectedManifestDirectory.list(new PatternFilenameFilter(".*AndroidManifest\\.xml$")); | 
|  | assertThat(debug).isNotNull(); | 
|  | final File[] expectedManifestDirectoryManifests = | 
|  | expectedManifestDirectory.listFiles((File dir, String name) -> true); | 
|  | assertThat(expectedManifestDirectoryManifests).isNotNull(); | 
|  | assertThat(expectedManifestDirectoryManifests).hasLength(1); | 
|  | final Path expectedManifest = expectedManifestDirectoryManifests[0].toPath(); | 
|  |  | 
|  | Files.createDirectories(working.resolve("output")); | 
|  | final Path mergedManifest = working.resolve("output/mergedManifest.xml"); | 
|  |  | 
|  | List<String> args = | 
|  | generateArgs( | 
|  | mergerManifest, | 
|  | ImmutableMap.of(mergeeManifestOne, "mergeeOne", mergeeManifestTwo, "mergeeTwo"), | 
|  | false, /* isLibrary */ | 
|  | ImmutableMap.of("applicationId", "com.google.android.apps.testapp"), | 
|  | "", /* custom_package */ | 
|  | mergedManifest, | 
|  | /* mergeManifestPermissions */ true); | 
|  | ManifestMergerAction.main(args.toArray(new String[0])); | 
|  |  | 
|  | assertThat( | 
|  | Joiner.on(" ") | 
|  | .join(Files.readAllLines(mergedManifest, UTF_8)) | 
|  | .replaceAll("\\s+", " ") | 
|  | .trim()) | 
|  | .isEqualTo( | 
|  | Joiner.on(" ") | 
|  | .join(Files.readAllLines(expectedManifest, UTF_8)) | 
|  | .replaceAll("\\s+", " ") | 
|  | .trim()); | 
|  | } | 
|  |  | 
|  | @Test public void fullIntegration() throws Exception { | 
|  | Files.createDirectories(working.resolve("output")); | 
|  | final Path binaryOutput = working.resolve("output/binaryManifest.xml"); | 
|  | final Path libFooOutput = working.resolve("output/libFooManifest.xml"); | 
|  | final Path libBarOutput = working.resolve("output/libBarManifest.xml"); | 
|  |  | 
|  | final Path binaryManifest = AndroidDataBuilder.of(working.resolve("binary")) | 
|  | .createManifest("AndroidManifest.xml", "com.google.app", "") | 
|  | .buildUnvalidated() | 
|  | .getManifest(); | 
|  | final Path libFooManifest = AndroidDataBuilder.of(working.resolve("libFoo")) | 
|  | .createManifest("AndroidManifest.xml", "com.google.foo", | 
|  | " <application android:name=\"${applicationId}\" />") | 
|  | .buildUnvalidated() | 
|  | .getManifest(); | 
|  | final Path libBarManifest = AndroidDataBuilder.of(working.resolve("libBar")) | 
|  | .createManifest("AndroidManifest.xml", "com.google.bar", | 
|  | "<application android:name=\"${applicationId}\">", | 
|  | "<activity android:name=\".activityFoo\" />", | 
|  | "</application>") | 
|  | .buildUnvalidated() | 
|  | .getManifest(); | 
|  |  | 
|  | // libFoo manifest merging | 
|  | List<String> args = | 
|  | generateArgs( | 
|  | libFooManifest, | 
|  | ImmutableMap.<Path, String>of(), | 
|  | true, | 
|  | ImmutableMap.<String, String>of(), | 
|  | "", | 
|  | libFooOutput, | 
|  | false); | 
|  | ManifestMergerAction.main(args.toArray(new String[0])); | 
|  | assertThat(Joiner.on(" ") | 
|  | .join(Files.readAllLines(libFooOutput, UTF_8)) | 
|  | .replaceAll("\\s+", " ").trim()).contains( | 
|  | "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | 
|  | + "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"" | 
|  | + " package=\"com.google.foo\">" | 
|  | + " <application android:name=\"${applicationId}\" />" | 
|  | + "</manifest>"); | 
|  |  | 
|  | // libBar manifest merging | 
|  | args = | 
|  | generateArgs( | 
|  | libBarManifest, | 
|  | ImmutableMap.<Path, String>of(), | 
|  | true, | 
|  | ImmutableMap.<String, String>of(), | 
|  | "com.google.libbar", | 
|  | libBarOutput, | 
|  | false); | 
|  | ManifestMergerAction.main(args.toArray(new String[0])); | 
|  | assertThat(Joiner.on(" ") | 
|  | .join(Files.readAllLines(libBarOutput, UTF_8)) | 
|  | .replaceAll("\\s+", " ").trim()).contains( | 
|  | "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | 
|  | + " <manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"" | 
|  | + " package=\"com.google.libbar\" >" | 
|  | + " <application android:name=\"${applicationId}\" >" | 
|  | + " <activity android:name=\"com.google.bar.activityFoo\" />" | 
|  | + " </application>" | 
|  | + " </manifest>"); | 
|  |  | 
|  | // binary manifest merging | 
|  | args = | 
|  | generateArgs( | 
|  | binaryManifest, | 
|  | ImmutableMap.of(libFooOutput, "libFoo", libBarOutput, "libBar"), | 
|  | /* library= */ false, | 
|  | ImmutableMap.of( | 
|  | "applicationId", "com.google.android.app", | 
|  | "foo", "this \\\\: is \"a, \"bad string"), | 
|  | /* customPackage= */ "", | 
|  | binaryOutput, | 
|  | /* mergeManifestPermissions */ false); | 
|  | ManifestMergerAction.main(args.toArray(new String[0])); | 
|  | assertThat(Joiner.on(" ") | 
|  | .join(Files.readAllLines(binaryOutput, UTF_8)) | 
|  | .replaceAll("\\s+", " ").trim()).contains( | 
|  | "<?xml version=\"1.0\" encoding=\"utf-8\"?>" | 
|  | + " <manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"" | 
|  | + " package=\"com.google.android.app\" >" | 
|  | + " <application android:name=\"com.google.android.app\" >" | 
|  | + " <activity android:name=\"com.google.bar.activityFoo\" />" | 
|  | + " </application>" | 
|  | + " </manifest>"); | 
|  | } | 
|  |  | 
|  | private List<String> generateArgs( | 
|  | Path manifest, | 
|  | Map<Path, String> mergeeManifests, | 
|  | boolean library, | 
|  | Map<String, String> manifestValues, | 
|  | String customPackage, | 
|  | Path manifestOutput, | 
|  | boolean mergeManifestPermissions) { | 
|  | ImmutableList.Builder<String> builder = ImmutableList.builder(); | 
|  | builder.add( | 
|  | "--manifest", manifest.toString(), | 
|  | "--mergeeManifests", mapToDictionaryString(mergeeManifests)); | 
|  | if (mergeManifestPermissions) { | 
|  | builder.add("--mergeManifestPermissions"); | 
|  | } | 
|  |  | 
|  | builder.add( | 
|  | "--mergeType", | 
|  | library ? "LIBRARY" : "APPLICATION", | 
|  | "--manifestValues", | 
|  | mapToDictionaryString(manifestValues), | 
|  | "--customPackage", | 
|  | customPackage, | 
|  | "--manifestOutput", | 
|  | manifestOutput.toString()); | 
|  | return builder.build(); | 
|  | } | 
|  |  | 
|  | private <K, V> String mapToDictionaryString(Map<K, V> map) { | 
|  | StringBuilder sb = new StringBuilder(); | 
|  | Iterator<Map.Entry<K, V>> iter = map.entrySet().iterator(); | 
|  | while (iter.hasNext()) { | 
|  | Map.Entry<K, V> entry = iter.next(); | 
|  | sb.append(entry.getKey().toString().replace(":", "\\:").replace(",", "\\,")); | 
|  | sb.append(':'); | 
|  | sb.append(entry.getValue().toString().replace(":", "\\:").replace(",", "\\,")); | 
|  | if (iter.hasNext()) { | 
|  | sb.append(','); | 
|  | } | 
|  | } | 
|  | return sb.toString(); | 
|  | } | 
|  | } |