IDEA-70123 Test Assistant: Make it possible to work with test data for existing test classes that are not properly annotated

Improved test data file guessing (e.g. our project has a lot of tests and test data files that conform to '*InComment*' pattern)
This commit is contained in:
Denis Zhdanov
2011-05-26 14:56:58 +04:00
parent cb647c4d78
commit 42dc87cba9
2 changed files with 151 additions and 45 deletions
@@ -4,6 +4,7 @@ import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.ide.util.gotoByName.GotoFileModel;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
@@ -12,6 +13,7 @@ import com.intellij.psi.PsiMethod;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.testIntegration.TestFramework;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ConcurrentHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,23 +27,23 @@ import java.util.concurrent.TimeUnit;
/**
* There is a possible case that particular test class is not properly configured with test annotations but uses test data files.
* This class contains utility methods for guessing test data files location and name patterns from existing one.
*
*
* @author Denis Zhdanov
* @since 5/24/11 2:28 PM
*/
public class TestDataGuessByExistingFilesUtil {
private static final long CACHE_ENTRY_TTL_MS = TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
private static final Map<String, Pair<TestDataDescriptor, Long>> CACHE = new ConcurrentHashMap<String, Pair<TestDataDescriptor, Long>>();
private TestDataGuessByExistingFilesUtil() {
}
/**
* Tries to guess what test data files match to the given method if it's test method and there are existing test data
* files for the target test class.
*
*
* @param method test method candidate
* @return collection of paths to the test data files for the given test if it's possible to guess them;
* <code>null</code> otherwise
@@ -57,7 +59,7 @@ public class TestDataGuessByExistingFilesUtil {
}
return collectTestDataByExistingFiles(psiFile, getTestName(method.getName()));
}
@Nullable
private static <T extends PsiElement> T getParent(@NotNull PsiElement element, Class<T> clazz) {
for (PsiElement e = element; e != null; e = e.getParent()) {
@@ -67,7 +69,7 @@ public class TestDataGuessByExistingFilesUtil {
}
return null;
}
@Nullable
static List<String> collectTestDataByExistingFiles(@NotNull PsiFile psiFile, @NotNull String testName) {
GotoFileModel model = new GotoFileModel(psiFile.getProject());
@@ -78,7 +80,7 @@ public class TestDataGuessByExistingFilesUtil {
return descriptor.generate(testName);
}
@Nullable
private static String getTestName(@NotNull PsiMethod method) {
final PsiClass psiClass = getParent(method, PsiClass.class);
@@ -98,7 +100,7 @@ public class TestDataGuessByExistingFilesUtil {
if (framework == null || isUtilityMethod(method, psiClass, framework)) {
return null;
}
return getTestName(method.getName());
}
@@ -106,19 +108,19 @@ public class TestDataGuessByExistingFilesUtil {
if (method == framework.findSetUpMethod(psiClass) || method == framework.findTearDownMethod(psiClass)) {
return true;
}
// JUnit3
if (framework.getClass().getName().contains("JUnit3")) {
return !method.getName().startsWith("test");
}
// JUnit4
else if (framework.getClass().getName().contains("JUnit4")) {
return !AnnotationUtil.isAnnotated(method, "org.junit.Test", false);
}
return false;
}
@NotNull
public static String getTestName(@NotNull String methodName) {
return methodName.startsWith("test") ? methodName.substring("test".length()) : methodName;
@@ -134,7 +136,7 @@ public class TestDataGuessByExistingFilesUtil {
final Pair<TestDataDescriptor, Long> cached = CACHE.get(psiClass.getQualifiedName());
if (cached != null && cached.second + CACHE_ENTRY_TTL_MS > System.currentTimeMillis()) {
return cached.first.isComplete() ? cached.first : null;
}
}
TestFramework[] frameworks = Extensions.getExtensions(TestFramework.EXTENSION_NAME);
TestFramework framework = null;
@@ -151,22 +153,28 @@ public class TestDataGuessByExistingFilesUtil {
final PsiElement setUpMethod = framework.findSetUpMethod(psiClass);
final PsiElement tearDownMethod = framework.findTearDownMethod(psiClass);
TestDataDescriptor descriptor = new TestDataDescriptor();
List<String> testNames = new ArrayList<String>();
for (PsiMethod method : psiClass.getMethods()) {
final String name = getTestName(method.getName());
if (method == setUpMethod || method == tearDownMethod || name.equals(psiClass.getName())) {
if (method == setUpMethod || method == tearDownMethod || name.equals(psiClass.getName())
|| isUtilityMethod(method, psiClass, framework))
{
continue;
}
final Collection<VirtualFile> matchedFiles = getMatchedFiles(gotoModel, name);
if (!descriptor.isComplete()) {
descriptor.populate(name, matchedFiles);
}
if (descriptor.isComplete()) {
CACHE.put(psiClass.getQualifiedName(), new Pair<TestDataDescriptor, Long>(descriptor, System.currentTimeMillis()));
return descriptor;
}
testNames.add(name);
}
CACHE.put(psiClass.getQualifiedName(), new Pair<TestDataDescriptor, Long>(new TestDataDescriptor(), System.currentTimeMillis()));
return null;
final Pair<String, Collection<VirtualFile>> matchedFiles = getMatchedFiles(gotoModel, testNames, psiClass);
if (matchedFiles == null) {
CACHE.put(psiClass.getQualifiedName(), new Pair<TestDataDescriptor, Long>(descriptor, System.currentTimeMillis()));
return descriptor;
}
descriptor.populate(matchedFiles.first, matchedFiles.second);
if (!descriptor.isComplete()) {
CACHE.put(psiClass.getQualifiedName(), new Pair<TestDataDescriptor, Long>(descriptor, System.currentTimeMillis()));
return null;
}
CACHE.put(psiClass.getQualifiedName(), new Pair<TestDataDescriptor, Long>(descriptor, System.currentTimeMillis()));
return descriptor;
}
//@NotNull
@@ -196,28 +204,130 @@ public class TestDataGuessByExistingFilesUtil {
// }, project);
// return result;
//}
@NotNull
private static Collection<VirtualFile> getMatchedFiles(@NotNull GotoFileModel gotoModel, @NotNull String testName) {
String pattern = String.format("*%s*", testName);
final NameUtil.Matcher matcher = NameUtil.buildMatcher(pattern, 0, true, true, pattern.toLowerCase().equals(pattern));
List<VirtualFile> result = new ArrayList<VirtualFile>();
@Nullable
private static Pair<String, Collection<VirtualFile>> getMatchedFiles(@NotNull GotoFileModel gotoModel,
@NotNull Collection<String> testNames,
@NotNull PsiClass psiClass)
{
List<Trinity<NameUtil.Matcher, String, String>> input = new ArrayList<Trinity<NameUtil.Matcher, String, String>>();
for (String testName : testNames) {
String pattern = String.format("*%s*", testName);
input.add(new Trinity<NameUtil.Matcher, String, String>(
NameUtil.buildMatcher(pattern, 0, true, true, pattern.toLowerCase().equals(pattern)), testName, pattern
));
}
String dir = null;
String testName = null;
List<VirtualFile> files = new ArrayList<VirtualFile>();
for (String name : gotoModel.getNames(false)) {
if (matcher.matches(name)) {
final Object[] elements = gotoModel.getElementsByName(name, false, pattern);
if (elements != null) {
for (Object element : elements) {
if (element instanceof PsiFile) {
result.add(((PsiFile)element).getVirtualFile());
}
boolean currentNameProcessed = false;
for (Trinity<NameUtil.Matcher, String, String> trinity : input) {
if (!trinity.first.matches(name)) {
continue;
}
final Object[] elements = gotoModel.getElementsByName(name, false, trinity.third);
if (elements == null) {
continue;
}
for (Object element : elements) {
if (!(element instanceof PsiFile)) {
continue;
}
final VirtualFile file = ((PsiFile)element).getVirtualFile();
if (file == null) {
continue;
}
final String filePath = PathUtil.getFileName(file.getPath()).toLowerCase();
int i = filePath.indexOf(trinity.second.toLowerCase());
// Skip files that doesn't contain target test name and files that contain digit after target test name fragment.
// Example: there are tests with names 'testEnter()' and 'testEnter2()' and we don't want test data file 'testEnter2'
// to be matched to the test 'testEnter()'.
if (i < 0 || (i + trinity.second.length() < filePath.length())
&& Character.isDigit(filePath.charAt(i + trinity.second.length())))
{
continue;
}
currentNameProcessed = true;
final String parentPath = PathUtil.getParentPath(file.getPath());
if (dir == null || dir.equals(parentPath)) {
dir = parentPath;
if (testName == null || !testName.equals(trinity.second)) {
files.clear();
}
testName = trinity.second;
files.add(file);
continue;
}
if (moreRelevantPath(file, files, psiClass, trinity.second)) {
testName = trinity.second;
dir = parentPath;
files.clear();
files.add(file);
}
}
if (currentNameProcessed) {
break;
}
}
}
return result;
return (testName == null || files.isEmpty()) ? null : new Pair<String, Collection<VirtualFile>>(testName, files);
}
private static boolean moreRelevantPath(@NotNull VirtualFile candidate, @NotNull List<VirtualFile> current, @NotNull PsiClass psiClass,
@NotNull String testName)
{
final String className = psiClass.getQualifiedName();
if (className == null) {
return false;
}
final String candidatePath = candidate.getPath();
final String candidateDir = PathUtil.getParentPath(candidatePath);
final String currentDir = PathUtil.getParentPath(current.get(0).getPath());
// By package.
int i = className.lastIndexOf(".");
if (i >= 0) {
String packageAsPath = className.substring(0, i).replace('.', '/').toLowerCase();
if (candidateDir.toLowerCase().contains(packageAsPath) && !currentDir.toLowerCase().contains(packageAsPath)) {
return true;
}
}
// By class name.
String pattern = className.toLowerCase();
if (pattern.endsWith("test")) {
pattern = pattern.substring(0, pattern.length() - "Test".length());
}
i = pattern.lastIndexOf('.');
if (i >= 0) {
pattern = pattern.substring(i + 1);
}
if (candidateDir.toLowerCase().contains(pattern) && !currentDir.toLowerCase().contains(pattern)) {
return true;
}
// By test name.
if (PathUtil.getFileName(candidatePath).toLowerCase().startsWith(testName.toLowerCase())) {
boolean moreRelevant = true;
for (VirtualFile file : current) {
if (PathUtil.getFileName(file.getPath()).toLowerCase().startsWith(testName.toLowerCase())) {
moreRelevant = false;
break;
}
}
if (moreRelevant) {
return true;
}
}
return false;
}
private static class TestLocationDescriptor {
public String dir;
@@ -245,13 +355,6 @@ public class TestDataGuessByExistingFilesUtil {
startWithLowerCase = testNameStartsWithLowerCase;
}
// Skip files that doesn't contain target test name and files that contain digit after target test name fragment.
// Example: there are tests with names 'testEnter()' and 'testEnter2()' and we don't want test data file 'testEnter2'
// to be matched to the test 'testEnter()'.
if (i < 0 || (i + testName.length() < fileName.length()) && Character.isDigit(fileName.charAt(i + testName.length()))) {
return;
}
filePrefix = fileName.substring(0, i);
fileSuffix = fileName.substring(i + testName.length());
ext = matched.getExtension();
@@ -55,6 +55,9 @@ public class TestDataReferenceCollector {
private List<String> collectTestDataReferences(final PsiMethod method, final Map<String, Computable<String>> argumentMap) {
final List<String> result = new ArrayList<String>();
if (myTestDataPath == null) {
return result;
}
method.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {