| // Copyright 2016 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.resources; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import com.android.SdkConstants; |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.common.util.concurrent.ListeningExecutorService; |
| import com.google.common.util.concurrent.MoreExecutors; |
| import com.google.devtools.build.android.resources.JavaIdentifierValidator.InvalidJavaIdentifier; |
| import java.io.IOException; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.Modifier; |
| import java.net.URL; |
| import java.net.URLClassLoader; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.DirectoryStream; |
| import java.nio.file.FileAlreadyExistsException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.List; |
| import java.util.concurrent.ExecutionException; |
| import org.hamcrest.BaseMatcher; |
| import org.hamcrest.Description; |
| import org.hamcrest.Matcher; |
| 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 RClassGenerator}. */ |
| @RunWith(JUnit4.class) |
| public class RClassGeneratorTest { |
| |
| private Path temp; |
| @Rule public final ExpectedException thrown = ExpectedException.none(); |
| |
| @Before |
| public void setUp() throws Exception { |
| temp = Files.createTempDirectory(toString()); |
| } |
| |
| @Test |
| public void plainInts() throws Exception { |
| checkSimpleInts(true); |
| } |
| |
| @Test |
| public void nonFinalFields() throws Exception { |
| checkSimpleInts(false); |
| } |
| |
| private void checkSimpleInts(boolean finalFields) throws Exception { |
| // R.txt with the real IDs after linking together libraries. |
| ResourceSymbols symbolValues = |
| createSymbolFile( |
| "R.txt", |
| "int attr agility 0x7f010000", |
| "int attr dexterity 0x7f010001", |
| "int drawable heart 0x7f020000", |
| "int id someTextView 0x7f080000", |
| "int integer maxNotifications 0x7f090000", |
| "int string alphabet 0x7f100000", |
| "int string ok 0x7f100001"); |
| // R.txt for the library, where the values are not the final ones (so ignore them). We only use |
| // this to keep the # of inner classes small (exactly the set needed by the library). |
| ResourceSymbols symbolsInLibrary = |
| createSymbolFile( |
| "lib.R.txt", "int attr agility 0x1", "int id someTextView 0x1", "int string ok 0x1"); |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("com.bar", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve("com/bar"); |
| checkFilesInPackage(packageDir, "R.class", "R$attr.class", "R$id.class", "R$string.class"); |
| Class<?> outerClass = |
| checkTopLevelClass(out, "com.bar.R", "com.bar.R$attr", "com.bar.R$id", "com.bar.R$string"); |
| checkInnerClass( |
| out, |
| "com.bar.R$attr", |
| outerClass, |
| ImmutableMap.of("agility", 0x7f010000), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| checkInnerClass( |
| out, |
| "com.bar.R$id", |
| outerClass, |
| ImmutableMap.of("someTextView", 0x7f080000), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| checkInnerClass( |
| out, |
| "com.bar.R$string", |
| outerClass, |
| ImmutableMap.of("ok", 0x7f100001), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| } |
| |
| @Test |
| public void checkFileWriteThrowsOnExisting() throws Exception { |
| checkFileWriteThrowsOnExisting(SdkConstants.FN_COMPILED_RESOURCE_CLASS); |
| } |
| |
| private void checkFileWriteThrowsOnExisting(String existingFile) throws Exception { |
| ResourceSymbols symbolValues = createSymbolFile("R.txt", "int string ok 0x7f100001"); |
| ResourceSymbols symbolsInLibrary = createSymbolFile("lib.R.txt", "int string ok 0x1"); |
| |
| Path out = temp.resolve("classes"); |
| String packageName = "com"; |
| Path packageFolder = out.resolve(packageName); |
| Files.createDirectories(packageFolder); |
| |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), false); |
| Files.write(packageFolder.resolve(existingFile), new byte[0]); |
| |
| try { |
| writer.write(packageName, symbolsInLibrary.asInitializers()); |
| } catch (FileAlreadyExistsException e) { |
| return; |
| } |
| throw new Exception("Expected to throw a FileAlreadyExistsException"); |
| } |
| |
| @Test |
| public void checkInnerFileWriteThrowsOnExisting() throws Exception { |
| checkFileWriteThrowsOnExisting("R$string.class"); |
| } |
| |
| @Test |
| public void emptyIntArrays() throws Exception { |
| boolean finalFields = true; |
| // Make sure we parse an empty array the way the R.txt writes it. |
| ResourceSymbols symbolValues = createSymbolFile("R.txt", "int[] styleable ActionMenuView { }"); |
| ResourceSymbols symbolsInLibrary = symbolValues; |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("com.testEmptyIntArray", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve("com/testEmptyIntArray"); |
| checkFilesInPackage(packageDir, "R.class", "R$styleable.class"); |
| Class<?> outerClass = |
| checkTopLevelClass(out, "com.testEmptyIntArray.R", "com.testEmptyIntArray.R$styleable"); |
| checkInnerClass( |
| out, |
| "com.testEmptyIntArray.R$styleable", |
| outerClass, |
| ImmutableMap.<String, Integer>of(), |
| ImmutableMap.<String, List<Integer>>of("ActionMenuView", ImmutableList.<Integer>of()), |
| finalFields); |
| } |
| |
| static final Matcher<Throwable> NUMBER_FORMAT_EXCEPTION = |
| new BaseMatcher<Throwable>() { |
| @Override |
| public boolean matches(Object item) { |
| if (item instanceof NumberFormatException) { |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public void describeTo(Description description) { |
| description.appendText(NumberFormatException.class.toString()); |
| } |
| }; |
| |
| static final Matcher<Throwable> INVALID_JAVA_IDENTIFIER = |
| new BaseMatcher<Throwable>() { |
| @Override |
| public boolean matches(Object item) { |
| return item instanceof InvalidJavaIdentifier; |
| } |
| |
| @Override |
| public void describeTo(Description description) { |
| description.appendText(InvalidJavaIdentifier.class.getName()); |
| } |
| }; |
| |
| @Test |
| public void corruptIntArraysTrailingComma() throws Exception { |
| // Test a few cases of what happens if the R.txt is corrupted. It shouldn't happen unless there |
| // is a bug in aapt, or R.txt is manually written the wrong way. |
| Path path = createFile("R.txt", new String[] {"int[] styleable ActionMenuView { 1, }"}); |
| thrown.expectCause(NUMBER_FORMAT_EXCEPTION); |
| ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); |
| } |
| |
| @Test |
| public void corruptIntArraysOmittedMiddle() throws Exception { |
| Path path = createFile("R.txt", "int[] styleable ActionMenuView { 1, , 2 }"); |
| thrown.expectCause(NUMBER_FORMAT_EXCEPTION); |
| ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); |
| } |
| |
| @Test |
| public void invalidJavaIdentifierNumber() throws Exception { |
| Path path = createFile("R.txt", "int id 42ActionMenuView 0x7f020000"); |
| final ResourceSymbols resourceSymbols = |
| ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); |
| Path out = Files.createDirectories(temp.resolve("classes")); |
| thrown.expect(INVALID_JAVA_IDENTIFIER); |
| RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); |
| } |
| |
| @Test |
| public void invalidJavaIdentifierColon() throws Exception { |
| Path path = createFile("R.txt", "int id Action:MenuView 0x7f020000"); |
| final ResourceSymbols resourceSymbols = |
| ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); |
| Path out = Files.createDirectories(temp.resolve("classes")); |
| thrown.expect(INVALID_JAVA_IDENTIFIER); |
| RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); |
| } |
| |
| @Test |
| public void reservedJavaIdentifier() throws Exception { |
| Path path = createFile("R.txt", "int id package 0x7f020000"); |
| final ResourceSymbols resourceSymbols = |
| ResourceSymbols.load(path, MoreExecutors.newDirectExecutorService()).get(); |
| Path out = Files.createDirectories(temp.resolve("classes")); |
| thrown.expect(INVALID_JAVA_IDENTIFIER); |
| RClassGenerator.with(out, resourceSymbols.asInitializers(), true).write("somepackage"); |
| } |
| |
| @Test |
| public void binaryDropsLibraryFields() throws Exception { |
| boolean finalFields = true; |
| // Test what happens if the binary R.txt is not a strict superset of the |
| // library R.txt (overrides that drop elements). |
| ResourceSymbols symbolValues = |
| createSymbolFile("R.txt", "int layout stubbable_activity 0x7f020000"); |
| ResourceSymbols symbolsInLibrary = |
| createSymbolFile( |
| "lib.R.txt", |
| "int id debug_text_field 0x1", |
| "int id debug_text_field2 0x1", |
| "int layout stubbable_activity 0x1"); |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("com.foo", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve("com/foo"); |
| checkFilesInPackage(packageDir, "R.class", "R$layout.class"); |
| Class<?> outerClass = checkTopLevelClass(out, "com.foo.R", "com.foo.R$layout"); |
| checkInnerClass( |
| out, |
| "com.foo.R$layout", |
| outerClass, |
| ImmutableMap.of("stubbable_activity", 0x7f020000), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| } |
| |
| @Test |
| public void writeNothingWithNoResources() throws Exception { |
| boolean finalFields = true; |
| // Test what happens if the library R.txt has no elements. |
| ResourceSymbols symbolValues = |
| createSymbolFile("R.txt", "int layout stubbable_activity 0x7f020000"); |
| ResourceSymbols symbolsInLibrary = createSymbolFile("lib.R.txt"); |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("com.foo", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve("com/foo"); |
| |
| checkFilesInPackage(packageDir); |
| } |
| |
| @Test |
| public void intArraysFinal() throws Exception { |
| checkIntArrays(true); |
| } |
| |
| @Test |
| public void intArraysNonFinal() throws Exception { |
| checkIntArrays(false); |
| } |
| |
| public void checkIntArrays(boolean finalFields) throws Exception { |
| ResourceSymbols symbolValues = |
| createSymbolFile( |
| "R.txt", |
| "int attr android_layout 0x010100f2", |
| "int attr bar 0x7f010001", |
| "int attr baz 0x7f010002", |
| "int attr fox 0x7f010003", |
| "int attr attr 0x7f010004", |
| "int attr another_attr 0x7f010005", |
| "int attr zoo 0x7f010006", |
| // Test several > 5 elements, clinit must use bytecodes other than iconst_0 to 5. |
| "int[] styleable ActionButton { 0x010100f2, 0x7f010001, 0x7f010002, 0x7f010003, " |
| + "0x7f010004, 0x7f010005, 0x7f010006 }", |
| // The array indices of each attribute. |
| "int styleable ActionButton_android_layout 0", |
| "int styleable ActionButton_another_attr 5", |
| "int styleable ActionButton_attr 4", |
| "int styleable ActionButton_bar 1", |
| "int styleable ActionButton_baz 2", |
| "int styleable ActionButton_fox 3", |
| "int styleable ActionButton_zoo 6"); |
| ResourceSymbols symbolsInLibrary = symbolValues; |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("com.intArray", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve("com/intArray"); |
| checkFilesInPackage(packageDir, "R.class", "R$attr.class", "R$styleable.class"); |
| Class<?> outerClass = |
| checkTopLevelClass( |
| out, "com.intArray.R", "com.intArray.R$attr", "com.intArray.R$styleable"); |
| checkInnerClass( |
| out, |
| "com.intArray.R$attr", |
| outerClass, |
| ImmutableMap.<String, Integer>builder() |
| .put("android_layout", 0x010100f2) |
| .put("bar", 0x7f010001) |
| .put("baz", 0x7f010002) |
| .put("fox", 0x7f010003) |
| .put("attr", 0x7f010004) |
| .put("another_attr", 0x7f010005) |
| .put("zoo", 0x7f010006) |
| .build(), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| checkInnerClass( |
| out, |
| "com.intArray.R$styleable", |
| outerClass, |
| ImmutableMap.<String, Integer>builder() |
| .put("ActionButton_android_layout", 0) |
| .put("ActionButton_bar", 1) |
| .put("ActionButton_baz", 2) |
| .put("ActionButton_fox", 3) |
| .put("ActionButton_attr", 4) |
| .put("ActionButton_another_attr", 5) |
| .put("ActionButton_zoo", 6) |
| .build(), |
| ImmutableMap.<String, List<Integer>>of( |
| "ActionButton", |
| ImmutableList.of( |
| 0x010100f2, |
| 0x7f010001, |
| 0x7f010002, |
| 0x7f010003, |
| 0x7f010004, |
| 0x7f010005, |
| 0x7f010006)), |
| finalFields); |
| } |
| |
| @Test |
| public void emptyPackage() throws Exception { |
| boolean finalFields = true; |
| // Make sure we handle an empty package string. |
| ResourceSymbols symbolValues = createSymbolFile("R.txt", "int string some_string 0x7f200000"); |
| ResourceSymbols symbolsInLibrary = symbolValues; |
| Path out = temp.resolve("classes"); |
| Files.createDirectories(out); |
| RClassGenerator writer = RClassGenerator.with(out, symbolValues.asInitializers(), finalFields); |
| writer.write("", symbolsInLibrary.asInitializers()); |
| |
| Path packageDir = out.resolve(""); |
| checkFilesInPackage(packageDir, "R.class", "R$string.class"); |
| Class<?> outerClass = checkTopLevelClass(out, "R", "R$string"); |
| checkInnerClass( |
| out, |
| "R$string", |
| outerClass, |
| ImmutableMap.of("some_string", 0x7f200000), |
| ImmutableMap.<String, List<Integer>>of(), |
| finalFields); |
| } |
| |
| // Test utilities |
| |
| private Path createFile(String name, String... contents) throws IOException { |
| Path path = temp.resolve(name); |
| Files.createDirectories(path.getParent()); |
| Files.newOutputStream(path) |
| .write(Joiner.on("\n").join(contents).getBytes(StandardCharsets.UTF_8)); |
| return path; |
| } |
| |
| private ResourceSymbols createSymbolFile(String name, String... contents) |
| throws IOException, InterruptedException, ExecutionException { |
| Path path = createFile(name, contents); |
| ListeningExecutorService executorService = MoreExecutors.newDirectExecutorService(); |
| ResourceSymbols symbolFile = ResourceSymbols.load(path, executorService).get(); |
| return symbolFile; |
| } |
| |
| private static void checkFilesInPackage(Path packageDir, String... expectedFiles) |
| throws IOException { |
| try (DirectoryStream<Path> stream = Files.newDirectoryStream(packageDir)) { |
| ImmutableList<String> filesInPackage = |
| ImmutableList.copyOf( |
| Iterables.transform( |
| stream, |
| new Function<Path, String>() { |
| @Override |
| public String apply(Path path) { |
| return path.getFileName().toString(); |
| } |
| })); |
| assertThat(filesInPackage).containsExactly((Object[]) expectedFiles); |
| } |
| } |
| |
| private static Class<?> checkTopLevelClass( |
| Path baseDir, String expectedClassName, String... expectedInnerClasses) throws Exception { |
| try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {baseDir.toUri().toURL()})) { |
| Class<?> toplevelClass = urlClassLoader.loadClass(expectedClassName); |
| assertThat(toplevelClass.getSuperclass()).isEqualTo(Object.class); |
| int outerModifiers = toplevelClass.getModifiers(); |
| assertThat(Modifier.isFinal(outerModifiers)).isTrue(); |
| assertThat(Modifier.isPublic(outerModifiers)).isTrue(); |
| ImmutableList.Builder<String> actualClasses = ImmutableList.builder(); |
| for (Class<?> innerClass : toplevelClass.getClasses()) { |
| assertThat(innerClass.getDeclaredClasses()).isEmpty(); |
| int modifiers = innerClass.getModifiers(); |
| assertThat(Modifier.isFinal(modifiers)).isTrue(); |
| assertThat(Modifier.isPublic(modifiers)).isTrue(); |
| assertThat(Modifier.isStatic(modifiers)).isTrue(); |
| actualClasses.add(innerClass.getName()); |
| } |
| assertThat(actualClasses.build()).containsExactly((Object[]) expectedInnerClasses); |
| return toplevelClass; |
| } |
| } |
| |
| private void checkInnerClass( |
| Path baseDir, |
| String expectedClassName, |
| Class<?> outerClass, |
| ImmutableMap<String, Integer> intFields, |
| ImmutableMap<String, List<Integer>> intArrayFields, |
| boolean areFieldsFinal) |
| throws Exception { |
| try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {baseDir.toUri().toURL()})) { |
| Class<?> innerClass = urlClassLoader.loadClass(expectedClassName); |
| assertThat(innerClass.getSuperclass()).isEqualTo(Object.class); |
| assertThat(innerClass.getEnclosingClass().toString()).isEqualTo(outerClass.toString()); |
| ImmutableMap.Builder<String, Integer> actualIntFields = ImmutableMap.builder(); |
| ImmutableMap.Builder<String, List<Integer>> actualIntArrayFields = ImmutableMap.builder(); |
| for (Field f : innerClass.getFields()) { |
| int fieldModifiers = f.getModifiers(); |
| assertThat(Modifier.isFinal(fieldModifiers)).isEqualTo(areFieldsFinal); |
| assertThat(Modifier.isPublic(fieldModifiers)).isTrue(); |
| assertThat(Modifier.isStatic(fieldModifiers)).isTrue(); |
| |
| Class<?> fieldType = f.getType(); |
| if (fieldType.isPrimitive()) { |
| assertThat(fieldType).isEqualTo(Integer.TYPE); |
| actualIntFields.put(f.getName(), (Integer) f.get(null)); |
| } else { |
| assertThat(fieldType.isArray()).isTrue(); |
| int[] asArray = (int[]) f.get(null); |
| ImmutableList.Builder<Integer> list = ImmutableList.builder(); |
| for (int i : asArray) { |
| list.add(i); |
| } |
| actualIntArrayFields.put(f.getName(), list.build()); |
| } |
| } |
| assertThat(actualIntFields.build()).containsExactlyEntriesIn(intFields); |
| assertThat(actualIntArrayFields.build()).containsExactlyEntriesIn(intArrayFields); |
| } |
| } |
| } |