1 /*******************************************************************************
2  * Copyright (c) 2014 Moritz Eysholdt and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     Moritz Eysholdt <moritz.eysholdt@itemis.de> - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.jdt.internal.junit4.runner;
15 
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 
21 import org.junit.runner.Description;
22 import org.junit.runners.Parameterized;
23 
24 /**
25  * This class matches JUnit's {@link Description} against a string.
26  *
27  * See {@link #create(Class, String)} for details.
28  */
29 public abstract class DescriptionMatcher {
30 
31 	private static class CompositeMatcher extends DescriptionMatcher {
32 		private final List<DescriptionMatcher> fMatchers;
33 
CompositeMatcher(List<DescriptionMatcher> matchers)34 		public CompositeMatcher(List<DescriptionMatcher> matchers) {
35 			fMatchers= matchers;
36 		}
37 
38 		@Override
matches(Description description)39 		public boolean matches(Description description) {
40 			for (DescriptionMatcher matcher : fMatchers)
41 				if (matcher.matches(description))
42 					return true;
43 			return false;
44 		}
45 
46 		@Override
toString()47 		public String toString() {
48 			return fMatchers.toString();
49 		}
50 	}
51 
52 	private static class ExactMatcher extends DescriptionMatcher {
53 		private final String fDisplayName;
54 
ExactMatcher(String className)55 		public ExactMatcher(String className) {
56 			fDisplayName= className;
57 		}
58 
ExactMatcher(String className, String testName)59 		public ExactMatcher(String className, String testName) {
60 			// see org.junit.runner.Description.formatDisplayName(String, String)
61 			fDisplayName= String.format("%s(%s)", testName, className); //$NON-NLS-1$
62 		}
63 
64 		@Override
matches(Description description)65 		public boolean matches(Description description) {
66 			return fDisplayName.equals(description.getDisplayName());
67 		}
68 
69 		@Override
toString()70 		public String toString() {
71 			return String.format("{%s:fDisplayName=%s]", getClass().getSimpleName(), fDisplayName); //$NON-NLS-1$
72 		}
73 	}
74 
75 	/**
76 	 * This class extracts the leading chars from {@link Description#getMethodName()} which are a
77 	 * valid Java identifier. If this identifier equals this class' identifier, the Description is
78 	 * matched.
79 	 *
80 	 * Please be aware that {@link Description#getMethodName()} can be any value a JUnit runner has
81 	 * computed. It is not necessarily a valid method name. For example, {@link Parameterized} uses
82 	 * the format 'methodname[i]', with 'i' being the row index in the table of test data.
83 	 */
84 	private static class LeadingIdentifierMatcher extends DescriptionMatcher {
85 
86 		private final String fClassName;
87 
88 		private final String fLeadingIdentifier;
89 
LeadingIdentifierMatcher(String className, String leadingIdentifier)90 		public LeadingIdentifierMatcher(String className, String leadingIdentifier) {
91 			super();
92 			fClassName= className;
93 			fLeadingIdentifier= leadingIdentifier;
94 		}
95 
96 		@Override
matches(Description description)97 		public boolean matches(Description description) {
98 			String className= description.getClassName();
99 			if (fClassName.equals(className)) {
100 				String methodName= description.getMethodName();
101 				if (methodName != null) {
102 					return fLeadingIdentifier.equals(extractLeadingIdentifier(methodName));
103 				}
104 			}
105 			return false;
106 		}
107 
108 		@Override
toString()109 		public String toString() {
110 			return String.format("{%s:fClassName=%s,fLeadingIdentifier=%s]", getClass().getSimpleName(), fClassName, fLeadingIdentifier); //$NON-NLS-1$
111 		}
112 
113 	}
114 
115 	/**
116 	 * Creates a matcher object that can decide for {@link Description}s whether they match the
117 	 * supplied 'matchString' or not.
118 	 *
119 	 * Several strategies for matching are applied:
120 	 * <ul>
121 	 * <li>if 'matchString' equals {@link Description#getDisplayName()}, it's always a match</li>
122 	 * <li>if 'matchString' has the format foo(bar), which is JUnit's format, it tries to match
123 	 * methods base on leading identifiers. See {@link LeadingIdentifierMatcher}.</li>
124 	 * <li>if 'matchString' does not have the format foo(bar), it also tries to match Descriptions
125 	 * that equal clazz(matchString), with 'clazz' being this method's parameter. Furthermore, if
126 	 * 'matchString' is a Java identifier, it matches Descriptions via leading identifiers. See
127 	 * {@link LeadingIdentifierMatcher}</li>
128 	 * </ul>
129 	 *
130 	 * @param clazz A class that is used when 'matchString' does not have the format
131 	 *            methodName(className).
132 	 *
133 	 * @param matchString A string to match JUnit's {@link Description}s against.
134 	 *
135 	 * @return A matcher object.
136 	 */
create(Class<?> clazz, String matchString)137 	public static DescriptionMatcher create(Class<?> clazz, String matchString) {
138 		String className= clazz.getName();
139 		List<DescriptionMatcher> matchers= new ArrayList<DescriptionMatcher>();
140 		matchers.add(new ExactMatcher(matchString));
141 		Matcher parsed= METHOD_AND_CLASS_NAME_PATTERN.matcher(matchString);
142 		if (parsed.matches()) {
143 			String testName= parsed.group(1);
144 			if (testName.equals(extractLeadingIdentifier(testName)))
145 				matchers.add(new LeadingIdentifierMatcher(className, testName));
146 		} else {
147 			if (!className.equals(matchString)) {
148 				matchers.add(new ExactMatcher(className, matchString));
149 				if (matchString.equals(extractLeadingIdentifier(matchString)))
150 					matchers.add(new LeadingIdentifierMatcher(className, matchString));
151 			}
152 		}
153 		return new CompositeMatcher(matchers);
154 	}
155 
extractLeadingIdentifier(String string)156 	private static String extractLeadingIdentifier(String string) {
157 		if (string.length() == 0)
158 			return null;
159 		if (!Character.isJavaIdentifierStart(string.charAt(0)))
160 			return null;
161 		for (int i= 1; i < string.length(); i++) {
162 			if (!Character.isJavaIdentifierPart(string.charAt(i))) {
163 				return string.substring(0, i);
164 			}
165 		}
166 		return string;
167 	}
168 
169 	// see org.junit.runner.Description.METHOD_AND_CLASS_NAME_PATTERN
170 	private static final Pattern METHOD_AND_CLASS_NAME_PATTERN= Pattern.compile("(.*)\\((.*)\\)"); //$NON-NLS-1$
171 
matches(Description description)172 	public abstract boolean matches(Description description);
173 }
174