001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Florent Guillaume
016 */
017package org.nuxeo.runtime.test.runner;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import org.junit.runners.model.FrameworkMethod;
028import org.objectweb.asm.ClassReader;
029import org.objectweb.asm.ClassVisitor;
030import org.objectweb.asm.Label;
031import org.objectweb.asm.MethodVisitor;
032import org.objectweb.asm.Opcodes;
033
034/**
035 * Utility class that sorts a list of JUnit methods according to their source line number.
036 *
037 * @since 7.1
038 */
039public class MethodSorter {
040
041    private MethodSorter() {
042        // utility class
043    }
044
045    /**
046     * Sorts a list of JUnit methods according to their source line number.
047     *
048     * @param methods the JUnit methods
049     */
050    public static void sortMethodsUsingSourceOrder(List<FrameworkMethod> methods) {
051        if (methods.isEmpty()) {
052            return;
053        }
054        Map<String, Integer> nameToLine = new HashMap<>();
055        Class<?> cls = methods.get(0).getMethod().getDeclaringClass();
056        String name = "/" + cls.getName().replace(".", "/") + ".class";
057        try (InputStream is = cls.getResourceAsStream(name)) {
058            ClassReader cr = new ClassReader(is);
059            ClassVisitor cv = new CV(nameToLine);
060            cr.accept(cv, ClassReader.SKIP_FRAMES);
061        } catch (IOException e) {
062            throw new RuntimeException("Failed to parse " + name, e);
063        }
064        Collections.sort(methods, new LineComparator(nameToLine));
065    }
066
067    /**
068     * Class Visitor that constructs a map of method name to source line number.
069     */
070    public static class CV extends ClassVisitor {
071
072        public Map<String, Integer> nameToLine;
073
074        public CV(Map<String, Integer> nameToLine) {
075            super(Opcodes.ASM5);
076            this.nameToLine = nameToLine;
077        }
078
079        @Override
080        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
081            return new MV(nameToLine, name);
082        }
083    }
084
085    /**
086     * Method Visitor that records method source line number.
087     */
088    public static class MV extends MethodVisitor {
089
090        public Map<String, Integer> nameToLine;
091
092        public final String name;
093
094        public MV(Map<String, Integer> nameToLine, String name) {
095            super(Opcodes.ASM5);
096            this.nameToLine = nameToLine;
097            this.name = name;
098        }
099
100        @Override
101        public void visitLineNumber(int line, Label start) {
102            if (nameToLine.get(name) == null) {
103                nameToLine.put(name, Integer.valueOf(line));
104            }
105        }
106    }
107
108    /**
109     * Comparator of methods according to their line number.
110     */
111    public static class LineComparator implements Comparator<FrameworkMethod> {
112
113        public Map<String, Integer> nameToLine;
114
115        public LineComparator(Map<String, Integer> nameToLine) {
116            this.nameToLine = nameToLine;
117        }
118
119        @Override
120        public int compare(FrameworkMethod fm1, FrameworkMethod fm2) {
121            String name1 = fm1.getMethod().getName();
122            String name2 = fm2.getMethod().getName();
123            Integer pos1 = nameToLine.get(name1);
124            Integer pos2 = nameToLine.get(name2);
125            if (pos1 == null || pos2 == null) {
126                return name1.compareTo(name2);
127            } else {
128                return pos1.compareTo(pos2);
129            }
130        }
131    }
132
133}