blob: ccf00e063b4d671fbd41b3d08f65137019c4e502 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.junit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import org.netbeans.api.java.source.WorkingCopy;
import static java.lang.Boolean.TRUE;
/**
* Utility class for generating a collection of unique names of test methods.
*
* @author Marian Petras
*/
final class TestMethodNameGenerator {
/** maximum number of parameter-type method name suffixes */
private static final int MAX_SUFFIX_TYPES = 2;
/**
* collection of reserved type identifiers that should be left unchanged.
* A method name may be reserved because it was assigned to some method
* (value {@code Boolean.TRUE}) or because it was inherited from some parent
* class (value {@code Boolean.FALSE}).
*/
private Collection<String> reservedNames;
private final WorkingCopy workingCopy;
private final List<ExecutableElement> srcMethods;
private final TypeElement tstClassElem;
private final List<ExecutableElement> existingMethods;
private final String[] testMethodNames;
private TestMethodNameGenerator(final List<ExecutableElement> srcMethods,
final TypeElement tstClassElem,
WorkingCopy workingCopy) {
this.srcMethods = srcMethods;
this.tstClassElem = tstClassElem;
this.workingCopy = workingCopy;
existingMethods = (tstClassElem != null)
? getExistingMethods(tstClassElem)
: Collections.<ExecutableElement>emptyList();
reservedNames = new HashSet<String>((existingMethods.size() * 3 + 1) / 2);
testMethodNames = new String[srcMethods.size()];
}
/**
* Generates a list of unique names of test methods for the given source
* methods. The names are generated such that they do not conflict
* with names of methods that are already present in the test class
*
* @param srcMethods source methods for which test methods are about
* to be created - names of these test methods will
* be generated
* @param tstClassElem test class - the existing test class in which
* new test classes are about to be generated,
* or {@code null} if the test class does not exist yet
* @param reservedMethodNames list of reserved test method names
* - these should not be avoided
* by the test method name generator;
* it may be {@code null} if there are no
* reserved method names
* @return list of names for test methods for the given source methods;
* the names in the list are unique, they do not conflict
* with names of existing methods in the given test class (if any)
* and they are stored in the order of the source methods
*/
static List<String> getTestMethodNames(
final List<ExecutableElement> srcMethods,
final TypeElement tstClassElem,
final Collection<String> reservedMethodNames,
final WorkingCopy workingCopy) {
TestMethodNameGenerator inst
= new TestMethodNameGenerator(srcMethods, tstClassElem, workingCopy);
if (reservedMethodNames != null) {
inst.reservedNames.addAll(reservedMethodNames);
}
return inst.getTestMethodNames();
}
/**
* Determines names for test methods that are about to be generated.
*
* @return list of test method names, in the order corresponding to the
* order of {@linkplain #srcMethods source methods}
*/
private List<String> getTestMethodNames() {
if (tstClassElem != null) {
collectExistingMethodNames(tstClassElem, reservedNames);
}
final int methodsCount = srcMethods.size();
final String[] result = new String[methodsCount];
final Map<String, Object> namesUsage
= new HashMap<String, Object>(methodsCount * 3 + 1 / 2);
final BitSet conflicting = new BitSet(methodsCount);
int conflictingCount = 0;
/*
* Identify methods with overloaded names:
*/
int index = -1;
assert namesUsage.isEmpty();
for (ExecutableElement srcMethod : srcMethods) {
index++;
String srcMethodName = srcMethod.getSimpleName().toString();
String testMethodName = buildTestMethodName(srcMethodName);
testMethodNames[index] = testMethodName;
conflictingCount += registerTestMethodName(testMethodName,
index,
namesUsage,
conflicting);
}
namesUsage.clear();
assert conflictingCount <= methodsCount;
assert conflictingCount == conflicting.cardinality();
int uniqueCount = methodsCount - conflictingCount;
if (uniqueCount > 0) {
/* fixate all unique method names... */
for (index = conflicting.nextClearBit(0); //for all unique...
(index >= 0) && (index < methodsCount);
index = conflicting.nextClearBit(index + 1)) {
String name = testMethodNames[index];
result[index] = name;
reservedNames.add(name); //fixate
}
}
/* ... try to resolve the conflicting ones... */
int[] paramsCount = null;
Collection<TypeMirror> paramTypes = null;
if (conflictingCount > 0) {
/* will hold number of parameters of each source method */
paramsCount = new int[srcMethods.size()];
paramTypes = collectParamTypes(paramsCount);
/* ROUND #2 - check conflicts of test methods of no-arg source methods */
BitSet tested = findNoArgMethods(paramsCount, conflicting);
BitSet noArgConflicting = new BitSet(srcMethods.size());
final int testedCount = tested.cardinality();
assert namesUsage.isEmpty();
int noArgConflictingCount = 0;
for (index = tested.nextSetBit(0);
index >= 0;
index = tested.nextSetBit(index + 1)) {
noArgConflictingCount += registerTestMethodName(
testMethodNames[index],
index,
namesUsage,
noArgConflicting);
}
namesUsage.clear();
assert noArgConflictingCount <= testedCount;
assert noArgConflictingCount == noArgConflicting.cardinality();
int noArgUniqueCount = methodsCount - conflictingCount;
if (noArgUniqueCount > 0) { /* among those for no-arg methods */
/* fixate all unique names of test methods for no-arg source method: */
BitSet noArgUnique = new BitSet(tested.size());
noArgUnique.or(tested);
noArgUnique.andNot(noArgConflicting);
for (index = noArgUnique.nextSetBit(0);
index >= 0;
index = noArgUnique.nextSetBit(index + 1)) {
String name = testMethodNames[index];
result[index] = name;
reservedNames.add(name); //fixate
}
}
if (noArgConflictingCount > 0) {/* among those for no-arg methods */
/* resolve conflicting names of test methods for no-arg source methods: */
Map<String, Integer> usageNumbers
= new HashMap<String, Integer>((noArgConflictingCount + 1) * 3 / 2);
noArgConflicting = tested;
for (index = noArgConflicting.nextSetBit(0);
index >= 0;
index = noArgConflicting.nextSetBit(index + 1)) {
String simpleName = testMethodNames[index];
Integer oldValue = usageNumbers.get(simpleName);
int suffix = (oldValue == null)
? 0
: oldValue.intValue();
String numberedName;
do {
suffix++;
numberedName = simpleName + suffix;
} while (reservedNames.contains(numberedName));
usageNumbers.put(simpleName, Integer.valueOf(suffix));
/* fixate immediately to ensure thare are really no conflicts */
result[index] = numberedName;
reservedNames.add(numberedName); //fixate
}
}
/*
* OK, now we know that names of test methods for no-arg source
* methods are resolved.
*/
conflicting.andNot(noArgConflicting);
conflictingCount -= noArgConflictingCount;
uniqueCount += noArgConflictingCount;
assert conflictingCount + uniqueCount == methodsCount;
}
String[] typeIdSuffixes = null;
String[] parCntSuffixes = null;
if (conflictingCount > 0) {
/*
* ROUND #3 - try to distinguish test method names by appending
* identifiers of the source method parameter types or, if there
* are too many parameters, by appending the number of parameters
*/
BitSet tested = (BitSet) conflicting.clone();
int testedCount = conflictingCount;
conflicting.clear();
conflictingCount = 0;
assert paramsCount != null;
assert paramTypes != null;
/* only needed for methods with low number of arguments */
TypeNameIdGenerator typeIdGenerator = null;
if (!paramTypes.isEmpty()) {
typeIdGenerator = TypeNameIdGenerator.createFor(
paramTypes,
workingCopy.getElements(),
workingCopy.getTypes());
}
assert namesUsage.isEmpty();
String[] methodNames = new String[methodsCount];
typeIdSuffixes = new String[methodsCount];
parCntSuffixes = new String[methodsCount];
for (index = tested.nextSetBit(0);
index >= 0;
index = tested.nextSetBit(index + 1)) {
int parCount = paramsCount[index];
String suffix;
if (parCount > MAX_SUFFIX_TYPES) {
suffix = parCntSuffixes[index] = makeParamCountSuffix(parCount);
} else {
List<? extends VariableElement> params
= srcMethods.get(index).getParameters();
StringBuilder buf = new StringBuilder(40);
for (int i = 0; i < parCount; i++) {
buf.append('_');
buf.append(typeIdGenerator.getParamTypeId(params.get(i).asType()));
}
suffix = typeIdSuffixes[index] = buf.toString();
}
String methodName = methodNames[index] = testMethodNames[index] + suffix;
/* check whether it is duplicite: */
conflictingCount += registerTestMethodName(
methodName,
index,
namesUsage,
conflicting);
}
namesUsage.clear();
uniqueCount = testedCount - conflictingCount;
if (uniqueCount > 0) {
/* fixate all new unique names */
BitSet unique = (BitSet) tested.clone();
unique.andNot(conflicting);
assert unique.cardinality() == uniqueCount;
for (index = unique.nextSetBit(0);
index >= 0;
index = unique.nextSetBit(index + 1)) {
String methodName = methodNames[index];
result[index] = methodName;
reservedNames.add(methodName); //fixate
}
}
}
if (conflictingCount > 0) {
/*
* ROUND #4 - try to distinguish test method names by appending
* identifiers of the source method parameter types and their types,
* or, if there are too many parameters, by only appending
* the number of parameters
*/
assert typeIdSuffixes != null;
assert parCntSuffixes != null;
BitSet tested = (BitSet) conflicting.clone();
int testedCount = conflictingCount;
conflicting.clear();
conflictingCount = 0;
assert namesUsage.isEmpty();
String[] methodNames = new String[methodsCount];
for (index = tested.nextSetBit(0);
index >= 0;
index = tested.nextSetBit(index + 1)) {
int parCount = paramsCount[index];
StringBuilder buf = new StringBuilder(60);
buf.append(testMethodNames[index]);
if (parCount <= MAX_SUFFIX_TYPES) {
assert typeIdSuffixes[index] != null;
buf.append(typeIdSuffixes[index]);
}
String parCntSuffix = parCntSuffixes[index];
if (parCntSuffix == null) {
assert (parCount <= MAX_SUFFIX_TYPES);
parCntSuffix = parCntSuffixes[index] = makeParamCountSuffix(parCount);
}
buf.append(parCntSuffix);
String methodName = methodNames[index] = buf.toString();
/* check whether it is duplicite: */
conflictingCount += registerTestMethodName(
methodName,
index,
namesUsage,
conflicting);
}
namesUsage.clear();
uniqueCount = testedCount - conflictingCount;
if (uniqueCount > 0) {
/* fixate all new unique names */
BitSet unique = (BitSet) tested.clone();
unique.andNot(conflicting);
assert unique.cardinality() == uniqueCount;
for (index = unique.nextSetBit(0);
index >= 0;
index = unique.nextSetBit(index + 1)) {
String methodName = methodNames[index];
result[index] = methodName;
reservedNames.add(methodName); //fixate
}
}
}
if (conflictingCount > 0) {
/* ROUND #5 - append number of parameters + sequential number */
Map<String, Integer> usageNumbers
= new HashMap<String, Integer>((conflictingCount * 3 + 1) / 2);
assert parCntSuffixes != null;
for (index = conflicting.nextSetBit(0);
index >= 0;
index = conflicting.nextSetBit(index + 1)) {
String noNumMethodName = testMethodNames[index] + parCntSuffixes[index] + '_';
Integer oldValue = usageNumbers.get(noNumMethodName);
int suffix = (oldValue == null)
? 0
: oldValue.intValue();
String methodName;
do {
suffix++;
methodName = noNumMethodName + suffix;
} while (reservedNames.contains(methodName));
usageNumbers.put(methodName, Integer.valueOf(suffix));
/* fixate immediately to ensure thare are really no conflicts */
result[index] = methodName;
reservedNames.add(methodName); //fixate
}
}
return Arrays.asList(result);
}
/**
*/
private static final String makeParamCountSuffix(int paramCount) {
return new StringBuilder(8)
.append('_')
.append(paramCount)
.append("args") //NOI18N
.toString();
}
/**
*
*/
private int registerTestMethodName(String testMethodName,
int index,
Map<String, Object>namesUsage,
BitSet conflictingNamesIndices) {
Object oldValue = namesUsage.put(testMethodName, Integer.valueOf(index));
boolean nameConflict = (oldValue != null)
|| (reservedNames != null)
&& (reservedNames.contains(testMethodName));
assert !conflictingNamesIndices.get(index);
int rv = 0;
if (nameConflict) {
if ((oldValue != null) && (oldValue != TRUE)) {
/*
* (oldValue == Integer) ... conflict with another method name
* detected
* (oldValue == null) ...... conflict with a reserved method
* name detected
* (oldValue == TRUE) ...... name has been already known to be
* in conflict with some other name
*/
assert (oldValue.getClass() == Integer.class);
int conflictingNameIndex = ((Integer) oldValue).intValue();
assert !conflictingNamesIndices.get(conflictingNameIndex);
conflictingNamesIndices.set(conflictingNameIndex);
rv++;
}
conflictingNamesIndices.set(index);
namesUsage.put(testMethodName, TRUE);
rv++;
}
return rv;
}
/**
* Collects names of accessible no-argument methods that are present
* in the given class and its superclasses. Methods inherited from the
* class's superclasses are taken into account, too.
*
* @param clazz class whose methods' names should be collected
* @param reservedMethodNames collection to which the method names
* should be added
*/
private void collectExistingMethodNames(TypeElement clazz,
Collection<String> reservedMethodNames) {
final Elements elements = workingCopy.getElements();
List<? extends Element> allMembers = elements.getAllMembers(clazz);
List<? extends ExecutableElement> methods = ElementFilter.methodsIn(allMembers);
if (!methods.isEmpty()) {
for (ExecutableElement method : methods) {
if (method.getParameters().isEmpty()) {
reservedMethodNames.add(method.getSimpleName().toString());
}
}
}
}
/**
* Collects types of parameters used by source methods.
* Methods without parameters and methods having more than
* {@value #MAX_SUFFIX_TYPES} parameters are skipped.
* This method also stores number of parameters of each method to the given
* array.
*
* @param paramCount an empty array of numbers - its length must be equal
* to the number of {@link #srcMethods}
* @return types of parameters of methods having an overloaded name
*/
private Collection<TypeMirror> collectParamTypes(int[] paramsCount) {
Collection<TypeMirror> paramTypes = new ArrayList<TypeMirror>(
srcMethods.size());
int index = -1;
for (ExecutableElement srcMethod : srcMethods) {
index++;
List<? extends VariableElement> params = srcMethod.getParameters();
if (params.isEmpty()) {
paramsCount[index] = 0;
continue;
}
final int parCount;
paramsCount[index] = (parCount = params.size());
if (parCount <= MAX_SUFFIX_TYPES) {
for (int i = 0; i < parCount; i++) {
paramTypes.add(params.get(i).asType());
}
}
}
return !paramTypes.isEmpty() ? paramTypes
: Collections.<TypeMirror>emptyList();
}
/**
* Returns a list of methods contained directly in the given class.
*
* @param classElem class whose methods should be returned
* @return list of methods in the given class
*/
private static List<ExecutableElement> getExistingMethods(
final TypeElement classElem) {
List<? extends Element> elements = classElem.getEnclosedElements();
if (elements.isEmpty()) {
return Collections.<ExecutableElement>emptyList();
}
List<ExecutableElement> methods = ElementFilter.methodsIn(elements);
return !methods.isEmpty() ? methods
: Collections.<ExecutableElement>emptyList();
}
private static String buildTestMethodName(String srcMethodName) {
int length = srcMethodName.length();
StringBuilder buf = new StringBuilder(length + 4);
buf.append("test"); //NOI18N
buf.append(Character.toUpperCase(srcMethodName.charAt(0)));
if (length != 1) {
buf.append(srcMethodName.substring(1));
}
return buf.toString();
}
/**
* Finds indices of test methods which have conflicting names and test
* no-argument source methods.
*
* @param paramsCount array containing number of parameters of each source
* method
* @param conflicting bitmap - set bits determine indices of test methods
* having conflicting names
* @return bitmap which holds information which of these conflicting test
* methods is a test for a no-argument source method
*/
private static BitSet findNoArgMethods(int[] paramsCount, BitSet conflicting) {
BitSet result = new BitSet(paramsCount.length);
for (int index = 0; index < paramsCount.length; index++) {
if ((paramsCount[index] == 0) && conflicting.get(index)) {
result.set(index);
}
}
return result;
}
}