From 5a330002c57adaf8a6d4180618132c75bfcbb7fd Mon Sep 17 00:00:00 2001 From: ajmichael Date: Mon, 26 Feb 2018 08:58:55 -0800 Subject: Change android_library .aar timestamp and open-source AarGeneratorActionTest. See https://github.com/bazelbuild/bazel/issues/4614 and https://github.com/bazelbuild/bazel/commit/bcefd9833cb5620fef8a27c37c2808a66b57c7e6. RELNOTES: None PiperOrigin-RevId: 187029042 --- .../build/android/AarGeneratorActionTest.java | 680 +++++++++++++++++++++ .../java/com/google/devtools/build/android/BUILD | 15 + 2 files changed, 695 insertions(+) create mode 100644 src/test/java/com/google/devtools/build/android/AarGeneratorActionTest.java (limited to 'src/test/java/com/google/devtools/build/android') diff --git a/src/test/java/com/google/devtools/build/android/AarGeneratorActionTest.java b/src/test/java/com/google/devtools/build/android/AarGeneratorActionTest.java new file mode 100644 index 0000000000..347d38dde6 --- /dev/null +++ b/src/test/java/com/google/devtools/build/android/AarGeneratorActionTest.java @@ -0,0 +1,680 @@ +// Copyright 2018 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.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertNotNull; + +import com.android.builder.core.VariantType; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.devtools.build.android.AarGeneratorAction.AarGeneratorOptions; +import com.google.devtools.common.options.OptionsParser; +import com.google.devtools.common.options.OptionsParsingException; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link AarGeneratorAction}. */ +@RunWith(JUnit4.class) +public class AarGeneratorActionTest { + + private static class AarData { + /** Templates for resource files generation. */ + enum ResourceType { + VALUE { + @Override public String create(String... lines) { + return String.format( + "\n%s", + Joiner.on("\n").join(lines)); + } + }, + LAYOUT { + @Override public String create(String... lines) { + return String.format("" + + "%s", + Joiner.on("\n").join(lines)); + } + }, + UNFORMATTED { + @Override public String create(String... lines) { + return String.format(Joiner.on("\n").join(lines)); + } + }; + + public abstract String create(String... lines); + } + + private static class Builder { + + private final Path root; + private final Path assetDir; + private final Path resourceDir; + private Path manifest; + private Path rtxt; + private Path classes; + private Map filesToWrite = new HashMap<>(); + private Map classesToWrite = new HashMap<>(); + private boolean withEmptyRes = false; + private boolean withEmptyAssets = false; + + public Builder(Path root) { + this(root, "res", "assets"); + } + + public Builder(Path root, String resourceRoot, String assetRoot) { + this.root = root; + assetDir = root.resolve(assetRoot); + resourceDir = root.resolve(resourceRoot); + manifest = root.resolve("fake-manifest-path"); + rtxt = root.resolve("fake-rtxt-path"); + classes = root.resolve("fake-classes-path"); + } + + public Builder addResource(String path, ResourceType template, String... lines) { + filesToWrite.put(resourceDir.resolve(path), template.create(lines)); + return this; + } + + public Builder withEmptyResources(boolean isEmpty) { + this.withEmptyRes = isEmpty; + return this; + } + + public Builder addAsset(String path, String... lines) { + filesToWrite.put(assetDir.resolve(path), Joiner.on("\n").join(lines)); + return this; + } + + public Builder withEmptyAssets(boolean isEmpty) { + this.withEmptyAssets = isEmpty; + return this; + } + + public Builder createManifest(String path, String manifestPackage, String... lines) { + this.manifest = root.resolve(path); + filesToWrite.put(manifest, String.format("" + + "" + + "%s", manifestPackage, Joiner.on("\n").join(lines))); + return this; + } + + public Builder createRtxt(String path, String... lines) { + this.rtxt = root.resolve(path); + filesToWrite.put(rtxt, String.format("%s", Joiner.on("\n").join(lines))); + return this; + } + + public Builder createClassesJar(String path) { + this.classes = root.resolve(path); + classesToWrite.put("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n"); + return this; + } + + public Builder addClassesFile(String filePackage, String filename, String... lines) { + classesToWrite.put(filePackage.replace(".", "/") + "/" + filename, + String.format("%s", Joiner.on("\n").join(lines))); + return this; + } + + public AarData build() throws IOException { + writeFiles(); + return new AarData(buildMerged(), manifest, rtxt, classes); + } + + private MergedAndroidData buildMerged() { + return new MergedAndroidData( + resourceDir, + assetDir, + manifest); + } + + private void writeFiles() throws IOException { + assertNotNull("A manifest is required.", manifest); + assertNotNull("A resource file is required.", rtxt); + assertNotNull("A classes jar is required.", classes); + if (withEmptyRes) { + Files.createDirectories(resourceDir); + } + if (withEmptyAssets) { + Files.createDirectories(assetDir); + } + for (Entry entry : filesToWrite.entrySet()) { + Path file = entry.getKey(); + // only write files in assets if assets has not been set to empty and same for resources + if (!((file.startsWith(assetDir) && withEmptyAssets) + || (file.startsWith(resourceDir) && withEmptyRes))) { + Files.createDirectories(file.getParent()); + Files.write(file, entry.getValue().getBytes(StandardCharsets.UTF_8)); + assertThat(Files.exists(file)).isTrue(); + } + } + if (!classesToWrite.isEmpty()) { + writeClassesJar(); + } + } + + private void writeClassesJar() throws IOException { + final ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(classes.toFile())); + + for (Entry file : classesToWrite.entrySet()) { + ZipEntry entry = new ZipEntry(file.getKey()); + zout.putNextEntry(entry); + zout.write(file.getValue().getBytes(UTF_8)); + zout.closeEntry(); + } + + zout.close(); + + classes.toFile().setLastModified(AarGeneratorAction.DEFAULT_TIMESTAMP); + } + } + + final MergedAndroidData data; + final Path manifest; + final Path rtxt; + final Path classes; + + private AarData(MergedAndroidData data, Path manifest, Path rtxt, Path classes) { + this.data = data; + this.manifest = manifest; + this.rtxt = rtxt; + this.classes = classes; + } + } + + /** + * Operation to perform on a file. + */ + private interface FileOperation { + /** + * Performs the operation on a file, given its name, modificationTime and contents. + */ + void perform(String name, long modificationTime, String contents); + } + + /** + * Runs a {@link FileOperation} on every entry in a zip file. + * + * @param zip {@link Path} of the zip file to traverse. + * @param operation {@link FileOperation} to be run on every entry of the zip file. + * @throws IOException if there is an error reading the zip file. + */ + private void traverseZipFile(Path zip, FileOperation operation) throws IOException { + ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip)); + ZipEntry z = zis.getNextEntry(); + while (z != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int count = 0; count != -1; count = zis.read(buffer)) { + baos.write(buffer); + } + // Replace Windows path separators so that test cases are consistent across platforms. + String name = z.getName().replace('\\', '/'); + operation.perform( + name, z.getTime(), new String(baos.toByteArray(), StandardCharsets.UTF_8)); + z = zis.getNextEntry(); + } + } + + private Set getZipEntries(Path zip) throws IOException { + final Set zipEntries = new HashSet<>(); + traverseZipFile(zip, new FileOperation() { + @Override public void perform(String name, long modificationTime, String contents) { + zipEntries.add(name); + } + }); + return zipEntries; + } + + private Set getZipEntryTimestamps(Path zip) throws IOException { + final Set timestamps = new HashSet<>(); + traverseZipFile(zip, new FileOperation() { + @Override public void perform(String name, long modificationTime, String contents) { + timestamps.add(modificationTime); + } + }); + return timestamps; + } + + private Path tempDir; + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Before public void setUp() throws IOException { + tempDir = Files.createTempDirectory(toString()); + tempDir.toFile().deleteOnExit(); + + } + + @Test public void testCheckFlags() throws IOException, OptionsParsingException { + Path manifest = tempDir.resolve("AndroidManifest.xml"); + Files.createFile(manifest); + Path rtxt = tempDir.resolve("R.txt"); + Files.createFile(rtxt); + Path classes = tempDir.resolve("classes.jar"); + Files.createFile(classes); + + String[] args = new String[] {"--manifest", manifest.toString(), "--rtxt", rtxt.toString(), + "--classes", classes.toString()}; + OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class); + optionsParser.parse(args); + AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class); + AarGeneratorAction.checkFlags(options); + } + + @Test public void testCheckFlags_MissingClasses() throws IOException, OptionsParsingException { + Path manifest = tempDir.resolve("AndroidManifest.xml"); + Files.createFile(manifest); + Path rtxt = tempDir.resolve("R.txt"); + Files.createFile(rtxt); + + String[] args = new String[] {"--manifest", manifest.toString(), "--rtxt", rtxt.toString()}; + OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class); + optionsParser.parse(args); + AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("classes must be specified. Building an .aar without" + + " classes is unsupported."); + AarGeneratorAction.checkFlags(options); + } + + @Test public void testCheckFlags_MissingMultiple() throws IOException, OptionsParsingException { + Path manifest = tempDir.resolve("AndroidManifest.xml"); + Files.createFile(manifest); + String[] args = new String[] {"--manifest", manifest.toString()}; + OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class); + optionsParser.parse(args); + AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class); + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("rtxt, classes must be specified. Building an .aar without" + + " rtxt, classes is unsupported."); + AarGeneratorAction.checkFlags(options); + } + + @Test public void testWriteAar() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testWriteAar_DefaultTimestamps() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + + assertThat(getZipEntryTimestamps(aar)).containsExactly(AarGeneratorAction.DEFAULT_TIMESTAMP); + assertThat(aar.toFile().lastModified()).isEqualTo(AarGeneratorAction.DEFAULT_TIMESTAMP); + } + + @Test public void testAssetResourceSubdirs() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data"), "xyz", "assets") + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + + // verify aar archive + Set zipEntries = getZipEntries(aar); + assertThat(zipEntries).contains("res/"); + assertThat(zipEntries).contains("assets/"); + } + + @Test public void testMissingManifest() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage(containsString("fake-manifest-path")); + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testMissingRtxt() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage(containsString("fake-rtxt-path")); + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testMissingClasses() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage(containsString("fake-classes-path")); + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testMissingResources() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + thrown.expect(IOException.class); + thrown.expectMessage(containsString("res")); + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testEmptyResources() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .withEmptyResources(true) + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addAsset("some/other/ft/data.txt", "bar") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testMissingAssets() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testEmptyAssets() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .withEmptyAssets(true) + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + AarGeneratorAction.writeAar(aar, + aarData.data, + aarData.manifest, + aarData.rtxt, + aarData.classes); + } + + @Test public void testFullIntegration() throws Exception { + Path aar = tempDir.resolve("foo.aar"); + AarData aarData = new AarData.Builder(tempDir.resolve("data")) + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo", "") + .createRtxt("R.txt", + "int string app_name 0x7f050001", + "int string hello_world 0x7f050002") + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addResource("layout/layout.xml", + AarData.ResourceType.LAYOUT, + "") + .addAsset("some/other/ft/data.txt", + "foo") + .createClassesJar("classes.jar") + .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents") + .build(); + + MergedAndroidData md1 = new AarData.Builder(tempDir.resolve("d1")) + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addResource("layout/foo.xml", + AarData.ResourceType.LAYOUT, + "") + .addAsset("some/other/ft/data1.txt", + "bar") + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "") + .build().data; + + MergedAndroidData md2 = new AarData.Builder(tempDir.resolve("d2")) + .addResource("values/ids.xml", + AarData.ResourceType.VALUE, + "") + .addResource("layout/bar.xml", + AarData.ResourceType.LAYOUT, + "") + .addResource("drawable-mdpi/icon.png", + AarData.ResourceType.UNFORMATTED, + "Thttpt.") + .addResource("drawable-xxxhdpi/icon.png", + AarData.ResourceType.UNFORMATTED, + "Double Thttpt.") + .addAsset("some/other/ft/data2.txt", + "foo") + .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d2", "") + .build().data; + + UnvalidatedAndroidData primary = new UnvalidatedAndroidData( + ImmutableList.of(aarData.data.getResourceDir()), + ImmutableList.of(aarData.data.getAssetDir()), + aarData.data.getManifest()); + + DependencyAndroidData d1 = + new DependencyAndroidData( + ImmutableList.of(md1.getResourceDir()), + ImmutableList.of(md1.getAssetDir()), + md1.getManifest(), + null, + null, + null); + + DependencyAndroidData d2 = + new DependencyAndroidData( + ImmutableList.of(md2.getResourceDir()), + ImmutableList.of(md2.getAssetDir()), + md2.getManifest(), + null, + null, + null); + + Path working = tempDir; + + Path resourcesOut = working.resolve("resources"); + Path assetsOut = working.resolve("assets"); + + MergedAndroidData mergedData = + AndroidResourceMerger.mergeData( + primary, + ImmutableList.of(d1, d2), + ImmutableList.of(), + resourcesOut, + assetsOut, + null, + VariantType.LIBRARY, + null, + /* filteredResources= */ ImmutableList.of(), + true); + + AarGeneratorAction.writeAar(aar, mergedData, aarData.manifest, aarData.rtxt, aarData.classes); + + // verify aar archive + Set zipEntries = getZipEntries(aar); + assertThat(zipEntries).containsExactly( + "AndroidManifest.xml", + "R.txt", + "classes.jar", + "res/", + "res/values/", + "res/values/values.xml", + "res/layout/", + "res/layout/layout.xml", + "res/layout/foo.xml", + "res/layout/bar.xml", + "res/drawable-mdpi-v4/", + "res/drawable-mdpi-v4/icon.png", + "res/drawable-xxxhdpi-v4/", + "res/drawable-xxxhdpi-v4/icon.png", + "assets/", + "assets/some/", + "assets/some/other/", + "assets/some/other/ft/", + "assets/some/other/ft/data.txt", + "assets/some/other/ft/data1.txt", + "assets/some/other/ft/data2.txt"); + } +} diff --git a/src/test/java/com/google/devtools/build/android/BUILD b/src/test/java/com/google/devtools/build/android/BUILD index cb5f346c9b..af7082fcd6 100644 --- a/src/test/java/com/google/devtools/build/android/BUILD +++ b/src/test/java/com/google/devtools/build/android/BUILD @@ -12,6 +12,21 @@ filegroup( visibility = ["//src:__pkg__"], ) +java_test( + name = "AarGeneratorActionTest", + size = "small", + srcs = ["AarGeneratorActionTest.java"], + deps = [ + ":test_utils", + "//src/main/java/com/google/devtools/common/options", + "//src/tools/android/java/com/google/devtools/build/android:android_builder_lib", + "//third_party:android_common_25_0_0", + "//third_party:guava", + "//third_party:junit4", + "//third_party:truth", + ], +) + java_test( name = "AndroidResourceOutputsTest", size = "small", -- cgit v1.2.3