/*==========================================================================*\
| $Id: TestCaseVisitor.java,v 1.4 2009/09/13 12:59:29 aallowat Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2009 Virginia Tech
|
| This file is part of Web-CAT Eclipse Plugins.
|
| Web-CAT is free software; you can redistribute it and/or modify
| it under the terms of the GNU General Public License as published by
| the Free Software Foundation; either version 2 of the License, or
| (at your option) any later version.
|
| Web-CAT is distributed in the hope that it will be useful,
| but WITHOUT ANY WARRANTY; without even the implied warranty of
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
| GNU General Public License for more details.
|
| You should have received a copy of the GNU General Public License
| along with Web-CAT; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/
package net.sf.webcat.eclipse.cxxtest.internal.generator;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.cdt.core.model.CModelException;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICElementVisitor;
import org.eclipse.cdt.core.model.IFunctionDeclaration;
import org.eclipse.cdt.core.model.IMethodDeclaration;
import org.eclipse.cdt.core.model.IStructure;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.model.IUsing;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
/**
* A visitor class that traverses the DOM tree of a C++ project, collecting
* information about the CxxTest test suites that are implemented in the
* project.
*
* @author Tony Allevato (Virginia Tech Computer Science)
* @author latest changes by: $Author: aallowat $
* @version $Revision: 1.4 $ $Date: 2009/09/13 12:59:29 $
*/
public class TestCaseVisitor implements ICElementVisitor
{
public TestCaseVisitor(String file)
{
driverFileName = file;
}
/**
* Called by the runtime when each element of the C++ project is
* processed.
*/
public boolean visit(ICElement element) throws CoreException
{
boolean visitChildren;
switch(element.getElementType())
{
case ICElement.C_MODEL:
case ICElement.C_PROJECT:
case ICElement.C_CCONTAINER:
case ICElement.C_NAMESPACE:
/*
* Any of these container types should be unconditionally
* visited for child elements.
*/
visitChildren = true;
currentSuite = null;
break;
case ICElement.C_UNIT:
visitChildren = shouldVisitFile((ITranslationUnit)element);
break;
case ICElement.C_USING:
/*
* Check to see if it's a reference to "std".
*/
visitChildren = false;
checkForStandardLibrary((IUsing)element);
break;
case ICElement.C_CLASS:
case ICElement.C_STRUCT:
/*
* Only visit a class's or struct's children if it inherits
* from the CxxTest suite class.
*/
visitChildren = isClassTestSuite((IStructure)element);
break;
case ICElement.C_METHOD_DECLARATION:
case ICElement.C_METHOD:
/*
* Check the method to see if it's named properly, and if so
* add it to the list of tests for the current suite.
*/
visitChildren = false;
if(currentSuite != null)
checkMethod((IMethodDeclaration)element);
break;
case ICElement.C_FUNCTION_DECLARATION:
case ICElement.C_FUNCTION:
/*
* Check global functions to see if the user has created a
* main() function. If so, we need to generate the test
* runner as a static object instead.
*/
visitChildren = false;
checkForMain((IFunctionDeclaration)element);
break;
default:
visitChildren = false;
break;
}
return visitChildren;
}
private boolean shouldVisitFile(ITranslationUnit unit)
{
IPath path = unit.getLocation();
String name = path.lastSegment();
if(name.equals(driverFileName))
{
return false;
}
else
{
if(containsCxxTestIncludeDirective(unit))
{
suites.addPossibleTestFile(path.toOSString());
}
return true;
}
}
private boolean containsCxxTestIncludeDirective(ITranslationUnit unit)
{
String contents = String.valueOf(unit.getContents());
Matcher matcher = includePattern.matcher(contents);
if(matcher.find())
{
return true;
}
else
return false;
}
/**
* Gets the test suites that were processed by visiting the project DOM.
*
* @return An array of CxxTestSuiteInfo objects that represent the test
* suites in the project.
*/
public TestSuiteCollection getSuites()
{
return suites;
}
/**
* Gets a value indicating whether or not a reference to namespace std
* was made.
*
* @return true if namespace std was referenced; otherwise, false.
*/
public boolean isUsingStandardLibrary()
{
return usesStandardLibrary;
}
/**
* Check the specified "using" directive to see if it matches "std".
*
* @param element The IUsing directive to check.
*/
private void checkForStandardLibrary(IUsing element)
{
if("std".equals(element.getElementName())) //$NON-NLS-1$
usesStandardLibrary = true;
}
/**
* Determines if a class is a valid CxxTest test suite by checking
* for CxxTest::TestSuite in the superclass list.
*
* @param element the class handle to check
*
* @return true if the class is a test suite; otherwise, false.
*/
private boolean isClassTestSuite(IStructure element)
{
if(hasBeenGenerated(element))
return false;
String[] supers = element.getSuperClassesNames();
for(int i = 0; i < supers.length; i++)
{
Matcher matcher = superclassPattern.matcher(supers[i]);
if(matcher.matches())
{
generatedSuites.add(element);
IPath path = element.getTranslationUnit().getLocation();
currentSuite = new TestSuite(element.getElementName(),
path.toOSString(), getLineNumber(element));
suites.addSuite(currentSuite);
ITranslationUnit containingUnit = element.getTranslationUnit();
if(containingUnit != null)
suites.removePossibleTestFile(path.toOSString());
return true;
}
}
return false;
}
/**
* Determines if the test suite for this class has already been
* discovered.
*
* @param element the class handle to check
*
* @return true if the suite was already discovered; otherwise, false.
*/
private boolean hasBeenGenerated(IStructure element)
{
return generatedSuites.contains(element);
}
/**
* Checks a method inside a test suite class to determine if it
* is a valid test method (void return value, no arguments, name
* begins with "test"). If so, it is added to the suite's list of
* tests.
*
* @param element the method handle to be checked.
*/
private void checkMethod(IMethodDeclaration element)
{
String name = element.getElementName();
int lineNum = getLineNumber(element);
boolean isStatic = false;
try
{
isStatic = element.isStatic();
}
catch(CModelException e) { }
if(name.startsWith("Test") || name.startsWith("test")) //$NON-NLS-1$ //$NON-NLS-2$
{
if("void".equals(element.getReturnType()) //$NON-NLS-1$
&& isMethodParameterless(element))
{
currentSuite.addTestCase(
new TestCase(element.getElementName(),
getLineNumber(element)));
}
}
else if(name.equals("createSuite")) //$NON-NLS-1$
{
if(isStatic &&
element.getReturnType().indexOf('*') >= 0 &&
isMethodParameterless(element))
currentSuite.setCreateLineNumber(lineNum);
}
else if(name.equals("destroySuite")) //$NON-NLS-1$
{
String[] params = element.getParameterTypes();
if(isStatic && params.length == 1 &&
params[0].indexOf('*') >= 0 &&
"void".equals(element.getReturnType())) //$NON-NLS-1$
currentSuite.setDestroyLineNumber(lineNum);
}
}
/**
* Checks a global function to determine if it is the main() function.
*
* @param element the function handle to be checked.
*/
private void checkForMain(IFunctionDeclaration element)
{
if("main".equals(element.getElementName())) //$NON-NLS-1$
suites.setDoesMainFunctionExist(true);
}
/**
* A convenience function to check if a function takes no arguments
* (CDT's DOM treats a function with no arguments differently from one
* with a single "void" argument).
*
* @param element the method handle to check.
*
* @return true if the function has no arguments; otherwise, false.
*/
private boolean isMethodParameterless(IMethodDeclaration element)
{
return (element.getNumberOfParameters() == 0 ||
(element.getNumberOfParameters() == 1 &&
"void".equals(element.getParameterTypes()[0]))); //$NON-NLS-1$
}
/**
* A convenience function to get the line number of a method.
*
* @param method the method number to get the line number of.
*
* @return An integer representing the line number of the method
* in the source code.
*/
private int getLineNumber(IMethodDeclaration method)
{
try
{
return method.getSourceRange().getStartLine();
}
catch (CModelException e)
{
return 1;
}
}
private int getLineNumber(IStructure structure)
{
try
{
return structure.getSourceRange().getStartLine();
}
catch (CModelException e)
{
return 1;
}
}
//~ Static/instance variables ............................................
/* The current suite being processed. */
private TestSuite currentSuite = null;
/* A collection of all the test suites processed. */
private TestSuiteCollection suites = new TestSuiteCollection();
/*
* A regular expression that matches the name of the CxxTest suite
* base class in the superclass list. This will match either
* TestSuite, CxxTest::TestSuite, or ::CxxTest::TestSuite.
*/
private Pattern superclassPattern =
Pattern.compile("((::)?\\s*CxxTest\\s*::\\s*)?TestSuite"); //$NON-NLS-1$
private Pattern includePattern =
Pattern.compile("\\s*#\\s*include\\s+<cxxtest/TestSuite.h>"); //$NON-NLS-1$
/*
* Keeps track of whether a "using namespace std" directive was
* encountered during the traversal.
*/
private boolean usesStandardLibrary = true;
/*
* Contains the IStructure handles of all the test suites encountered,
* for quick lookup, such that each suite will only be generated once
* (sometimes the tree traversal will encounter the same element twice,
* may have to do with resource deltas?)
*/
private Set<IStructure> generatedSuites = new HashSet<IStructure>();
private String driverFileName = null;
}