| /* |
| * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| import com.sun.tools.classfile.*; |
| |
| import java.io.IOException; |
| import java.lang.annotation.Repeatable; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.stream.Stream; |
| |
| import static java.lang.String.format; |
| import static java.util.stream.Collectors.*; |
| |
| /** |
| * Base class for LocalVariableTable and LocalVariableTypeTable attributes tests. |
| * To add tests cases you should extend this class. |
| * Then implement {@link #getVariableTables} to get LocalVariableTable or LocalVariableTypeTable attribute. |
| * Then add method with local variables. |
| * Finally, annotate method with information about expected variables and their types |
| * by several {@link LocalVariableTestBase.ExpectedLocals} annotations. |
| * To run test invoke {@link #test()} method. |
| * If there are variables with the same name, set different scopes for them. |
| * |
| * @see #test() |
| */ |
| public abstract class LocalVariableTestBase extends TestBase { |
| public static final int DEFAULT_SCOPE = 0; |
| private final ClassFile classFile; |
| private final Class<?> clazz; |
| |
| /** |
| * @param clazz class to test. Must contains annotated methods with expected results. |
| */ |
| public LocalVariableTestBase(Class<?> clazz) { |
| this.clazz = clazz; |
| try { |
| this.classFile = ClassFile.read(getClassFile(clazz)); |
| } catch (IOException | ConstantPoolException e) { |
| throw new IllegalArgumentException("Can't read classfile for specified class", e); |
| } |
| } |
| |
| protected abstract List<VariableTable> getVariableTables(Code_attribute codeAttribute); |
| |
| /** |
| * Finds expected variables with their type in VariableTable. |
| * Also does consistency checks, like variables from the same scope must point to different indexes. |
| */ |
| public void test() throws IOException { |
| List<java.lang.reflect.Method> testMethods = Stream.of(clazz.getDeclaredMethods()) |
| .filter(m -> m.getAnnotationsByType(ExpectedLocals.class).length > 0) |
| .collect(toList()); |
| int failed = 0; |
| for (java.lang.reflect.Method method : testMethods) { |
| try { |
| Map<String, String> expectedLocals2Types = new HashMap<>(); |
| Map<String, Integer> sig2scope = new HashMap<>(); |
| for (ExpectedLocals anno : method.getDeclaredAnnotationsByType(ExpectedLocals.class)) { |
| expectedLocals2Types.put(anno.name(), anno.type()); |
| sig2scope.put(anno.name() + "&" + anno.type(), anno.scope()); |
| } |
| |
| test(method.getName(), expectedLocals2Types, sig2scope); |
| } catch (AssertionFailedException ex) { |
| System.err.printf("Test %s failed.%n", method.getName()); |
| ex.printStackTrace(); |
| failed++; |
| } |
| } |
| if (failed > 0) |
| throw new RuntimeException(format("Failed %d out of %d. See logs.", failed, testMethods.size())); |
| } |
| |
| public void test(String methodName, Map<String, String> expectedLocals2Types, Map<String, Integer> sig2scope) |
| throws IOException { |
| |
| for (Method m : classFile.methods) { |
| String mName = getString(m.name_index); |
| if (methodName.equals(mName)) { |
| System.out.println("Testing local variable table in method " + mName); |
| Code_attribute code_attribute = (Code_attribute) m.attributes.get(Attribute.Code); |
| |
| List<? extends VariableTable> variableTables = getVariableTables(code_attribute); |
| generalLocalVariableTableCheck(variableTables); |
| |
| List<VariableTable.Entry> entries = variableTables.stream() |
| .flatMap(table -> table.entries().stream()) |
| .collect(toList()); |
| |
| generalEntriesCheck(entries, code_attribute); |
| assertIndexesAreUnique(entries, sig2scope); |
| checkNamesAndTypes(entries, expectedLocals2Types); |
| checkDoubleAndLongIndexes(entries, sig2scope, code_attribute.max_locals); |
| } |
| } |
| } |
| |
| private void generalLocalVariableTableCheck(List<? extends VariableTable> variableTables) { |
| for (VariableTable localTable : variableTables) { |
| //only one per variable. |
| assertEquals(localTable.localVariableTableLength(), |
| localTable.entries().size(), "Incorrect local variable table length"); |
| //attribute length is offset(line_number_table_length) + element_size*element_count |
| assertEquals(localTable.attributeLength(), |
| 2 + (5 * 2) * localTable.localVariableTableLength(), "Incorrect attribute length"); |
| } |
| } |
| |
| private void generalEntriesCheck(List<VariableTable.Entry> entries, Code_attribute code_attribute) { |
| for (VariableTable.Entry e : entries) { |
| assertTrue(e.index() >= 0 && e.index() < code_attribute.max_locals, |
| "Index " + e.index() + " out of variable array. Size of array is " + code_attribute.max_locals); |
| assertTrue(e.startPC() >= 0, "StartPC is less then 0. StartPC = " + e.startPC()); |
| assertTrue(e.length() >= 0, "Length is less then 0. Length = " + e.length()); |
| assertTrue(e.startPC() + e.length() <= code_attribute.code_length, |
| format("StartPC+Length > code length.%n" + |
| "%s%n" + |
| "code_length = %s" |
| , e, code_attribute.code_length)); |
| } |
| } |
| |
| private void checkNamesAndTypes(List<VariableTable.Entry> entries, |
| Map<String, String> expectedLocals2Types) { |
| Map<String, List<String>> actualNames2Types = entries.stream() |
| .collect( |
| groupingBy(VariableTable.Entry::name, |
| mapping(VariableTable.Entry::type, toList()))); |
| for (Map.Entry<String, String> name2type : expectedLocals2Types.entrySet()) { |
| String name = name2type.getKey(); |
| String type = name2type.getValue(); |
| |
| assertTrue(actualNames2Types.containsKey(name), |
| format("There is no record for local variable %s%nEntries: %s", name, entries)); |
| |
| assertTrue(actualNames2Types.get(name).contains(type), |
| format("Types are different for local variable %s%nExpected type: %s%nActual type: %s", |
| name, type, actualNames2Types.get(name))); |
| } |
| } |
| |
| |
| private void assertIndexesAreUnique(Collection<VariableTable.Entry> entries, Map<String, Integer> scopes) { |
| //check every scope separately |
| Map<Object, List<VariableTable.Entry>> entriesByScope = groupByScope(entries, scopes); |
| for (Map.Entry<Object, List<VariableTable.Entry>> mapEntry : entriesByScope.entrySet()) { |
| mapEntry.getValue().stream() |
| .collect(groupingBy(VariableTable.Entry::index)) |
| .entrySet() |
| .forEach(e -> |
| assertTrue(e.getValue().size() == 1, |
| "Multiple variables point to the same index in common scope. " + e.getValue())); |
| } |
| |
| } |
| |
| private void checkDoubleAndLongIndexes(Collection<VariableTable.Entry> entries, |
| Map<String, Integer> scopes, int maxLocals) { |
| //check every scope separately |
| Map<Object, List<VariableTable.Entry>> entriesByScope = groupByScope(entries, scopes); |
| for (List<VariableTable.Entry> entryList : entriesByScope.values()) { |
| Map<Integer, VariableTable.Entry> index2Entry = entryList.stream() |
| .collect(toMap(VariableTable.Entry::index, e -> e)); |
| |
| entryList.stream() |
| .filter(e -> "J".equals(e.type()) || "D".equals(e.type())) |
| .forEach(e -> { |
| assertTrue(e.index() + 1 < maxLocals, |
| format("Index %s is out of variable array. Long and double occupy 2 cells." + |
| " Size of array is %d", e.index() + 1, maxLocals)); |
| assertTrue(!index2Entry.containsKey(e.index() + 1), |
| format("An entry points to the second cell of long/double entry.%n%s%n%s", e, |
| index2Entry.get(e.index() + 1))); |
| }); |
| } |
| } |
| |
| private Map<Object, List<VariableTable.Entry>> groupByScope( |
| Collection<VariableTable.Entry> entries, Map<String, Integer> scopes) { |
| return entries.stream().collect(groupingBy(e -> scopes.getOrDefault(e.name() + "&" + e.type(), DEFAULT_SCOPE))); |
| } |
| |
| protected String getString(int i) { |
| try { |
| return classFile.constant_pool.getUTF8Info(i).value; |
| } catch (ConstantPool.InvalidIndex | ConstantPool.UnexpectedEntry ex) { |
| ex.printStackTrace(); |
| throw new AssertionFailedException("Issue while reading constant pool"); |
| } |
| } |
| |
| /** |
| * LocalVariableTable and LocalVariableTypeTable are similar. |
| * VariableTable interface is introduced to test this attributes in the same way without code duplication. |
| */ |
| interface VariableTable { |
| |
| int localVariableTableLength(); |
| |
| List<VariableTable.Entry> entries(); |
| |
| int attributeLength(); |
| |
| interface Entry { |
| |
| int index(); |
| |
| int startPC(); |
| |
| int length(); |
| |
| String name(); |
| |
| String type(); |
| |
| default String dump() { |
| return format("Entry{" + |
| "%n name = %s" + |
| "%n type = %s" + |
| "%n index = %d" + |
| "%n startPC = %d" + |
| "%n length = %d" + |
| "%n}", name(), type(), index(), startPC(), length()); |
| } |
| } |
| } |
| |
| /** |
| * Used to store expected results in sources |
| */ |
| @Retention(RetentionPolicy.RUNTIME) |
| @Repeatable(Container.class) |
| @interface ExpectedLocals { |
| /** |
| * @return name of a local variable |
| */ |
| String name(); |
| |
| /** |
| * @return type of local variable in the internal format. |
| */ |
| String type(); |
| |
| //variables from different scopes can share the local variable table index and/or name. |
| int scope() default DEFAULT_SCOPE; |
| } |
| |
| @Retention(RetentionPolicy.RUNTIME) |
| @interface Container { |
| ExpectedLocals[] value(); |
| } |
| } |