aboutsummaryrefslogtreecommitdiffstats
blob: 2c78d2b8dc0b173c14dcd73ff26176a6d52c560a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.test;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * Test suite with user-specified test order (fails if not all test methods are listed) or bytecode declaration order.
 * 
 * <p>
 * <b>Background:</b> {@link java.lang.Class#getDeclaredMethods()} does not specify the order of the methods. Up to Java SE 6, the
 * methods were usually sorted in declaration order, but in Java SE 7 and later, the order is random. This class guarantees reliable
 * test execution order even for questionable VM implementations.
 * </p>
 * 
 * @since 3.8
 */
public class OrderedTestSuite extends TestSuite {

    /**
     * Creates a new ordered test suite that runs tests in the specified execution order.
     * 
     * @param testClass
     *            the JUnit-3-style test class
     * @param testMethods
     *            the names of all test methods in the expected execution order
     */
    public OrderedTestSuite(final Class<? extends TestCase> testClass, String[] testMethods) {
        super(testClass.getName());

        Set<String> existingMethods = new HashSet<>();
        Method[] methods = testClass.getMethods(); // just public member methods
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            existingMethods.add(method.getName());
        }

        for (int i = 0; i < testMethods.length; i++) {
            final String testMethod = testMethods[i];
            if (existingMethods.remove(testMethod)) {
                addTest(createTest(testClass, testMethod));
            } else {
                addTest(error(testClass, testMethod, new IllegalArgumentException("Class '" + testClass.getName() + " misses test method '" + testMethod + "'."))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        }

        for (Iterator<String> iter = existingMethods.iterator(); iter.hasNext();) {
            String existingMethod = iter.next();
            if (existingMethod.startsWith("test")) { //$NON-NLS-1$
                addTest(error(testClass, existingMethod, new IllegalArgumentException("Test method '" + existingMethod + "' not listed in OrderedTestSuite of class '" + testClass.getName() + "'."))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        }

    }

    /**
     * Creates a new test suite that runs tests in bytecode declaration order.
     * 
     * @param testClass
     *            the JUnit-3-style test class
     * @since 3.9
     */
    public OrderedTestSuite(Class<? extends TestCase> testClass) {
        this(testClass, testClass.getName());
    }

    /**
     * Creates a new test suite that runs tests in bytecode declaration order.
     * 
     * @param testClass
     *            the JUnit-3-style test class
     * @param name
     *            the name of the suite
     * @since 3.9
     */
    public OrderedTestSuite(Class<? extends TestCase> testClass, String name) {
        super(name);

        TestSuite vmOrderSuite = new TestSuite(testClass);
        ArrayList<Test> tests = Collections.list(vmOrderSuite.tests());

        class SortingException extends RuntimeException {

            private static final long serialVersionUID = 1L;

            public SortingException(String message) {
                super(message);
            }
        }

        try {
            final List<String> orderedMethodNames = getBytecodeOrderedTestNames(testClass);
            Collections.sort(tests, new Comparator<Test>() {

                @Override
                public int compare(Test o1, Test o2) {
                    if (o1 instanceof TestCase && o2 instanceof TestCase) {
                        TestCase t1 = (TestCase) o1;
                        TestCase t2 = (TestCase) o2;
                        int i1 = orderedMethodNames.indexOf(t1.getName());
                        int i2 = orderedMethodNames.indexOf(t2.getName());
                        if (i1 != -1 && i2 != -1)
                            return i1 - i2;
                    }
                    throw new SortingException("suite failed to detect test order: " + o1 + ", " + o2); //$NON-NLS-1$ //$NON-NLS-2$
                }
            });
        } catch (SortingException e) {
            addTest(error(testClass, "suite failed to detect test order", e)); //$NON-NLS-1$
        } catch (IOException e) {
            addTest(error(testClass, "suite failed to detect test order", e)); //$NON-NLS-1$
        }

        for (Iterator<Test> iter = tests.iterator(); iter.hasNext();) {
            Test test = iter.next();
            addTest(test);
        }
    }

    /**
     * Returns the names of JUnit-3-style test cases declared in the given <code>testClass</code> and its superclasses, in bytecode
     * declaration order, listing test cases from a class before the non-overridden test cases from its superclass.
     * 
     * @param testClass
     *            the JUnit-3-style test class
     * @return a modifiable <code>List&lt;String&gt;</code> of test names in bytecode declaration order
     * @throws IOException
     *             if an I/O error occurs.
     * 
     * @since 3.10
     */
    public static List<String> getBytecodeOrderedTestNames(Class<? extends TestCase> testClass) throws IOException {
        ArrayList<String> orderedMethodNames = new ArrayList<>();
        Class<?> c = testClass;
        while (Test.class.isAssignableFrom(c)) {
            addDeclaredTestMethodNames(c, orderedMethodNames);
            c = c.getSuperclass();
        }
        return orderedMethodNames;
    }

    private static void addDeclaredTestMethodNames(Class<?> c, ArrayList<String> methodNames) throws IOException {
        /*
         * XXX: This method needs to be updated if new constant pool tags are specified. Current supported major class file version:
         * 52 (Java SE 8).
         * 
         * See JVMS 8, 4.4 The Constant Pool.
         */
        String className = c.getName();
        int lastDot = className.lastIndexOf("."); //$NON-NLS-1$
        if (lastDot != -1)
            className = className.substring(lastDot + 1);
        DataInputStream is = new DataInputStream(new BufferedInputStream(c.getResourceAsStream(className + ".class"))); //$NON-NLS-1$
        int magic = is.readInt();
        if (magic != 0xcafebabe)
            throw new IOException("bad magic bytes: 0x" + Integer.toHexString(magic)); //$NON-NLS-1$
        int minor = is.readUnsignedShort();
        int major = is.readUnsignedShort();
        int cpCount = is.readUnsignedShort();
        String[] constantPoolStrings = new String[cpCount];
        for (int i = 1; i < cpCount; i++) {

            byte tag = is.readByte();
            switch (tag) {
                case 7: // CONSTANT_Class
                    skip(is, 2);
                    break;
                case 9: // CONSTANT_Fieldref
                case 10: // CONSTANT_Methodref
                case 11: // CONSTANT_InterfaceMethodref
                    skip(is, 4);
                    break;
                case 8: // CONSTANT_String
                    skip(is, 2);
                    break;
                case 3: // CONSTANT_Integer
                case 4: // CONSTANT_Float
                    skip(is, 4);
                    break;
                case 5: // CONSTANT_Long
                case 6: // CONSTANT_Double
                    skip(is, 8);
                    i++; // weird spec wants this
                    break;
                case 12: // CONSTANT_NameAndType
                    skip(is, 4);
                    break;
                case 1: // CONSTANT_Utf8
                    constantPoolStrings[i] = is.readUTF();
                    break;
                case 15: // CONSTANT_MethodHandle
                    skip(is, 3);
                    break;
                case 16: // CONSTANT_MethodType
                    skip(is, 2);
                    break;
                case 18: // CONSTANT_InvokeDynamic
                    skip(is, 4);
                    break;
                default:
                    throw new IOException("unknown constant pool tag " + tag + " at index " + i + ". Class file version: " + major + "." + minor); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
            }
        }
        skip(is, 2 * 3); // access_flags, this_class, super_class
        int interfacesCount = is.readUnsignedShort();
        skip(is, 2 * interfacesCount);
        int fieldsCount = is.readUnsignedShort();
        for (int i = 0; i < fieldsCount; i++) {
            skip(is, 2 * 3); // access_flags, name_index, descriptor_index
            int attributesCount = is.readUnsignedShort();
            for (int j = 0; j < attributesCount; j++) {
                skip(is, 2); // attribute_name_index
                long attInfoCount = readUnsignedInt(is);
                skip(is, attInfoCount);
            }
        }

        int methodsCount = is.readUnsignedShort();
        for (int i = 0; i < methodsCount; i++) {
            skip(is, 2); // access_flags
            int nameIndex = is.readUnsignedShort();
            int descIndex = is.readUnsignedShort();
            if ("()V".equals(constantPoolStrings[descIndex])) { //$NON-NLS-1$
                String name = constantPoolStrings[nameIndex];
                if (name.startsWith("test") && !methodNames.contains(name)) //$NON-NLS-1$
                    methodNames.add(name);
            }
            int attributesCount = is.readUnsignedShort();
            for (int j = 0; j < attributesCount; j++) {
                skip(is, 2); // attribute_name_index
                long attInfoCount = readUnsignedInt(is);
                skip(is, attInfoCount);
            }
        }
    }

    private static void skip(DataInputStream is, long bytes) throws IOException {
        while (bytes > 0)
            bytes -= is.skip(bytes);
        if (bytes != 0)
            throw new IOException("error in skipping bytes: " + bytes); //$NON-NLS-1$
    }

    private static long readUnsignedInt(DataInputStream is) throws IOException {
        return is.readInt() & 0xFFFFffffL;
    }

    private static Test error(Class<? extends TestCase> testClass, String testMethod, Exception exception) {
        final Throwable e2 = exception.fillInStackTrace();
        return new TestCase(testMethod + "(" + testClass.getName() + ")") { //$NON-NLS-1$ //$NON-NLS-2$

            @Override
            protected void runTest() throws Throwable {
                throw e2;
            }
        };
    }
}