1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.dx.merge;
18 
19 import com.android.dex.Dex;
20 import com.android.dx.command.dexer.DxContext;
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.lang.annotation.Annotation;
28 import java.lang.reflect.Field;
29 import java.lang.reflect.Method;
30 import java.util.Arrays;
31 import java.util.jar.JarEntry;
32 import java.util.jar.JarOutputStream;
33 import junit.framework.TestCase;
34 
35 /**
36  * Test that DexMerge works by merging dex files, and then loading them into
37  * the current VM.
38  */
39 public final class DexMergeTest extends TestCase {
40 
testFillArrayData()41     public void testFillArrayData() throws Exception {
42         ClassLoader loader = mergeAndLoad(
43                 "/testdata/Basic.dex",
44                 "/testdata/FillArrayData.dex");
45 
46         Class<?> basic = loader.loadClass("testdata.Basic");
47         assertEquals(1, basic.getDeclaredMethods().length);
48 
49         Class<?> fillArrayData = loader.loadClass("testdata.FillArrayData");
50         assertTrue(Arrays.equals(
51                 new byte[] { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, -112, -23, 121 },
52                 (byte[]) fillArrayData.getMethod("newByteArray").invoke(null)));
53         assertTrue(Arrays.equals(
54                 new char[]{0xFFFF, 0x4321, 0xABCD, 0, 'a', 'b', 'c'},
55                 (char[]) fillArrayData.getMethod("newCharArray").invoke(null)));
56         assertTrue(Arrays.equals(
57                 new long[]{4660046610375530309L, 7540113804746346429L, -6246583658587674878L},
58                 (long[]) fillArrayData.getMethod("newLongArray").invoke(null)));
59     }
60 
testTryCatchFinally()61     public void testTryCatchFinally() throws Exception {
62         ClassLoader loader = mergeAndLoad(
63                 "/testdata/Basic.dex",
64                 "/testdata/TryCatchFinally.dex");
65 
66         Class<?> basic = loader.loadClass("testdata.Basic");
67         assertEquals(1, basic.getDeclaredMethods().length);
68 
69         Class<?> tryCatchFinally = loader.loadClass("testdata.TryCatchFinally");
70         tryCatchFinally.getDeclaredMethod("method").invoke(null);
71     }
72 
testStaticValues()73     public void testStaticValues() throws Exception {
74         ClassLoader loader = mergeAndLoad(
75                 "/testdata/Basic.dex",
76                 "/testdata/StaticValues.dex");
77 
78         Class<?> basic = loader.loadClass("testdata.Basic");
79         assertEquals(1, basic.getDeclaredMethods().length);
80 
81         Class<?> staticValues = loader.loadClass("testdata.StaticValues");
82         assertEquals((byte) 1, staticValues.getField("a").get(null));
83         assertEquals((short) 2, staticValues.getField("b").get(null));
84         assertEquals('C', staticValues.getField("c").get(null));
85         assertEquals(0xabcd1234, staticValues.getField("d").get(null));
86         assertEquals(4660046610375530309L,staticValues.getField("e").get(null));
87         assertEquals(0.5f, staticValues.getField("f").get(null));
88         assertEquals(-0.25, staticValues.getField("g").get(null));
89         assertEquals("this is a String", staticValues.getField("h").get(null));
90         assertEquals(String.class, staticValues.getField("i").get(null));
91         assertEquals("[0, 1]", Arrays.toString((int[]) staticValues.getField("j").get(null)));
92         assertEquals(null, staticValues.getField("k").get(null));
93         assertEquals(true, staticValues.getField("l").get(null));
94         assertEquals(false, staticValues.getField("m").get(null));
95     }
96 
testAnnotations()97     public void testAnnotations() throws Exception {
98         ClassLoader loader = mergeAndLoad(
99                 "/testdata/Basic.dex",
100                 "/testdata/Annotated.dex");
101 
102         Class<?> basic = loader.loadClass("testdata.Basic");
103         assertEquals(1, basic.getDeclaredMethods().length);
104 
105         Class<?> annotated = loader.loadClass("testdata.Annotated");
106         Method method = annotated.getMethod("method", String.class, String.class);
107         Field field = annotated.getField("field");
108 
109         @SuppressWarnings("unchecked")
110         Class<? extends Annotation> marker
111                 = (Class<? extends Annotation>) loader.loadClass("testdata.Annotated$Marker");
112 
113         assertEquals("@testdata.Annotated$Marker(a=on class, b=[A, B, C], "
114                 + "c=@testdata.Annotated$Nested(e=E1, f=1695938256, g=7264081114510713000), "
115                 + "d=[@testdata.Annotated$Nested(e=E2, f=1695938256, g=7264081114510713000)])",
116                 annotated.getAnnotation(marker).toString());
117         assertEquals("@testdata.Annotated$Marker(a=on method, b=[], "
118                 + "c=@testdata.Annotated$Nested(e=, f=0, g=0), d=[])",
119                 method.getAnnotation(marker).toString());
120         assertEquals("@testdata.Annotated$Marker(a=on field, b=[], "
121                 + "c=@testdata.Annotated$Nested(e=, f=0, g=0), d=[])",
122                 field.getAnnotation(marker).toString());
123         assertEquals("@testdata.Annotated$Marker(a=on parameter, b=[], "
124                         + "c=@testdata.Annotated$Nested(e=, f=0, g=0), d=[])",
125                 method.getParameterAnnotations()[1][0].toString());
126     }
127 
128     /**
129      * Merging dex files uses pessimistic sizes that naturally leave gaps in the
130      * output files. If those gaps grow too large, the merger is supposed to
131      * compact the result. This exercises that by repeatedly merging a dex with
132      * itself.
133      */
testMergedOutputSizeIsBounded()134     public void testMergedOutputSizeIsBounded() throws Exception {
135         /*
136          * At the time this test was written, the output would grow ~25% with
137          * each merge. Setting a low 1KiB ceiling on the maximum size caused
138          * the file to be compacted every four merges.
139          */
140         int steps = 100;
141         int compactWasteThreshold = 1024;
142 
143         Dex dexA = resourceToDexBuffer("/testdata/Basic.dex");
144         Dex dexB = resourceToDexBuffer("/testdata/TryCatchFinally.dex");
145         Dex merged = new DexMerger(new Dex[]{dexA, dexB}, CollisionPolicy.KEEP_FIRST,
146                                    new DxContext()).merge();
147 
148         int maxLength = 0;
149         for (int i = 0; i < steps; i++) {
150             DexMerger dexMerger = new DexMerger(new Dex[]{dexA, merged},
151                                                 CollisionPolicy.KEEP_FIRST, new DxContext());
152             dexMerger.setCompactWasteThreshold(compactWasteThreshold);
153             merged = dexMerger.merge();
154             maxLength = Math.max(maxLength, merged.getLength());
155         }
156 
157         int maxExpectedLength = dexA.getLength() + dexB.getLength() + compactWasteThreshold;
158         assertTrue(maxLength + " < " + maxExpectedLength, maxLength < maxExpectedLength);
159     }
160 
mergeAndLoad(String dexAResource, String dexBResource)161     public ClassLoader mergeAndLoad(String dexAResource, String dexBResource) throws Exception {
162         Dex dexA = resourceToDexBuffer(dexAResource);
163         Dex dexB = resourceToDexBuffer(dexBResource);
164         Dex merged = new DexMerger(new Dex[]{dexA, dexB}, CollisionPolicy.KEEP_FIRST,
165                                    new DxContext()).merge();
166         File mergedDex = File.createTempFile("DexMergeTest", ".classes.dex");
167         merged.writeTo(mergedDex);
168         File mergedJar = dexToJar(mergedDex);
169         // simplify the javac classpath by not depending directly on 'dalvik.system' classes
170         return (ClassLoader) Class.forName("dalvik.system.PathClassLoader")
171                 .getConstructor(String.class, ClassLoader.class)
172                 .newInstance(mergedJar.getPath(), getClass().getClassLoader());
173     }
174 
resourceToDexBuffer(String resource)175     private Dex resourceToDexBuffer(String resource) throws IOException {
176         return new Dex(getClass().getResourceAsStream(resource));
177     }
178 
dexToJar(File dex)179     private File dexToJar(File dex) throws IOException {
180         File result = File.createTempFile("DexMergeTest", ".jar");
181         result.deleteOnExit();
182         JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(result));
183         jarOut.putNextEntry(new JarEntry("classes.dex"));
184         copy(new FileInputStream(dex), jarOut);
185         jarOut.closeEntry();
186         jarOut.close();
187         return result;
188     }
189 
copy(InputStream in, OutputStream out)190     private void copy(InputStream in, OutputStream out) throws IOException {
191         byte[] buffer = new byte[1024];
192         int count;
193         while ((count = in.read(buffer)) != -1) {
194             out.write(buffer, 0, count);
195         }
196         in.close();
197     }
198 }
199