1/*
2 * Copyright (c) 2014, 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 com.sun.tools.classfile.*;
25
26import java.io.IOException;
27import java.lang.annotation.Repeatable;
28import java.lang.annotation.Retention;
29import java.lang.annotation.RetentionPolicy;
30import java.util.Collection;
31import java.util.HashMap;
32import java.util.List;
33import java.util.Map;
34import java.util.stream.Stream;
35
36import static java.lang.String.format;
37import static java.util.stream.Collectors.*;
38
39/**
40 * Base class for LocalVariableTable and LocalVariableTypeTable attributes tests.
41 * To add tests cases you should extend this class.
42 * Then implement {@link #getVariableTables} to get LocalVariableTable or LocalVariableTypeTable attribute.
43 * Then add method with local variables.
44 * Finally, annotate method with information about expected variables and their types
45 * by several {@link LocalVariableTestBase.ExpectedLocals} annotations.
46 * To run test invoke {@link #test()} method.
47 * If there are variables with the same name, set different scopes for them.
48 *
49 * @see #test()
50 */
51public abstract class LocalVariableTestBase extends TestBase {
52    public static final int DEFAULT_SCOPE = 0;
53    private final ClassFile classFile;
54    private final Class<?> clazz;
55
56    /**
57     * @param clazz class to test. Must contains annotated methods with expected results.
58     */
59    public LocalVariableTestBase(Class<?> clazz) {
60        this.clazz = clazz;
61        try {
62            this.classFile = ClassFile.read(getClassFile(clazz));
63        } catch (IOException | ConstantPoolException e) {
64            throw new IllegalArgumentException("Can't read classfile for specified class", e);
65        }
66    }
67
68    protected abstract List<VariableTable> getVariableTables(Code_attribute codeAttribute);
69
70    /**
71     * Finds expected variables with their type in VariableTable.
72     * Also does consistency checks, like variables from the same scope must point to different indexes.
73     */
74    public void test() throws IOException {
75        List<java.lang.reflect.Method> testMethods = Stream.of(clazz.getDeclaredMethods())
76                .filter(m -> m.getAnnotationsByType(ExpectedLocals.class).length > 0)
77                .collect(toList());
78        int failed = 0;
79        for (java.lang.reflect.Method method : testMethods) {
80            try {
81                Map<String, String> expectedLocals2Types = new HashMap<>();
82                Map<String, Integer> sig2scope = new HashMap<>();
83                for (ExpectedLocals anno : method.getDeclaredAnnotationsByType(ExpectedLocals.class)) {
84                    expectedLocals2Types.put(anno.name(), anno.type());
85                    sig2scope.put(anno.name() + "&" + anno.type(), anno.scope());
86                }
87
88                test(method.getName(), expectedLocals2Types, sig2scope);
89            } catch (AssertionFailedException ex) {
90                System.err.printf("Test %s failed.%n", method.getName());
91                ex.printStackTrace();
92                failed++;
93            }
94        }
95        if (failed > 0)
96            throw new RuntimeException(format("Failed %d out of %d. See logs.", failed, testMethods.size()));
97    }
98
99    public void test(String methodName, Map<String, String> expectedLocals2Types, Map<String, Integer> sig2scope)
100            throws IOException {
101
102        for (Method m : classFile.methods) {
103            String mName = getString(m.name_index);
104            if (methodName.equals(mName)) {
105                System.out.println("Testing local variable table in method " + mName);
106                Code_attribute code_attribute = (Code_attribute) m.attributes.get(Attribute.Code);
107
108                List<? extends VariableTable> variableTables = getVariableTables(code_attribute);
109                generalLocalVariableTableCheck(variableTables);
110
111                List<VariableTable.Entry> entries = variableTables.stream()
112                        .flatMap(table -> table.entries().stream())
113                        .collect(toList());
114
115                generalEntriesCheck(entries, code_attribute);
116                assertIndexesAreUnique(entries, sig2scope);
117                checkNamesAndTypes(entries, expectedLocals2Types);
118                checkDoubleAndLongIndexes(entries, sig2scope, code_attribute.max_locals);
119            }
120        }
121    }
122
123    private void generalLocalVariableTableCheck(List<? extends VariableTable> variableTables) {
124        for (VariableTable localTable : variableTables) {
125            //only one per variable.
126            assertEquals(localTable.localVariableTableLength(),
127                    localTable.entries().size(), "Incorrect local variable table length");
128            //attribute length is offset(line_number_table_length) + element_size*element_count
129            assertEquals(localTable.attributeLength(),
130                    2 + (5 * 2) * localTable.localVariableTableLength(), "Incorrect attribute length");
131        }
132    }
133
134    private void generalEntriesCheck(List<VariableTable.Entry> entries, Code_attribute code_attribute) {
135        for (VariableTable.Entry e : entries) {
136            assertTrue(e.index() >= 0 && e.index() < code_attribute.max_locals,
137                    "Index " + e.index() + " out of variable array. Size of array is " + code_attribute.max_locals);
138            assertTrue(e.startPC() >= 0, "StartPC is less then 0. StartPC = " + e.startPC());
139            assertTrue(e.length() >= 0, "Length is less then 0. Length = " + e.length());
140            assertTrue(e.startPC() + e.length() <= code_attribute.code_length,
141                    format("StartPC+Length > code length.%n" +
142                            "%s%n" +
143                            "code_length = %s"
144                            , e, code_attribute.code_length));
145        }
146    }
147
148    private void checkNamesAndTypes(List<VariableTable.Entry> entries,
149                                    Map<String, String> expectedLocals2Types) {
150        Map<String, List<String>> actualNames2Types = entries.stream()
151                .collect(
152                        groupingBy(VariableTable.Entry::name,
153                                mapping(VariableTable.Entry::type, toList())));
154        for (Map.Entry<String, String> name2type : expectedLocals2Types.entrySet()) {
155            String name = name2type.getKey();
156            String type = name2type.getValue();
157
158            assertTrue(actualNames2Types.containsKey(name),
159                    format("There is no record for local variable %s%nEntries: %s", name, entries));
160
161            assertTrue(actualNames2Types.get(name).contains(type),
162                    format("Types are different for local variable %s%nExpected type: %s%nActual type: %s",
163                            name, type, actualNames2Types.get(name)));
164        }
165    }
166
167
168    private void assertIndexesAreUnique(Collection<VariableTable.Entry> entries, Map<String, Integer> scopes) {
169        //check every scope separately
170        Map<Object, List<VariableTable.Entry>> entriesByScope = groupByScope(entries, scopes);
171        for (Map.Entry<Object, List<VariableTable.Entry>> mapEntry : entriesByScope.entrySet()) {
172            mapEntry.getValue().stream()
173                    .collect(groupingBy(VariableTable.Entry::index))
174                    .entrySet()
175                    .forEach(e ->
176                            assertTrue(e.getValue().size() == 1,
177                                    "Multiple variables point to the same index in common scope. " + e.getValue()));
178        }
179
180    }
181
182    private void checkDoubleAndLongIndexes(Collection<VariableTable.Entry> entries,
183                                           Map<String, Integer> scopes, int maxLocals) {
184        //check every scope separately
185        Map<Object, List<VariableTable.Entry>> entriesByScope = groupByScope(entries, scopes);
186        for (List<VariableTable.Entry> entryList : entriesByScope.values()) {
187            Map<Integer, VariableTable.Entry> index2Entry = entryList.stream()
188                    .collect(toMap(VariableTable.Entry::index, e -> e));
189
190            entryList.stream()
191                    .filter(e -> "J".equals(e.type()) || "D".equals(e.type()))
192                    .forEach(e -> {
193                        assertTrue(e.index() + 1 < maxLocals,
194                                format("Index %s is out of variable array. Long and double occupy 2 cells." +
195                                        " Size of array is %d", e.index() + 1, maxLocals));
196                        assertTrue(!index2Entry.containsKey(e.index() + 1),
197                                format("An entry points to the second cell of long/double entry.%n%s%n%s", e,
198                                        index2Entry.get(e.index() + 1)));
199                    });
200        }
201    }
202
203    private Map<Object, List<VariableTable.Entry>> groupByScope(
204            Collection<VariableTable.Entry> entries, Map<String, Integer> scopes) {
205        return entries.stream().collect(groupingBy(e -> scopes.getOrDefault(e.name() + "&" + e.type(), DEFAULT_SCOPE)));
206    }
207
208    protected String getString(int i) {
209        try {
210            return classFile.constant_pool.getUTF8Info(i).value;
211        } catch (ConstantPool.InvalidIndex | ConstantPool.UnexpectedEntry ex) {
212            ex.printStackTrace();
213            throw new AssertionFailedException("Issue while reading constant pool");
214        }
215    }
216
217    /**
218     * LocalVariableTable and LocalVariableTypeTable are similar.
219     * VariableTable interface is introduced to test this attributes in the same way without code duplication.
220     */
221    interface VariableTable {
222
223        int localVariableTableLength();
224
225        List<VariableTable.Entry> entries();
226
227        int attributeLength();
228
229        interface Entry {
230
231            int index();
232
233            int startPC();
234
235            int length();
236
237            String name();
238
239            String type();
240
241            default String dump() {
242                return format("Entry{" +
243                        "%n    name    = %s" +
244                        "%n    type    = %s" +
245                        "%n    index   = %d" +
246                        "%n    startPC = %d" +
247                        "%n    length  = %d" +
248                        "%n}", name(), type(), index(), startPC(), length());
249            }
250        }
251    }
252
253    /**
254     * Used to store expected results in sources
255     */
256    @Retention(RetentionPolicy.RUNTIME)
257    @Repeatable(Container.class)
258    @interface ExpectedLocals {
259        /**
260         * @return name of a local variable
261         */
262        String name();
263
264        /**
265         * @return type of local variable in the internal format.
266         */
267        String type();
268
269        //variables from different scopes can share the local variable table index and/or name.
270        int scope() default DEFAULT_SCOPE;
271    }
272
273    @Retention(RetentionPolicy.RUNTIME)
274    @interface Container {
275        ExpectedLocals[] value();
276    }
277}
278