Commit a2fe4cc1 authored by Tim Horst's avatar Tim Horst

Merge branch 'refactor' into 'development'

Refactor

See merge request !7
parents f4f396e0 14de7479
Pipeline #588 passed with stage
in 40 seconds
......@@ -26,3 +26,4 @@ hs_err_pid*
.gradle
build
*.iml
/out/
......@@ -13,15 +13,35 @@ repositories {
}
dependencies {
compile 'com.github.javaparser:javaparser-core:3.13.10'
compile 'com.opencsv:opencsv:3.9'
implementation 'com.github.javaparser:javaparser-core:3.13.10'
implementation 'com.opencsv:opencsv:3.9'
implementation 'javax.annotation:javax.annotation-api:1.3.2'
implementation 'com.google.code.findbugs:jsr305:3.0.+'
testImplementation 'org.assertj:assertj-core:3.12.+'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.+'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.+'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.+'
}
group = 'nl.f00f'
version = '0.0.2'
version = '0.0.3'
archivesBaseName = 'test-smell-detector'
sourceCompatibility = '1.8'
sourceCompatibility = '1.11'
test {
sourceCompatibility = '1.11'
targetCompatibility = '1.11'
failFast = true
testLogging {
exceptionFormat = 'FULL'
showStandardStreams = true
}
useJUnitPlatform()
}
task javadocJar(type: Jar) {
classifier = 'javadoc'
......
......@@ -5,12 +5,39 @@ import com.github.javaparser.ast.CompilationUnit;
import java.io.FileNotFoundException;
import java.util.List;
/**
* An abstract class for a smell.
*/
public abstract class AbstractSmell {
/**
* Retrieves the name of the smell.
* @return The name of the smell
*/
public abstract String getSmellName();
public abstract boolean getHasSmell();
/**
* Returns whether there was a smell found or not.
* @return {@code true} if a smell has been found, {@code false} otherwise
*/
public abstract boolean hasSmell();
public abstract void runAnalysis(CompilationUnit testFileCompilationUnit,CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException;
/**
* Checks whether the specified smell is present in the file.
* @param testFileCompilationUnit The test file
* @param productionFileCompilationUnit The production file
* @param testFileName The name of the test file
* @param productionFileName The name of the production file
* @throws FileNotFoundException When a file is not found
*/
public abstract void runAnalysis(final CompilationUnit testFileCompilationUnit,
final CompilationUnit productionFileCompilationUnit,
final String testFileName,
final String productionFileName) throws FileNotFoundException;
/**
* Returns the list of the methods/classes found in the file.
* @return The list of methods/classes
*/
public abstract List<SmellyElement> getSmellyElements();
}
......@@ -2,10 +2,26 @@ package testsmell;
import java.util.Map;
/**
* An abstract class that represents an element from a file that might be smelly.
*/
public abstract class SmellyElement {
/**
* Retrieves the name of the element.
* @return The name of the element
*/
public abstract String getElementName();
public abstract boolean getHasSmell();
/**
* Returns whether a smell was found in this element or not.
* @return {@code true} if a smell has been found, {@code false} otherwise
*/
public abstract boolean hasSmell();
/**
* Returns the stored data about this element.
* @return The data stored in this element
*/
public abstract Map<String, String> getData();
}
package testsmell;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* The testclass that will be evaluated for smells.
*/
public class TestClass extends SmellyElement {
/**
* The name of the class.
*/
private String className;
/**
* Whether this class has a smell or not.
*/
private boolean hasSmell;
/**
* The data about the methods and the smells they contain.
*/
private Map<String, String> data;
public TestClass(String className) {
/**
* Creates a new TestClass instance.
* @param className The name of the class
*/
public TestClass(final String className) {
this.className = className;
data = new HashMap<>();
}
public void setHasSmell(boolean hasSmell) {
/**
* Sets the state of whether or not this class has a smell.
* @param hasSmell Whether this class has a smell or not
*/
public void setHasSmell(final boolean hasSmell) {
this.hasSmell = hasSmell;
}
public void addDataItem(String name, String value) {
data.put(name, value);
/**
* Adds an item to the data object.
* @param name The name of the object
* @param value The value of the object
*/
public void addDataItem(final String name, final String value) {
this.data.put(name, value);
}
/**
* Returns the name of the class.
* @return The name of the class
*/
@Override
public String getElementName() {
return className;
}
/**
* Returns whether this class has a smell or not.
* @return {@code true} if a smell has been found, {@code false} otherwise
*/
@Override
public boolean getHasSmell() {
public boolean hasSmell() {
return hasSmell;
}
/**
* Returns the data that this object holds.
* @return The data of this object
*/
@Override
public Map<String, String> getData() {
return data;
return this.data;
}
}
......@@ -5,50 +5,94 @@ import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* This is an object that serves as a means of combining the testfiles and the productionfiles.
*/
public class TestFile {
/**
* The paths to the test and production files.
*/
private String testFilePath, productionFilePath;
/**
* A list of smells that were found in the testfile.
*/
private List<AbstractSmell> testSmells;
/**
* Creates a new testfile object.
* @param testFilePath The path to the test file
* @param productionFilePath The path to the production file
*/
public TestFile(final String testFilePath, final String productionFilePath) {
this.testFilePath = testFilePath;
this.productionFilePath = productionFilePath;
this.testSmells = new ArrayList<>();
}
/**
* Retrieves the filepath of the production file.
* @return The path of the production file
*/
public String getProductionFilePath() {
return productionFilePath;
return this.productionFilePath;
}
/**
* Retrieves the file path of the test file.
* @return The path of the test file
*/
public String getTestFilePath() {
return testFilePath;
return this.testFilePath;
}
/**
* Retrieves the list of smells in the test file.
* @return List of smells in the test file
*/
public List<AbstractSmell> getTestSmells() {
return testSmells;
return this.testSmells;
}
public boolean getHasProductionFile() {
return ((productionFilePath != null && !productionFilePath.isEmpty()));
/**
* Returns whether or not there is a production file.
* @return {@code true} if there is a production file, {@code false} otherwise
*/
public boolean hasProductionFile() {
return ((this.productionFilePath != null && !this.productionFilePath.isEmpty()));
}
public TestFile(String testFilePath, String productionFilePath) {
this.testFilePath = testFilePath;
this.productionFilePath = productionFilePath;
this.testSmells = new ArrayList<>();
}
public void addSmell(AbstractSmell smell) {
testSmells.add(smell);
}
public String getTagName(){
return testFilePath.split("\\\\")[4];
/**
* Adds a smell to this file.
* @param smell The smell to add to the file
*/
public void addSmell(final AbstractSmell smell) {
this.testSmells.add(smell);
}
/**
* Retrieves the name of the test file.
* @return The name of the test file
*/
public String getTestFileName(){
int lastIndex = testFilePath.lastIndexOf("\\");
return testFilePath.substring(lastIndex+1,testFilePath.length());
int lastIndex = this.testFilePath.lastIndexOf("\\");
return this.testFilePath.substring(lastIndex+1, this.testFilePath.length());
}
/**
* Retrieves the name of the test file without the extension.
* @return The name of the test file without the extension
*/
public String getTestFileNameWithoutExtension(){
int lastIndex = getTestFileName().lastIndexOf(".");
return getTestFileName().substring(0,lastIndex);
}
/**
* Retrieves the name of the production file without the extension.
* @return The name of the production file without the extension
*/
public String getProductionFileNameWithoutExtension(){
int lastIndex = getProductionFileName().lastIndexOf(".");
if(lastIndex==-1)
......@@ -56,30 +100,45 @@ public class TestFile {
return getProductionFileName().substring(0,lastIndex);
}
/**
* Retrieves the name of the production file.
* @return The name of the production file
*/
public String getProductionFileName(){
int lastIndex = productionFilePath.lastIndexOf("\\");
if(lastIndex==-1)
return "";
return productionFilePath.substring(lastIndex+1,productionFilePath.length());
return this.productionFilePath.substring(lastIndex+1, this.productionFilePath.length());
}
/**
* Retrieves the relative path of the test file.
* @return The relative path of the test file
*/
public String getRelativeTestFilePath() {
String[] splitString = testFilePath.split("\\\\");
String[] splitString = this.testFilePath.split("\\\\");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 5; i++) {
stringBuilder.append(splitString[i] + "\\");
}
return testFilePath.substring(stringBuilder.toString().length()).replace("\\", "/");
return this.testFilePath.substring(stringBuilder.toString().length())
.replace("\\", "/");
}
/**
* Retrieves the relative path of the production file.
* @return The relative path of the production file
*/
public String getRelativeProductionFilePath() {
if (!StringUtils.isEmpty(productionFilePath)) {
String[] splitString = productionFilePath.split("\\\\");
if (!StringUtils.isEmpty(this.productionFilePath)) {
String[] splitString = this.productionFilePath.split("\\\\");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 5; i++) {
stringBuilder.append(splitString[i] + "\\");
}
return productionFilePath.substring(stringBuilder.toString().length()).replace("\\", "/");
return this.productionFilePath.substring(stringBuilder.toString().length())
.replace("\\", "/");
} else {
return "";
......
package testsmell;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* The testmethod that will be evaluated for smells.
*/
public class TestMethod extends SmellyElement {
/**
* The name of the method
*/
private String methodName;
private boolean hasSmell;
/**
* Whether this method has a smell or not.
*/
private @Nullable boolean hasSmell;
/**
* The data of the method about what smells it has.
*/
private Map<String, String> data;
/**
* Creates a new test method object.
* @param methodName The name of the method
*/
public TestMethod(String methodName) {
this.methodName = methodName;
data = new HashMap<>();
this.data = new HashMap<>();
}
public void setHasSmell(boolean hasSmell) {
/**
* Sets whether the method has a smell or not.
* @param hasSmell Whether the method has a smell or not
*/
public void setHasSmell(final boolean hasSmell) {
this.hasSmell = hasSmell;
}
public void addDataItem(String name, String value) {
data.put(name, value);
/**
* Adds a smell to the data object and whether it is present in the method or not.
* @param name The name of the smell
* @param value Whether the smell is present in the test or not
*/
public void addDataItem(final String name, final String value) {
this.data.put(name, value);
}
/**
* Retrieves the name of the method.
* @return The name of the method
*/
@Override
public String getElementName() {
return methodName;
return this.methodName;
}
/**
* Retrieves whether this method has a smell or not.
* @return {@code true} if a smell has been found, {@code false} otherwise
*/
@Override
public boolean getHasSmell() {
return hasSmell;
public boolean hasSmell() {
return this.hasSmell;
}
/**
* Retrieves the data about the smells that this method has.
* @return The data about the smells that this method has
*/
@Override
public Map<String, String> getData() {
return data;
return this.data;
}
}
......@@ -12,8 +12,14 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Class that can analyze a test file when given a path.
*/
public class TestSmellDetector {
/**
* A list of testsmells.
*/
private List<AbstractSmell> testSmells;
/**
......@@ -23,31 +29,6 @@ public class TestSmellDetector {
initializeSmells();
}
private void initializeSmells(){
testSmells = new ArrayList<>();
testSmells.add(new AssertionRoulette());
testSmells.add(new ConditionalTestLogic());
testSmells.add(new ConstructorInitialization());
testSmells.add(new DefaultTest());
testSmells.add(new EmptyTest());
testSmells.add(new ExceptionCatchingThrowing());
testSmells.add(new GeneralFixture());
testSmells.add(new MysteryGuest());
testSmells.add(new PrintStatement());
testSmells.add(new RedundantAssertion());
testSmells.add(new SensitiveEquality());
testSmells.add(new VerboseTest());
testSmells.add(new SleepyTest());
testSmells.add(new EagerTest());
testSmells.add(new LazyTest());
testSmells.add(new DuplicateAssert());
testSmells.add(new UnknownTest());
testSmells.add(new IgnoredTest());
testSmells.add(new ResourceOptimism());
testSmells.add(new MagicNumberTest());
testSmells.add(new DependentTest());
}
/**
* Factory method that provides a new instance of the TestSmellDetector
*
......@@ -58,18 +39,47 @@ public class TestSmellDetector {
}
/**
* Provides the names of the smells that are being checked for in the code
* Creates an instance of every smell analyzer.
*/
private void initializeSmells(){
this.testSmells = new ArrayList<>();
this.testSmells.add(new AssertionRoulette());
this.testSmells.add(new ConditionalTestLogic());
this.testSmells.add(new ConstructorInitialization());
this.testSmells.add(new DefaultTest());
this.testSmells.add(new EmptyTest());
this.testSmells.add(new ExceptionCatchingThrowing());
this.testSmells.add(new GeneralFixture());
this.testSmells.add(new MysteryGuest());
this.testSmells.add(new PrintStatement());
this.testSmells.add(new RedundantAssertion());
this.testSmells.add(new SensitiveEquality());
this.testSmells.add(new SleepyTest());
this.testSmells.add(new EagerTest());
this.testSmells.add(new LazyTest());
this.testSmells.add(new DuplicateAssert());
this.testSmells.add(new UnknownTest());
this.testSmells.add(new IgnoredTest());
this.testSmells.add(new MagicNumberTest());
}
/**
* Provides the names of the smells that are being checked for in the code.
*
* @return list of smell names
*/
public List<String> getTestSmellNames() {
return testSmells.stream().map(AbstractSmell::getSmellName).collect(Collectors.toList());
return this.testSmells.stream().map(AbstractSmell::getSmellName).collect(Collectors.toList());
}
/**
* Loads the java source code file into an AST and then analyzes it for the existence of the different types of test smells
* Loads the java source code file into an AST.
* and then analyzes it for the existence of the different types of test smells
* @param testFile The files to test
* @return A testfile with the data about the smells
* @throws IOException When a file is not found
*/
public TestFile detectSmells(TestFile testFile) throws IOException {
public TestFile detectSmells(final TestFile testFile) throws IOException {
CompilationUnit testFileCompilationUnit=null, productionFileCompilationUnit=null;
FileInputStream testFileInputStream, productionFileInputStream;
......@@ -82,23 +92,23 @@ public class TestSmellDetector {
if(!StringUtils.isEmpty(testFile.getProductionFilePath())){
JavaParser parser = new JavaParser();
productionFileInputStream = new FileInputStream(testFile.getProductionFilePath());
productionFileCompilationUnit = parser.parse(productionFileInputStream).getResult().get();
productionFileCompilationUnit = parser.parse(productionFileInputStream)
.getResult().get();
}
initializeSmells();
for (AbstractSmell smell : testSmells) {
for (final AbstractSmell smell : this.testSmells) {
try {
smell.runAnalysis(testFileCompilationUnit, productionFileCompilationUnit,testFile.getTestFileNameWithoutExtension(),testFile.getProductionFileNameWithoutExtension());
smell.runAnalysis(testFileCompilationUnit,
productionFileCompilationUnit,
testFile.getTestFileNameWithoutExtension(),
testFile.getProductionFileNameWithoutExtension());
} catch (FileNotFoundException e) {
testFile.addSmell(null);
continue;
}
testFile.addSmell(smell);
}
return testFile;
}
}
......@@ -3,50 +3,69 @@ package testsmell;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.MethodDeclaration;
/**
* Class used to validate certain method properties.
*/
public class Util {
public static boolean isValidTestMethod(MethodDeclaration n) {
/**
* Checks whether the method declaration is a valid method.
* @param n The method declaration
* @return {@code true} if the method is valid, {@code false} otherwise
*/
public static boolean isValidTestMethod(final MethodDeclaration n) {
boolean valid = false;
if (!n.getAnnotationByName("Ignore").isPresent()) {
//only analyze methods that either have a @test annotation (Junit 4) or the method name starts with 'test'
if (n.getAnnotationByName("Test").isPresent() || n.getNameAsString().toLowerCase().startsWith("test")) {
//must be a public method
if (n.getAnnotationByName("Test").isPresent()
|| n.getNameAsString().toLowerCase().startsWith("test")) {
if (n.getModifiers().contains(Modifier.publicModifier())) {
valid = true;
}
}
}
return valid;
}
public static boolean isValidSetupMethod(MethodDeclaration n) {
/**
* Checks whether the setup method is valid.
* @param n The method declaration
* @return {@code true} if the method is valid, {@code false} otherwise
*/
public static boolean isValidSetupMethod(final MethodDeclaration n) {
boolean valid = false;
if (!n.getAnnotationByName("Ignore").isPresent()) {
//only analyze methods that either have a @Before annotation (Junit 4) or the method name is 'setUp'
if (n.getAnnotationByName("Before").isPresent() || n.getNameAsString().equals("setUp")) {
//must be a public method
if (n.getModifiers().contains(Modifier.publicModifier())) {
valid = true;
}
if (n.getAnnotationByName("Before").isPresent()
|| n.getNameAsString().equals("setUp")
|| n.getAnnotationByName("BeforeEach").isPresent()) {
valid = true;
}
}
return valid;
}
public static boolean isInt(String s)
/**
* Checks whether a string is an integer.
* @param s The string
* @return {@code true} if the string is an integer, {@code false} otherwise
*/
public static boolean isInt(final String s)
{
try
{ int i = Integer.parseInt(s); return true; }
catch(NumberFormatException er)
{ return false; }
try {
int i = Integer.parseInt(s);
return true;
} catch(NumberFormatException er) {
return false;
}
}
public static boolean isNumber(String str) {
/**
* Checks whether a string is a double.
* @param str The string
* @return {@code true} if the string is a double, {@code false} otherwise
*/
public static boolean isNumber(final String str) {
try {
double v = Double.parseDouble(str);
return true;
......
/**
* The support classes for the analyzers.
*/
@ParametersAreNonnullByDefault
package testsmell;
import javax.annotation.ParametersAreNonnullByDefault;
......@@ -4,6 +4,8 @@ import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import javax.annotation.Nullable;
import testsmell.AbstractSmell;