BridgeMethodTestCase.java revision 3170:dc017a37aac5
1/*
2 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import java.io.File;
25import java.io.IOException;
26import java.lang.reflect.Method;
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.HashMap;
30import java.util.List;
31import java.util.Map;
32import java.util.StringJoiner;
33
34import org.testng.annotations.BeforeMethod;
35import org.testng.annotations.Test;
36
37import tools.javac.combo.*;
38
39import static org.testng.Assert.fail;
40
41/**
42 * BridgeMethodTestCase -- used for asserting linkage to bridges under separate compilation.
43 *
44 * Example test case:
45 *     public void test1() throws IOException, ReflectiveOperationException {
46 *         compileSpec("C(Bc1(A))");
47 *         assertLinkage("C", LINKAGE_ERROR, "B1");
48 *         recompileSpec("C(Bc1(Ac0))", "A");
49 *          assertLinkage("C", "A0", "B1");
50 *     }
51 *
52 * This compiles A, B, and C, asserts that C.m()Object does not exist, asserts
53 * that C.m()Number eventually invokes B.m()Number, recompiles B, and then asserts
54 * that the result of calling C.m()Object now arrives at A.
55 *
56 * @author Brian Goetz
57 */
58
59@Test
60public abstract class BridgeMethodTestCase extends JavacTemplateTestBase {
61
62    private static final String TYPE_LETTERS = "ABCDIJK";
63
64    private static final String BASE_INDEX_CLASS = "class C0 {\n" +
65                                                   "    int val;\n" +
66                                                   "    C0(int val) { this.val = val; }\n" +
67                                                   "    public int getVal() { return val; }\n" +
68                                                   "}\n";
69    private static final String INDEX_CLASS_TEMPLATE = "class C#ID extends C#PREV {\n" +
70                                                       "    C#ID(int val) { super(val); }\n" +
71                                                       "}\n";
72
73
74
75    protected static String LINKAGE_ERROR = "-1";
76
77    private List<File> compileDirs = new ArrayList<>();
78
79    /**
80     * Compile all the classes in a class spec, and put them on the classpath.
81     *
82     * The spec is the specification for a nest of classes, using the following notation
83     * A, B represent abstract classes
84     * C represents a concrete class
85     * I, J, K represent interfaces
86     * Lowercase 'c' following a class means that the method m() is concrete
87     * Lowercase 'a' following a class or interface means that the method m() is abstract
88     * Lowercase 'd' following an interface means that the method m() is default
89     * A number 0, 1, or 2 following the lowercase letter indicates the return type of that method
90     * 0 = Object, 1 = Number, 2 = Integer (these form an inheritance chain so bridges are generated)
91     * A classes supertypes follow its method spec, in parentheses
92     * Examples:
93     *   C(Ia0, Jd0) -- C extends I and J, I has abstract m()Object, J has default m()Object
94     *   Cc1(Ia0) -- C has concrete m()Number, extends I with abstract m()Object
95     * If a type must appear multiple times, its full spec must be in the first occurrence
96     * Example:
97     *   C(I(Kd0), J(K))
98     */
99   protected void compileSpec(String spec) throws IOException {
100        compileSpec(spec, false);
101    }
102
103    /**
104     * Compile all the classes in a class spec, and assert that there were compilation errors.
105     */
106    protected void compileSpec(String spec, String... errorKeys) throws IOException {
107        compileSpec(spec, false, errorKeys);
108    }
109
110    protected void compileSpec(String spec, boolean debug, String... errorKeys) throws IOException {
111        ClassModel cm = new Parser(spec).parseClassModel();
112        for (int i = 0; i <= cm.maxIndex() ; i++) {
113            if (debug) System.out.println(indexClass(i));
114            addSourceFile(String.format("C%d.java", i), new StringTemplate(indexClass(i)));
115        }
116        for (Map.Entry<String, ClassModel> e : classes(cm).entrySet()) {
117            if (debug) System.out.println(e.getValue().toSource());
118            addSourceFile(e.getKey() + ".java", new StringTemplate(e.getValue().toSource()));
119        }
120        compileDirs.add(compile(true));
121        resetSourceFiles();
122        if (errorKeys.length == 0)
123            assertCompileSucceeded();
124        else
125            assertCompileErrors(errorKeys);
126    }
127
128    /**
129     * Recompile only a subset of classes in the class spec, as named by names,
130     * and put them on the classpath such that they shadow earlier versions of that class.
131     */
132    protected void recompileSpec(String spec, String... names) throws IOException {
133        List<String> nameList = Arrays.asList(names);
134        ClassModel cm = new Parser(spec).parseClassModel();
135        for (int i = 0; i <= cm.maxIndex() ; i++) {
136            addSourceFile(String.format("C%d.java", i), new StringTemplate(indexClass(i)));
137        }
138        for (Map.Entry<String, ClassModel> e : classes(cm).entrySet())
139            if (nameList.contains(e.getKey()))
140                addSourceFile(e.getKey() + ".java", new StringTemplate(e.getValue().toSource()));
141        compileDirs.add(compile(Arrays.asList(classPaths()), true));
142        resetSourceFiles();
143        assertCompileSucceeded();
144    }
145
146    protected void assertLinkage(String name, String... expected) throws ReflectiveOperationException {
147        for (int i=0; i<expected.length; i++) {
148            String e = expected[i];
149            if (e.equals(LINKAGE_ERROR)) {
150                try {
151                    int actual = invoke(name, i);
152                    fail("Expected linkage error, got" + fromNum(actual));
153                }
154                catch (LinkageError x) {
155                    // success
156                }
157            }
158            else {
159                if (e.length() == 1)
160                    e += "0";
161                int expectedInt = toNum(e);
162                int actual = invoke(name, i);
163                if (expectedInt != actual)
164                    fail(String.format("Expected %s but found %s for %s.m()%d", fromNum(expectedInt), fromNum(actual), name, i));
165            }
166        }
167    }
168
169    private Map<String, ClassModel> classes(ClassModel cm) {
170        HashMap<String, ClassModel> m = new HashMap<>();
171        classesHelper(cm, m);
172        return m;
173    }
174
175    private String indexClass(int index) {
176        if (index == 0) {
177            return BASE_INDEX_CLASS;
178        } else {
179            return INDEX_CLASS_TEMPLATE
180                    .replace("#ID", String.valueOf(index))
181                    .replace("#PREV", String.valueOf(index - 1));
182        }
183    }
184
185    private static String overrideName(int index) {
186        return "C" + index;
187    }
188
189    private void classesHelper(ClassModel cm, Map<String, ClassModel> m) {
190        if (!m.containsKey(cm.name))
191            m.put(cm.name, cm);
192        for (ClassModel s : cm.supertypes)
193            classesHelper(s, m);
194    }
195
196    private static String fromNum(int num) {
197        return String.format("%c%d", TYPE_LETTERS.charAt(num / 10), num % 10);
198    }
199
200    private static int toNum(String name, int index) {
201        return 10*(TYPE_LETTERS.indexOf(name.charAt(0))) + index;
202    }
203
204    private static int toNum(String string) {
205        return 10*(TYPE_LETTERS.indexOf(string.charAt(0))) + Integer.parseInt(string.substring(1, 2));
206    }
207
208    private int invoke(String name, int index) throws ReflectiveOperationException {
209        File[] files = classPaths();
210        Class clazz = loadClass(name, files);
211        Method[] ms = clazz.getMethods();
212        for (Method m : ms) {
213            if (m.getName().equals("m") && m.getReturnType().getName().equals(overrideName(index))) {
214                m.setAccessible(true);
215                Object instance = clazz.newInstance();
216                Object c0 = m.invoke(instance);
217                Method getVal = c0.getClass().getMethod("getVal");
218                getVal.setAccessible(true);
219                return (int)getVal.invoke(c0);
220            }
221        }
222        throw new NoSuchMethodError("cannot find method m()" + index + " in class " + name);
223    }
224
225    private File[] classPaths() {
226        File[] files = new File[compileDirs.size()];
227        for (int i=0; i<files.length; i++)
228            files[files.length - i - 1] = compileDirs.get(i);
229        return files;
230    }
231
232    @BeforeMethod
233    @Override
234    public void reset() {
235        compileDirs.clear();
236        super.reset();
237    }
238
239    private static class ClassModel {
240
241        enum MethodType {
242            ABSTRACT('a'), CONCRETE('c'), DEFAULT('d');
243
244            public final char designator;
245
246            MethodType(char designator) {
247                this.designator = designator;
248            }
249
250            public static MethodType find(char c) {
251                for (MethodType m : values())
252                    if (m.designator == c)
253                        return m;
254                throw new IllegalArgumentException();
255            }
256        }
257
258        private final String name;
259        private final boolean isInterface;
260        private final List<ClassModel> supertypes;
261        private final MethodType methodType;
262        private final int methodIndex;
263
264        private ClassModel(String name,
265                           boolean anInterface,
266                           List<ClassModel> supertypes,
267                           MethodType methodType,
268                           int methodIndex) {
269            this.name = name;
270            isInterface = anInterface;
271            this.supertypes = supertypes;
272            this.methodType = methodType;
273            this.methodIndex = methodIndex;
274        }
275
276        @Override
277        public String toString() {
278            StringBuilder sb = new StringBuilder();
279            sb.append(name);
280            if (methodType != null) {
281                sb.append(methodType.designator);
282                sb.append(methodIndex);
283            }
284            if (!supertypes.isEmpty()) {
285                sb.append("(");
286                for (int i=0; i<supertypes.size(); i++) {
287                    if (i > 0)
288                        sb.append(",");
289                    sb.append(supertypes.get(i).toString());
290                }
291                sb.append(")");
292            }
293            return sb.toString();
294        }
295
296        int maxIndex() {
297            int maxSoFar = methodIndex;
298            for (ClassModel cm : supertypes) {
299                maxSoFar = Math.max(cm.maxIndex(), maxSoFar);
300            }
301            return maxSoFar;
302        }
303
304        public String toSource() {
305            String extendsClause = "";
306            String implementsClause = "";
307            String methodBody = "";
308            boolean isAbstract = "AB".contains(name);
309
310            for (ClassModel s : supertypes) {
311                if (!s.isInterface) {
312                    extendsClause = String.format("extends %s", s.name);
313                    break;
314                }
315            }
316
317            StringJoiner sj = new StringJoiner(", ");
318            for (ClassModel s : supertypes)
319                if (s.isInterface)
320                    sj.add(s.name);
321            if (sj.length() > 0) {
322                if (isInterface)
323                    implementsClause = "extends " + sj.toString();
324                else
325                implementsClause = "implements " + sj.toString();
326            }
327            if (methodType != null) {
328                switch (methodType) {
329                    case ABSTRACT:
330                        methodBody = String.format("public abstract %s m();", overrideName(methodIndex));
331                        break;
332                    case CONCRETE:
333                        methodBody = String.format("public %s m() { return new %s(%d); };",
334                                overrideName(methodIndex), overrideName(methodIndex), toNum(name, methodIndex));
335                        break;
336                    case DEFAULT:
337                        methodBody = String.format("public default %s m() { return new %s(%d); };",
338                                overrideName(methodIndex), overrideName(methodIndex), toNum(name, methodIndex));
339                        break;
340
341                }
342            }
343
344            return String.format("public %s %s %s %s %s {  %s }", isAbstract ? "abstract" : "",
345                                 isInterface ? "interface" : "class",
346                                 name, extendsClause, implementsClause, methodBody);
347        }
348    }
349
350    private static class Parser {
351        private final String input;
352        private final char[] chars;
353        private int index;
354
355        private Parser(String s) {
356            input = s;
357            chars = s.toCharArray();
358        }
359
360        private char peek() {
361            return index < chars.length ? chars[index] : 0;
362        }
363
364        private boolean peek(String validChars) {
365            return validChars.indexOf(peek()) >= 0;
366        }
367
368        private char advanceIf(String validChars) {
369            if (peek(validChars))
370                return chars[index++];
371            else
372                return 0;
373        }
374
375        private char advanceIfDigit() {
376            return advanceIf("0123456789");
377        }
378
379        private int index() {
380            StringBuilder buf = new StringBuilder();
381            char c = advanceIfDigit();
382            while (c != 0) {
383                buf.append(c);
384                c = advanceIfDigit();
385            }
386            return Integer.valueOf(buf.toString());
387        }
388
389        private char advance() {
390            return chars[index++];
391        }
392
393        private char expect(String validChars) {
394            char c = advanceIf(validChars);
395            if (c == 0)
396                throw new IllegalArgumentException(String.format("Expecting %s at position %d of %s", validChars, index, input));
397            return c;
398        }
399
400        public ClassModel parseClassModel() {
401            List<ClassModel> supers = new ArrayList<>();
402            char name = expect(TYPE_LETTERS);
403            boolean isInterface = "IJK".indexOf(name) >= 0;
404            ClassModel.MethodType methodType = peek(isInterface ? "ad" : "ac") ? ClassModel.MethodType.find(advance()) : null;
405            int methodIndex = 0;
406            if (methodType != null) {
407                methodIndex = index();
408            }
409            if (peek() == '(') {
410                advance();
411                supers.add(parseClassModel());
412                while (peek() == ',') {
413                    advance();
414                    supers.add(parseClassModel());
415                }
416                expect(")");
417            }
418            return new ClassModel(new String(new char[]{ name }), isInterface, supers, methodType, methodIndex);
419        }
420    }
421}
422