From f776d5e15ddb9d0aee08a8597e65e3ca3e2d079b Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 26 Nov 2020 13:11:36 +0100 Subject: [PATCH 01/19] implemented the detection tresholds of Spadini at al --- .../java/testsmell/DetectionThresholds.java | 18 ++++++++++++++++++ .../testsmell/smell/AssertionRoulette.java | 7 ++----- .../testsmell/smell/ConditionalTestLogic.java | 12 +++++++----- src/main/java/testsmell/smell/EagerTest.java | 2 +- .../java/testsmell/smell/MagicNumberTest.java | 7 ++----- .../java/testsmell/smell/MysteryGuest.java | 7 ++----- .../java/testsmell/smell/ResourceOptimism.java | 14 +++++++------- src/main/java/testsmell/smell/SleepyTest.java | 7 ++----- src/main/java/testsmell/smell/VerboseTest.java | 7 ++----- 9 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 src/main/java/testsmell/DetectionThresholds.java diff --git a/src/main/java/testsmell/DetectionThresholds.java b/src/main/java/testsmell/DetectionThresholds.java new file mode 100644 index 0000000..db1aefd --- /dev/null +++ b/src/main/java/testsmell/DetectionThresholds.java @@ -0,0 +1,18 @@ +package testsmell; + +/** + * There are the thresholds for the test smells detection proposed by Spadini et.al. in + * the paper _Investigating severity thresholds for test smells_. + */ +public class DetectionThresholds { + + public static int EAGER_TEST = 4; + public static int ASSERTION_ROULETTE = 3; + public static int VERBOSE_TEST = 13; + public static int CONDITIONAL_TEST_LOGIC = 0; + public static int MAGIC_NUMBER_TEST = 0; + public static int GENERAL_FIXTURE = 0; + public static int MYSTERY_GUEST = 0; + public static int RESOURCE_OPTIMISM = 0; + public static int SLEEPY_TEST = 0; +} diff --git a/src/main/java/testsmell/smell/AssertionRoulette.java b/src/main/java/testsmell/smell/AssertionRoulette.java index ff196db..eede4d4 100644 --- a/src/main/java/testsmell/smell/AssertionRoulette.java +++ b/src/main/java/testsmell/smell/AssertionRoulette.java @@ -4,10 +4,7 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -87,7 +84,7 @@ public void visit(MethodDeclaration n, Void arg) { // if there is only 1 assert statement in the method, then a explanation message is not needed if (assertCount == 1) testMethod.setHasSmell(false); - else if (assertNoMessageCount >= 1) //if there is more than one assert statement, then all the asserts need to have an explanation message + else if (assertNoMessageCount >= DetectionThresholds.ASSERTION_ROULETTE) //if there is more than one assert statement, then all the asserts need to have an explanation message testMethod.setHasSmell(true); testMethod.addDataItem("AssertCount", String.valueOf(assertNoMessageCount)); diff --git a/src/main/java/testsmell/smell/ConditionalTestLogic.java b/src/main/java/testsmell/smell/ConditionalTestLogic.java index 1279ac5..abc9c81 100644 --- a/src/main/java/testsmell/smell/ConditionalTestLogic.java +++ b/src/main/java/testsmell/smell/ConditionalTestLogic.java @@ -5,10 +5,7 @@ import com.github.javaparser.ast.expr.ConditionalExpr; import com.github.javaparser.ast.stmt.*; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -73,7 +70,12 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(conditionCount > 0 | ifCount > 0 | switchCount > 0 | foreachCount > 0 | forCount > 0 | whileCount > 0); + testMethod.setHasSmell(conditionCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | + ifCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | + switchCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | + foreachCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | + forCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | + whileCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC); testMethod.addDataItem("ConditionCount", String.valueOf(conditionCount)); testMethod.addDataItem("IfCount", String.valueOf(ifCount)); diff --git a/src/main/java/testsmell/smell/EagerTest.java b/src/main/java/testsmell/smell/EagerTest.java index 7412616..d377096 100644 --- a/src/main/java/testsmell/smell/EagerTest.java +++ b/src/main/java/testsmell/smell/EagerTest.java @@ -123,7 +123,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(eagerCount > 1); //the method has a smell if there is more than 1 call to production methods + testMethod.setHasSmell(eagerCount > DetectionThresholds.EAGER_TEST); //the method has a smell if there is more than 1 call to production methods smellyElementList.add(testMethod); //reset values for next method diff --git a/src/main/java/testsmell/smell/MagicNumberTest.java b/src/main/java/testsmell/smell/MagicNumberTest.java index 4643795..d2b6d39 100644 --- a/src/main/java/testsmell/smell/MagicNumberTest.java +++ b/src/main/java/testsmell/smell/MagicNumberTest.java @@ -6,10 +6,7 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -71,7 +68,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(magicCount >= 1); + testMethod.setHasSmell(magicCount >= DetectionThresholds.MAGIC_NUMBER_TEST); testMethod.addDataItem("MagicNumberCount", String.valueOf(magicCount)); smellyElementList.add(testMethod); diff --git a/src/main/java/testsmell/smell/MysteryGuest.java b/src/main/java/testsmell/smell/MysteryGuest.java index 6aa1dc8..ec309c2 100644 --- a/src/main/java/testsmell/smell/MysteryGuest.java +++ b/src/main/java/testsmell/smell/MysteryGuest.java @@ -5,10 +5,7 @@ import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -118,7 +115,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(mysteryCount > 0); + testMethod.setHasSmell(mysteryCount > DetectionThresholds.MYSTERY_GUEST); testMethod.addDataItem("MysteryCount", String.valueOf(mysteryCount)); smellyElementList.add(testMethod); diff --git a/src/main/java/testsmell/smell/ResourceOptimism.java b/src/main/java/testsmell/smell/ResourceOptimism.java index 26e5efb..827feef 100644 --- a/src/main/java/testsmell/smell/ResourceOptimism.java +++ b/src/main/java/testsmell/smell/ResourceOptimism.java @@ -4,12 +4,12 @@ import com.github.javaparser.ast.body.FieldDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.VariableDeclarator; -import com.github.javaparser.ast.expr.*; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ObjectCreationExpr; +import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -76,7 +76,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(methodVariables.size() >= 1 || hasSmell==true); + testMethod.setHasSmell(methodVariables.size() > DetectionThresholds.RESOURCE_OPTIMISM || hasSmell == true); testMethod.addDataItem("ResourceOptimismCount", String.valueOf(resourceOptimismCount)); smellyElementList.add(testMethod); @@ -150,7 +150,7 @@ public void visit(MethodCallExpr n, Void arg) { n.getNameAsString().equals("isFile") || n.getNameAsString().equals("notExists")) { if (n.getScope().isPresent()) { - if(n.getScope().get() instanceof NameExpr) { + if (n.getScope().get() instanceof NameExpr) { if (methodVariables.contains(((NameExpr) n.getScope().get()).getNameAsString())) { methodVariables.remove(((NameExpr) n.getScope().get()).getNameAsString()); } diff --git a/src/main/java/testsmell/smell/SleepyTest.java b/src/main/java/testsmell/smell/SleepyTest.java index bd397b3..d5c2c86 100644 --- a/src/main/java/testsmell/smell/SleepyTest.java +++ b/src/main/java/testsmell/smell/SleepyTest.java @@ -5,10 +5,7 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -74,7 +71,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setHasSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(sleepCount >= 1); + testMethod.setHasSmell(sleepCount > DetectionThresholds.SLEEPY_TEST); testMethod.addDataItem("ThreadSleepCount", String.valueOf(sleepCount)); smellyElementList.add(testMethod); diff --git a/src/main/java/testsmell/smell/VerboseTest.java b/src/main/java/testsmell/smell/VerboseTest.java index 28346a9..c847480 100644 --- a/src/main/java/testsmell/smell/VerboseTest.java +++ b/src/main/java/testsmell/smell/VerboseTest.java @@ -3,10 +3,7 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.AbstractSmell; -import testsmell.SmellyElement; -import testsmell.TestMethod; -import testsmell.Util; +import testsmell.*; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -80,7 +77,7 @@ public void visit(MethodDeclaration n, Void arg) { } } } - testMethod.setHasSmell(verboseCount >= 1); + testMethod.setHasSmell(verboseCount >= DetectionThresholds.VERBOSE_TEST); testMethod.addDataItem("VerboseCount", String.valueOf(verboseCount)); smellyElementList.add(testMethod); From 5fa48e11155ddd84e4cc3ea03c9260a2a6239162 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 26 Nov 2020 18:57:11 +0100 Subject: [PATCH 02/19] implemented the changes needed to get the absolute number of smelly units per eager tests and assertion roulette (with the thresholds of Davide) --- src/main/java/Main.java | 20 ++++----- .../testsmell/MethodGranularitySmell.java | 9 ++++ src/main/java/testsmell/TestFile.java | 45 +++++++++++++------ .../java/testsmell/TestSmellDetector.java | 26 ++++++++--- .../testsmell/smell/AssertionRoulette.java | 7 ++- src/main/java/testsmell/smell/EagerTest.java | 8 +++- 6 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 src/main/java/testsmell/MethodGranularitySmell.java diff --git a/src/main/java/Main.java b/src/main/java/Main.java index 94f6786..aacfe41 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -2,8 +2,6 @@ import testsmell.ResultsWriter; import testsmell.TestFile; import testsmell.TestSmellDetector; -import testsmell.smell.AssertionRoulette; -import testsmell.smell.EagerTest; import java.io.BufferedReader; import java.io.File; @@ -21,9 +19,9 @@ public static void main(String[] args) throws IOException { System.out.println("Please provide the file containing the paths to the collection of test files"); return; } - if(!args[0].isEmpty()){ + if (!args[0].isEmpty()) { File inputFile = new File(args[0]); - if(!inputFile.exists() || inputFile.isDirectory()) { + if (!inputFile.exists() || inputFile.isDirectory()) { System.out.println("Please provide a valid file containing the paths to the collection of test files"); return; } @@ -45,10 +43,9 @@ public static void main(String[] args) throws IOException { lineItem = str.split(","); //check if the test file has an associated production file - if(lineItem.length ==2){ + if (lineItem.length == 2) { testFile = new TestFile(lineItem[0], lineItem[1], ""); - } - else{ + } else { testFile = new TestFile(lineItem[0], lineItem[1], lineItem[2]); } @@ -69,6 +66,7 @@ public static void main(String[] args) throws IOException { columnNames.add(3, "ProductionFilePath"); columnNames.add(4, "RelativeTestFilePath"); columnNames.add(5, "RelativeProductionFilePath"); + columnNames.add(6, "NumberOfMethods"); resultsWriter.writeColumnName(columnNames); @@ -80,8 +78,8 @@ public static void main(String[] args) throws IOException { Date date; for (TestFile file : testFiles) { date = new Date(); - System.out.println(dateFormat.format(date) + " Processing: "+file.getTestFilePath()); - System.out.println("Processing: "+file.getTestFilePath()); + System.out.println(dateFormat.format(date) + " Processing: " + file.getTestFilePath()); + System.out.println("Processing: " + file.getTestFilePath()); //detect smells tempFile = testSmellDetector.detectSmells(file); @@ -94,11 +92,11 @@ public static void main(String[] args) throws IOException { columnValues.add(file.getProductionFilePath()); columnValues.add(file.getRelativeTestFilePath()); columnValues.add(file.getRelativeProductionFilePath()); + columnValues.add(String.valueOf(file.getNumberOfTestMethods())); for (AbstractSmell smell : tempFile.getTestSmells()) { try { columnValues.add(String.valueOf(smell.getHasSmell())); - } - catch (NullPointerException e){ + } catch (NullPointerException e) { columnValues.add(""); } } diff --git a/src/main/java/testsmell/MethodGranularitySmell.java b/src/main/java/testsmell/MethodGranularitySmell.java new file mode 100644 index 0000000..d5c6cd4 --- /dev/null +++ b/src/main/java/testsmell/MethodGranularitySmell.java @@ -0,0 +1,9 @@ +package testsmell; + +public abstract class MethodGranularitySmell extends AbstractSmell { + + /** + * Returns in output the number of test cases in a test suite (a file) that suffer of a given smell + */ + public abstract int getNumberOfSmellyTests(); +} diff --git a/src/main/java/testsmell/TestFile.java b/src/main/java/testsmell/TestFile.java index 333ba84..9404bc0 100644 --- a/src/main/java/testsmell/TestFile.java +++ b/src/main/java/testsmell/TestFile.java @@ -9,6 +9,7 @@ public class TestFile { private String app, testFilePath, productionFilePath; private List testSmells; + private int numberOfTestMethods = 0; public String getApp() { return app; @@ -46,37 +47,38 @@ public void addSmell(AbstractSmell smell) { * Returns the "N.I.Y", Not Implemented Yet string * todo: not implemented in any way yet */ - public String getTagName(){ + public String getTagName() { return "N.I.Y"; } - public String getTestFileName(){ + public String getTestFileName() { int lastIndex = testFilePath.lastIndexOf(File.separator); - return testFilePath.substring(lastIndex+1); + return testFilePath.substring(lastIndex + 1); } - public String getTestFileNameWithoutExtension(){ + public String getTestFileNameWithoutExtension() { int lastIndex = getTestFileName().lastIndexOf("."); - return getTestFileName().substring(0,lastIndex); + return getTestFileName().substring(0, lastIndex); } - public String getProductionFileNameWithoutExtension(){ + public String getProductionFileNameWithoutExtension() { int lastIndex = getProductionFileName().lastIndexOf("."); - if(lastIndex==-1) + if (lastIndex == -1) return ""; - return getProductionFileName().substring(0,lastIndex); + return getProductionFileName().substring(0, lastIndex); } - public String getProductionFileName(){ + public String getProductionFileName() { int lastIndex = productionFilePath.lastIndexOf(File.separator); - if(lastIndex==-1) + if (lastIndex == -1) return ""; - return productionFilePath.substring(lastIndex+1); + return productionFilePath.substring(lastIndex + 1); } /** * Returns the path of the test file relative to the folder with the name of the project. * If the project directory has a different name, returns an empty string. + * * @return the relative test file path */ public String getRelativeTestFilePath() { @@ -84,7 +86,7 @@ public String getRelativeTestFilePath() { int projectNameIndex = testFilePath.lastIndexOf(app); if (projectNameIndex == -1) return ""; - return testFilePath.substring(projectNameIndex+app.length()+File.separator.length()); + return testFilePath.substring(projectNameIndex + app.length() + File.separator.length()); } else return ""; } @@ -92,16 +94,31 @@ public String getRelativeTestFilePath() { /** * Returns the path of the production file relative to the folder with the name of the project. * If the project directory has a different name, returns an empty string. - * @return the relative production file path * + * @return the relative production file path */ public String getRelativeProductionFilePath() { if (!StringUtils.isEmpty(productionFilePath)) { int projectNameIndex = productionFilePath.lastIndexOf(app); if (projectNameIndex == -1) return ""; - return productionFilePath.substring(projectNameIndex+app.length()+File.separator.length()); + return productionFilePath.substring(projectNameIndex + app.length() + File.separator.length()); } else return ""; } + + /** + * Returns the number of test methods in a test suite + */ + public int getNumberOfTestMethods() { + return numberOfTestMethods; + } + + /** + * Sets the number of test methods in a test suite + * @param numberOfTestMethods + */ + public void setNumberOfTestMethods(int numberOfTestMethods) { + this.numberOfTestMethods = numberOfTestMethods; + } } \ No newline at end of file diff --git a/src/main/java/testsmell/TestSmellDetector.java b/src/main/java/testsmell/TestSmellDetector.java index a76fad3..1a2c227 100644 --- a/src/main/java/testsmell/TestSmellDetector.java +++ b/src/main/java/testsmell/TestSmellDetector.java @@ -2,6 +2,7 @@ import com.github.javaparser.JavaParser; import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.TypeDeclaration; import org.apache.commons.lang3.StringUtils; import testsmell.smell.*; @@ -20,12 +21,20 @@ public class TestSmellDetector { * Instantiates the various test smell analyzer classes and loads the objects into an List */ public TestSmellDetector() { - initializeSmells(); +// initializeSmells(); + initializeGranularSmells(); + } + + public TestSmellDetector(boolean initialize) { } - public TestSmellDetector(boolean initialize) {} + private void initializeGranularSmells() { + testSmells = new ArrayList<>(); + testSmells.add(new EagerTest()); + testSmells.add(new AssertionRoulette()); + } - private void initializeSmells(){ + private void initializeSmells() { testSmells = new ArrayList<>(); testSmells.add(new AssertionRoulette()); testSmells.add(new ConditionalTestLogic()); @@ -76,15 +85,18 @@ public List getTestSmellNames() { * Loads the java source code file into an AST and then analyzes it for the existence of the different types of test smells */ public TestFile detectSmells(TestFile testFile) throws IOException { - CompilationUnit testFileCompilationUnit=null, productionFileCompilationUnit=null; + CompilationUnit testFileCompilationUnit = null; + CompilationUnit productionFileCompilationUnit = null; FileInputStream testFileInputStream, productionFileInputStream; - if(!StringUtils.isEmpty(testFile.getTestFilePath())) { + if (!StringUtils.isEmpty(testFile.getTestFilePath())) { testFileInputStream = new FileInputStream(testFile.getTestFilePath()); testFileCompilationUnit = JavaParser.parse(testFileInputStream); + TypeDeclaration typeDeclaration = testFileCompilationUnit.getTypes().get(0); + testFile.setNumberOfTestMethods(typeDeclaration.getMethods().size()); } - if(!StringUtils.isEmpty(testFile.getProductionFilePath())){ + if (!StringUtils.isEmpty(testFile.getProductionFilePath())) { productionFileInputStream = new FileInputStream(testFile.getProductionFilePath()); productionFileCompilationUnit = JavaParser.parse(productionFileInputStream); } @@ -92,7 +104,7 @@ public TestFile detectSmells(TestFile testFile) throws IOException { // initializeSmells(); for (AbstractSmell smell : 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; diff --git a/src/main/java/testsmell/smell/AssertionRoulette.java b/src/main/java/testsmell/smell/AssertionRoulette.java index eede4d4..4e55bbf 100644 --- a/src/main/java/testsmell/smell/AssertionRoulette.java +++ b/src/main/java/testsmell/smell/AssertionRoulette.java @@ -15,7 +15,7 @@ * If one of the assertions fails, you do not know which one it is. * A. van Deursen, L. Moonen, A. Bergh, G. Kok, “Refactoring Test Code”, Technical Report, CWI, 2001. */ -public class AssertionRoulette extends AbstractSmell { +public class AssertionRoulette extends MethodGranularitySmell { private List smellyElementList; private int assertionsCount = 0; @@ -63,6 +63,11 @@ public List getSmellyElements() { return smellyElementList; } + @Override + public int getNumberOfSmellyTests() { + return smellyElementList.size(); + } + private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; diff --git a/src/main/java/testsmell/smell/EagerTest.java b/src/main/java/testsmell/smell/EagerTest.java index d377096..7f713d5 100644 --- a/src/main/java/testsmell/smell/EagerTest.java +++ b/src/main/java/testsmell/smell/EagerTest.java @@ -8,7 +8,6 @@ import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; @@ -17,7 +16,7 @@ import java.util.List; import java.util.Objects; -public class EagerTest extends AbstractSmell { +public class EagerTest extends MethodGranularitySmell { private static final String TEST_FILE = "Test"; private static final String PRODUCTION_FILE = "Production"; @@ -78,6 +77,11 @@ public int getEagerCount() { return eagerCount; } + @Override + public int getNumberOfSmellyTests() { + return smellyElementList.size(); + } + /** * Visitor class */ From 822a79353c7b37f6745cb9e4b05e7758d7637d98 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 27 Nov 2020 15:33:29 +0100 Subject: [PATCH 03/19] modified the main to get the number of the smelly instances --- src/main/java/Main.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/Main.java b/src/main/java/Main.java index aacfe41..dd52062 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,7 +1,4 @@ -import testsmell.AbstractSmell; -import testsmell.ResultsWriter; -import testsmell.TestFile; -import testsmell.TestSmellDetector; +import testsmell.*; import java.io.BufferedReader; import java.io.File; @@ -95,7 +92,7 @@ public static void main(String[] args) throws IOException { columnValues.add(String.valueOf(file.getNumberOfTestMethods())); for (AbstractSmell smell : tempFile.getTestSmells()) { try { - columnValues.add(String.valueOf(smell.getHasSmell())); + columnValues.add(String.valueOf(((MethodGranularitySmell)smell).getNumberOfSmellyTests())); } catch (NullPointerException e) { columnValues.add(""); } From 6226432660aafbf849d56a3d11e2c6b64182c762 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 1 Dec 2020 16:16:46 +0100 Subject: [PATCH 04/19] the boolen value for the small is not expressed as an int to guarantee better compatibility with the granularity --- .../testsmell/MethodGranularitySmell.java | 9 ------- .../testsmell/smell/AssertionRoulette.java | 24 +++++++------------ .../testsmell/smell/ConditionalTestLogic.java | 19 ++++++--------- .../smell/ConstructorInitialization.java | 18 ++++++-------- .../java/testsmell/smell/DefaultTest.java | 18 ++++---------- .../java/testsmell/smell/DependentTest.java | 17 ++++--------- .../java/testsmell/smell/DuplicateAssert.java | 20 ++++++---------- src/main/java/testsmell/smell/EagerTest.java | 9 ++++--- src/main/java/testsmell/smell/EmptyTest.java | 15 +++--------- .../smell/ExceptionCatchingThrowing.java | 16 +++---------- .../java/testsmell/smell/GeneralFixture.java | 14 +++-------- .../java/testsmell/smell/IgnoredTest.java | 16 +++---------- src/main/java/testsmell/smell/LazyTest.java | 14 +++-------- .../java/testsmell/smell/MagicNumberTest.java | 13 +++------- .../java/testsmell/smell/MysteryGuest.java | 14 +++-------- .../java/testsmell/smell/PrintStatement.java | 16 +++---------- .../testsmell/smell/RedundantAssertion.java | 16 +++---------- .../testsmell/smell/ResourceOptimism.java | 15 +++--------- .../testsmell/smell/SensitiveEquality.java | 15 +++--------- src/main/java/testsmell/smell/SleepyTest.java | 16 +++---------- .../java/testsmell/smell/UnknownTest.java | 15 +++--------- .../java/testsmell/smell/VerboseTest.java | 16 +++---------- 22 files changed, 86 insertions(+), 259 deletions(-) delete mode 100644 src/main/java/testsmell/MethodGranularitySmell.java diff --git a/src/main/java/testsmell/MethodGranularitySmell.java b/src/main/java/testsmell/MethodGranularitySmell.java deleted file mode 100644 index d5c6cd4..0000000 --- a/src/main/java/testsmell/MethodGranularitySmell.java +++ /dev/null @@ -1,9 +0,0 @@ -package testsmell; - -public abstract class MethodGranularitySmell extends AbstractSmell { - - /** - * Returns in output the number of test cases in a test suite (a file) that suffer of a given smell - */ - public abstract int getNumberOfSmellyTests(); -} diff --git a/src/main/java/testsmell/smell/AssertionRoulette.java b/src/main/java/testsmell/smell/AssertionRoulette.java index 4e55bbf..45b5383 100644 --- a/src/main/java/testsmell/smell/AssertionRoulette.java +++ b/src/main/java/testsmell/smell/AssertionRoulette.java @@ -4,10 +4,13 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /** @@ -15,13 +18,12 @@ * If one of the assertions fails, you do not know which one it is. * A. van Deursen, L. Moonen, A. Bergh, G. Kok, “Refactoring Test Code”, Technical Report, CWI, 2001. */ -public class AssertionRoulette extends MethodGranularitySmell { +public class AssertionRoulette extends AbstractSmell { - private List smellyElementList; private int assertionsCount = 0; - public AssertionRoulette() { - smellyElementList = new ArrayList<>(); + public AssertionRoulette(Thresholds thresholds) { + super(thresholds); } /** @@ -32,14 +34,6 @@ public String getSmellName() { return "Assertion Roulette"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods for multiple assert statements without an explanation/message */ @@ -89,7 +83,7 @@ public void visit(MethodDeclaration n, Void arg) { // if there is only 1 assert statement in the method, then a explanation message is not needed if (assertCount == 1) testMethod.setHasSmell(false); - else if (assertNoMessageCount >= DetectionThresholds.ASSERTION_ROULETTE) //if there is more than one assert statement, then all the asserts need to have an explanation message + else if (assertNoMessageCount >= thresholds.getAssertionRoulette()) //if there is more than one assert statement, then all the asserts need to have an explanation message testMethod.setHasSmell(true); testMethod.addDataItem("AssertCount", String.valueOf(assertNoMessageCount)); diff --git a/src/main/java/testsmell/smell/ConditionalTestLogic.java b/src/main/java/testsmell/smell/ConditionalTestLogic.java index abc9c81..645a37d 100644 --- a/src/main/java/testsmell/smell/ConditionalTestLogic.java +++ b/src/main/java/testsmell/smell/ConditionalTestLogic.java @@ -6,19 +6,18 @@ import com.github.javaparser.ast.stmt.*; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* This class check a test method for the existence of loops and conditional statements in the methods body */ public class ConditionalTestLogic extends AbstractSmell { - private List smellyElementList; - public ConditionalTestLogic() { - smellyElementList = new ArrayList<>(); + public ConditionalTestLogic(Thresholds thresholds) { + super(thresholds); } /** @@ -29,14 +28,6 @@ public String getSmellName() { return "Conditional Test Logic"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that use conditional statements */ @@ -55,6 +46,10 @@ public List getSmellyElements() { return smellyElementList; } + @Override + public int getNumberOfSmellyTests() { + return 0; + } private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; diff --git a/src/main/java/testsmell/smell/ConstructorInitialization.java b/src/main/java/testsmell/smell/ConstructorInitialization.java index c964fc9..8b13917 100644 --- a/src/main/java/testsmell/smell/ConstructorInitialization.java +++ b/src/main/java/testsmell/smell/ConstructorInitialization.java @@ -8,6 +8,7 @@ import testsmell.AbstractSmell; import testsmell.SmellyElement; import testsmell.TestClass; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -20,11 +21,10 @@ This class checks if the code file contains a Constructor. Ideally, the test sui */ public class ConstructorInitialization extends AbstractSmell { - private List smellyElementList; private String testFileName; - public ConstructorInitialization() { - smellyElementList = new ArrayList<>(); + public ConstructorInitialization(Thresholds thresholds) { + super(thresholds); } /** @@ -35,14 +35,6 @@ public String getSmellName() { return "Constructor Initialization"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for Constructor Initialization smell */ @@ -62,6 +54,10 @@ public List getSmellyElements() { return smellyElementList; } + @Override + public int getNumberOfSmellyTests() { + return 0; + } private class ClassVisitor extends VoidVisitorAdapter { TestClass testClass; diff --git a/src/main/java/testsmell/smell/DefaultTest.java b/src/main/java/testsmell/smell/DefaultTest.java index e630946..cea21e0 100644 --- a/src/main/java/testsmell/smell/DefaultTest.java +++ b/src/main/java/testsmell/smell/DefaultTest.java @@ -6,9 +6,9 @@ import testsmell.AbstractSmell; import testsmell.SmellyElement; import testsmell.TestClass; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -17,10 +17,8 @@ */ public class DefaultTest extends AbstractSmell { - private List smellyElementList; - - public DefaultTest() { - smellyElementList = new ArrayList<>(); + public DefaultTest(Thresholds thresholds) { + super(thresholds); } /** @@ -31,16 +29,8 @@ public String getSmellName() { return "Default Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - @Override - public void runAnalysis(CompilationUnit testFileCompilationUnit,CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException { + public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException { DefaultTest.ClassVisitor classVisitor; classVisitor = new DefaultTest.ClassVisitor(); classVisitor.visit(testFileCompilationUnit, null); diff --git a/src/main/java/testsmell/smell/DependentTest.java b/src/main/java/testsmell/smell/DependentTest.java index 38fb2bd..9cd202f 100644 --- a/src/main/java/testsmell/smell/DependentTest.java +++ b/src/main/java/testsmell/smell/DependentTest.java @@ -7,6 +7,7 @@ import testsmell.AbstractSmell; import testsmell.SmellyElement; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -14,12 +15,10 @@ public class DependentTest extends AbstractSmell { - private List smellyElementList; private List testMethods; - - public DependentTest() { - smellyElementList = new ArrayList<>(); + public DependentTest(Thresholds thresholds) { + super(thresholds); testMethods = new ArrayList<>(); } @@ -31,14 +30,6 @@ public String getSmellName() { return "Dependent Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that call other test methods */ @@ -49,7 +40,7 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); for (TestMethod testMethod : testMethods) { - if (testMethod.getCalledMethods().stream().anyMatch(x -> x.getName().equals(testMethods.stream().map(z -> z.getMethodDeclaration().getNameAsString())))){ + if (testMethod.getCalledMethods().stream().anyMatch(x -> x.getName().equals(testMethods.stream().map(z -> z.getMethodDeclaration().getNameAsString())))) { smellyElementList.add(new testsmell.TestMethod(testMethod.getMethodDeclaration().getNameAsString())); } } diff --git a/src/main/java/testsmell/smell/DuplicateAssert.java b/src/main/java/testsmell/smell/DuplicateAssert.java index fcc6037..ff00785 100644 --- a/src/main/java/testsmell/smell/DuplicateAssert.java +++ b/src/main/java/testsmell/smell/DuplicateAssert.java @@ -8,16 +8,18 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; public class DuplicateAssert extends AbstractSmell { - private List smellyElementList; - - public DuplicateAssert() { - smellyElementList = new ArrayList<>(); + public DuplicateAssert(Thresholds thresholds) { + super(thresholds); } /** @@ -28,14 +30,6 @@ public String getSmellName() { return "Duplicate Assert"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that have multiple assert statements with the same explanation message */ diff --git a/src/main/java/testsmell/smell/EagerTest.java b/src/main/java/testsmell/smell/EagerTest.java index 7f713d5..8c07981 100644 --- a/src/main/java/testsmell/smell/EagerTest.java +++ b/src/main/java/testsmell/smell/EagerTest.java @@ -10,13 +10,14 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.Objects; -public class EagerTest extends MethodGranularitySmell { +public class EagerTest extends AbstractSmell { private static final String TEST_FILE = "Test"; private static final String PRODUCTION_FILE = "Production"; @@ -25,7 +26,8 @@ public class EagerTest extends MethodGranularitySmell { private List productionMethods; private int eagerCount; - public EagerTest() { + public EagerTest(Thresholds thresholds) { + super(thresholds); productionMethods = new ArrayList<>(); smellyElementList = new ArrayList<>(); } @@ -40,9 +42,10 @@ public String getSmellName() { /** * Returns true if any of the elements has a smell + * @return */ @Override - public boolean getHasSmell() { + public int getHasSmell() { return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; } diff --git a/src/main/java/testsmell/smell/EmptyTest.java b/src/main/java/testsmell/smell/EmptyTest.java index 4bc18ec..7d31cbd 100644 --- a/src/main/java/testsmell/smell/EmptyTest.java +++ b/src/main/java/testsmell/smell/EmptyTest.java @@ -7,6 +7,7 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -18,10 +19,8 @@ */ public class EmptyTest extends AbstractSmell { - private List smellyElementList; - - public EmptyTest() { - smellyElementList = new ArrayList<>(); + public EmptyTest(Thresholds thresholds) { + super(thresholds); } /** @@ -32,14 +31,6 @@ public String getSmellName() { return "EmptyTest"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that are empty (i.e. no method body) */ diff --git a/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java b/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java index 50cc5e0..61adb4e 100644 --- a/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java +++ b/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java @@ -9,9 +9,9 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -20,10 +20,8 @@ */ public class ExceptionCatchingThrowing extends AbstractSmell { - private List smellyElementList; - - public ExceptionCatchingThrowing() { - smellyElementList = new ArrayList<>(); + public ExceptionCatchingThrowing(Thresholds thresholds) { + super(thresholds); } /** @@ -34,14 +32,6 @@ public String getSmellName() { return "Exception Catching Throwing"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that have exception handling */ diff --git a/src/main/java/testsmell/smell/GeneralFixture.java b/src/main/java/testsmell/smell/GeneralFixture.java index 9d5ddf5..6d57f5e 100644 --- a/src/main/java/testsmell/smell/GeneralFixture.java +++ b/src/main/java/testsmell/smell/GeneralFixture.java @@ -15,20 +15,20 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.*; public class GeneralFixture extends AbstractSmell { - private List smellyElementList; List methodList; MethodDeclaration setupMethod; List fieldList; List setupFields; - public GeneralFixture() { - smellyElementList = new ArrayList<>(); + public GeneralFixture(Thresholds thresholds) { + super(thresholds); methodList = new ArrayList<>(); fieldList = new ArrayList<>(); setupFields = new ArrayList<>(); @@ -42,14 +42,6 @@ public String getSmellName() { return "General Fixture"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - @Override public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException { GeneralFixture.ClassVisitor classVisitor; diff --git a/src/main/java/testsmell/smell/IgnoredTest.java b/src/main/java/testsmell/smell/IgnoredTest.java index 275bdbd..17d24bb 100644 --- a/src/main/java/testsmell/smell/IgnoredTest.java +++ b/src/main/java/testsmell/smell/IgnoredTest.java @@ -9,17 +9,15 @@ import testsmell.SmellyElement; import testsmell.TestClass; import testsmell.TestMethod; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; public class IgnoredTest extends AbstractSmell { - private List smellyElementList; - - public IgnoredTest() { - smellyElementList = new ArrayList<>(); + public IgnoredTest(Thresholds thresholds) { + super(thresholds); } /** @@ -30,14 +28,6 @@ public String getSmellName() { return "IgnoredTest"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that contain Ignored test methods */ diff --git a/src/main/java/testsmell/smell/LazyTest.java b/src/main/java/testsmell/smell/LazyTest.java index 55efa39..2c3bd0b 100644 --- a/src/main/java/testsmell/smell/LazyTest.java +++ b/src/main/java/testsmell/smell/LazyTest.java @@ -8,12 +8,12 @@ import com.github.javaparser.ast.body.VariableDeclarator; import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.NameExpr; -import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -25,11 +25,11 @@ public class LazyTest extends AbstractSmell { private static final String TEST_FILE = "Test"; private static final String PRODUCTION_FILE = "Production"; private String productionClassName; - private List smellyElementList; private List calledProductionMethods; private List productionMethods; - public LazyTest() { + public LazyTest(Thresholds thresholds) { + super(thresholds); productionMethods = new ArrayList<>(); smellyElementList = new ArrayList<>(); calledProductionMethods = new ArrayList<>(); @@ -43,14 +43,6 @@ public String getSmellName() { return "Lazy Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that exhibit the 'Lazy Test' smell */ diff --git a/src/main/java/testsmell/smell/MagicNumberTest.java b/src/main/java/testsmell/smell/MagicNumberTest.java index d2b6d39..f365bdc 100644 --- a/src/main/java/testsmell/smell/MagicNumberTest.java +++ b/src/main/java/testsmell/smell/MagicNumberTest.java @@ -7,6 +7,7 @@ import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -16,8 +17,8 @@ public class MagicNumberTest extends AbstractSmell { private List smellyElementList; - public MagicNumberTest() { - smellyElementList = new ArrayList<>(); + public MagicNumberTest(Thresholds thresholds) { + super(thresholds); } /** @@ -28,14 +29,6 @@ public String getSmellName() { return "Magic Number Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that have magic numbers in as parameters in the assert methods */ diff --git a/src/main/java/testsmell/smell/MysteryGuest.java b/src/main/java/testsmell/smell/MysteryGuest.java index ec309c2..3ff7e52 100644 --- a/src/main/java/testsmell/smell/MysteryGuest.java +++ b/src/main/java/testsmell/smell/MysteryGuest.java @@ -6,6 +6,7 @@ import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -21,9 +22,8 @@ */ public class MysteryGuest extends AbstractSmell { - private List smellyElementList; - - public MysteryGuest() { + public MysteryGuest(Thresholds thresholds) { + super(thresholds); smellyElementList = new ArrayList<>(); } @@ -35,14 +35,6 @@ public String getSmellName() { return "Mystery Guest"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that use external resources */ diff --git a/src/main/java/testsmell/smell/PrintStatement.java b/src/main/java/testsmell/smell/PrintStatement.java index 3bcf3d0..707a0c2 100644 --- a/src/main/java/testsmell/smell/PrintStatement.java +++ b/src/main/java/testsmell/smell/PrintStatement.java @@ -10,9 +10,9 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -21,10 +21,8 @@ This code checks the body of each test method if System.out. print(), println(), */ public class PrintStatement extends AbstractSmell { - private List smellyElementList; - - public PrintStatement() { - smellyElementList = new ArrayList<>(); + public PrintStatement(Thresholds thresholds) { + super(thresholds); } /** @@ -35,14 +33,6 @@ public String getSmellName() { return "Print Statement"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that print output to the console */ diff --git a/src/main/java/testsmell/smell/RedundantAssertion.java b/src/main/java/testsmell/smell/RedundantAssertion.java index 47431d5..d277909 100644 --- a/src/main/java/testsmell/smell/RedundantAssertion.java +++ b/src/main/java/testsmell/smell/RedundantAssertion.java @@ -10,9 +10,9 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -20,10 +20,8 @@ */ public class RedundantAssertion extends AbstractSmell { - private List smellyElementList; - - public RedundantAssertion() { - smellyElementList = new ArrayList<>(); + public RedundantAssertion(Thresholds thresholds) { + super(thresholds); } /** @@ -34,14 +32,6 @@ public String getSmellName() { return "Redundant Assertion"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods for multiple assert statements */ diff --git a/src/main/java/testsmell/smell/ResourceOptimism.java b/src/main/java/testsmell/smell/ResourceOptimism.java index 827feef..3bb0a01 100644 --- a/src/main/java/testsmell/smell/ResourceOptimism.java +++ b/src/main/java/testsmell/smell/ResourceOptimism.java @@ -10,6 +10,7 @@ import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -17,10 +18,8 @@ public class ResourceOptimism extends AbstractSmell { - private List smellyElementList; - - public ResourceOptimism() { - smellyElementList = new ArrayList<>(); + public ResourceOptimism(Thresholds thresholds) { + super(thresholds); } /** @@ -31,14 +30,6 @@ public String getSmellName() { return "Resource Optimism"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for the 'ResourceOptimism' smell */ diff --git a/src/main/java/testsmell/smell/SensitiveEquality.java b/src/main/java/testsmell/smell/SensitiveEquality.java index 726df94..b0959f6 100644 --- a/src/main/java/testsmell/smell/SensitiveEquality.java +++ b/src/main/java/testsmell/smell/SensitiveEquality.java @@ -9,6 +9,7 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -16,10 +17,8 @@ public class SensitiveEquality extends AbstractSmell { - private List smellyElementList; - - public SensitiveEquality() { - smellyElementList = new ArrayList<>(); + public SensitiveEquality(Thresholds thresholds) { + super(thresholds); } /** @@ -30,14 +29,6 @@ public String getSmellName() { return "Sensitive Equality"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods the 'Sensitive Equality' smell */ diff --git a/src/main/java/testsmell/smell/SleepyTest.java b/src/main/java/testsmell/smell/SleepyTest.java index d5c2c86..b8b1022 100644 --- a/src/main/java/testsmell/smell/SleepyTest.java +++ b/src/main/java/testsmell/smell/SleepyTest.java @@ -6,9 +6,9 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -17,10 +17,8 @@ */ public class SleepyTest extends AbstractSmell { - private List smellyElementList; - - public SleepyTest() { - smellyElementList = new ArrayList<>(); + public SleepyTest(Thresholds thresholds) { + super(thresholds); } /** @@ -31,14 +29,6 @@ public String getSmellName() { return "Sleepy Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that use Thread.sleep() */ diff --git a/src/main/java/testsmell/smell/UnknownTest.java b/src/main/java/testsmell/smell/UnknownTest.java index 4c3d43c..6dc13fd 100644 --- a/src/main/java/testsmell/smell/UnknownTest.java +++ b/src/main/java/testsmell/smell/UnknownTest.java @@ -11,6 +11,7 @@ import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; +import thresholds.Thresholds; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -19,10 +20,8 @@ public class UnknownTest extends AbstractSmell { - private List smellyElementList; - - public UnknownTest() { - smellyElementList = new ArrayList<>(); + public UnknownTest(Thresholds thresholds) { + super(thresholds); } /** @@ -33,14 +32,6 @@ public String getSmellName() { return "Unknown Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that do not have assert statement or exceptions */ diff --git a/src/main/java/testsmell/smell/VerboseTest.java b/src/main/java/testsmell/smell/VerboseTest.java index c847480..e5bf9b1 100644 --- a/src/main/java/testsmell/smell/VerboseTest.java +++ b/src/main/java/testsmell/smell/VerboseTest.java @@ -4,9 +4,9 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.*; +import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; /* @@ -14,10 +14,8 @@ */ public class VerboseTest extends AbstractSmell { - private List smellyElementList; - - public VerboseTest() { - smellyElementList = new ArrayList<>(); + public VerboseTest(Thresholds thresholds) { + super(thresholds); } /** @@ -28,14 +26,6 @@ public String getSmellName() { return "Verbose Test"; } - /** - * Returns true if any of the elements has a smell - */ - @Override - public boolean getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods for the 'Verbose Test' smell */ From 361077695bab842a9dee73261fa92da81c8fb1f3 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 1 Dec 2020 16:17:32 +0100 Subject: [PATCH 05/19] skeleton for kotlin code; wip --- pom.xml | 81 +++++++++++++++++- src/main/java/Main.java | 11 ++- src/main/java/testsmell/AbstractSmell.java | 25 +++++- .../java/testsmell/TestSmellDetector.java | 85 ++++++++----------- src/main/kotlin/detection/Detection.kt | 45 ++++++++++ src/main/kotlin/detection/DetectionResult.kt | 15 ++++ .../kotlin/detection/TestProductionPair.kt | 8 ++ src/main/kotlin/io/InputData.kt | 10 +++ src/main/kotlin/testsmell/Runner.kt | 48 +++++++++++ src/main/kotlin/thresholds/Thresholds.kt | 52 ++++++++++++ 10 files changed, 324 insertions(+), 56 deletions(-) create mode 100644 src/main/kotlin/detection/Detection.kt create mode 100644 src/main/kotlin/detection/DetectionResult.kt create mode 100644 src/main/kotlin/detection/TestProductionPair.kt create mode 100644 src/main/kotlin/io/InputData.kt create mode 100644 src/main/kotlin/testsmell/Runner.kt create mode 100644 src/main/kotlin/thresholds/Thresholds.kt diff --git a/pom.xml b/pom.xml index 879b4e7..34aa463 100644 --- a/pom.xml +++ b/pom.xml @@ -4,20 +4,84 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + + 1.4.20 + 1.8 + + edu.rit.se.testsmells TestSmellDetector 0.1 jar + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + org.apache.maven.plugins maven-compiler-plugin + 3.5.1 1.8 1.8 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + + org.apache.maven.plugins maven-surefire-plugin @@ -86,6 +150,21 @@ 5.4.2 test + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + com.github.ajalt + clikt + 2.8.0 + + + com.github.doyaaaaaken + kotlin-csv-jvm + 0.13.0 + - + \ No newline at end of file diff --git a/src/main/java/Main.java b/src/main/java/Main.java index dd52062..eacb5b7 100644 --- a/src/main/java/Main.java +++ b/src/main/java/Main.java @@ -1,4 +1,9 @@ -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.ResultsWriter; +import testsmell.TestFile; +import testsmell.TestSmellDetector; +import thresholds.DefaultThresholds; +import thresholds.Thresholds; import java.io.BufferedReader; import java.io.File; @@ -24,7 +29,7 @@ public static void main(String[] args) throws IOException { } } - TestSmellDetector testSmellDetector = new TestSmellDetector(); + TestSmellDetector testSmellDetector = new TestSmellDetector(new DefaultThresholds()); /* Read the input file and build the TestFile objects @@ -92,7 +97,7 @@ public static void main(String[] args) throws IOException { columnValues.add(String.valueOf(file.getNumberOfTestMethods())); for (AbstractSmell smell : tempFile.getTestSmells()) { try { - columnValues.add(String.valueOf(((MethodGranularitySmell)smell).getNumberOfSmellyTests())); + columnValues.add(String.valueOf(smell.getNumberOfSmellyTests())); } catch (NullPointerException e) { columnValues.add(""); } diff --git a/src/main/java/testsmell/AbstractSmell.java b/src/main/java/testsmell/AbstractSmell.java index 26d6f9e..e0cfe0d 100644 --- a/src/main/java/testsmell/AbstractSmell.java +++ b/src/main/java/testsmell/AbstractSmell.java @@ -1,16 +1,37 @@ package testsmell; import com.github.javaparser.ast.CompilationUnit; +import thresholds.Thresholds; import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.List; public abstract class AbstractSmell { + protected Thresholds thresholds; + protected List smellyElementList; + + public AbstractSmell(Thresholds thresholds) { + this.thresholds = thresholds; + this.smellyElementList = new ArrayList<>(); + } + public abstract String getSmellName(); - public abstract boolean getHasSmell(); + /** + * Return 1 if any of the elements has a smell; 0 otherwise + */ + public int getHasSmell() { + boolean isSmelly = smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; + return isSmelly ? 1 : 0; + } - public abstract void runAnalysis(CompilationUnit testFileCompilationUnit,CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException; + public abstract void runAnalysis(CompilationUnit testFileCompilationUnit, + CompilationUnit productionFileCompilationUnit, + String testFileName, + String productionFileName) throws FileNotFoundException; public abstract List getSmellyElements(); + + public abstract int getNumberOfSmellyTests(); } diff --git a/src/main/java/testsmell/TestSmellDetector.java b/src/main/java/testsmell/TestSmellDetector.java index 1a2c227..a067e2f 100644 --- a/src/main/java/testsmell/TestSmellDetector.java +++ b/src/main/java/testsmell/TestSmellDetector.java @@ -5,6 +5,7 @@ import com.github.javaparser.ast.body.TypeDeclaration; import org.apache.commons.lang3.StringUtils; import testsmell.smell.*; +import thresholds.Thresholds; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -16,62 +17,48 @@ public class TestSmellDetector { private List testSmells; + private Thresholds thresholds; /** - * Instantiates the various test smell analyzer classes and loads the objects into an List + * Instantiates the various test smell analyzer classes and loads the objects into an list. + * Each smell analyzer is initialized with a threshold object to set the most appropriate rule for the detection + * + * @param thresholds it could be the default threshold of the ones defined by Spadini */ - public TestSmellDetector() { -// initializeSmells(); - initializeGranularSmells(); - } - - public TestSmellDetector(boolean initialize) { - } - - private void initializeGranularSmells() { - testSmells = new ArrayList<>(); - testSmells.add(new EagerTest()); - testSmells.add(new AssertionRoulette()); + public TestSmellDetector(Thresholds thresholds) { + this.thresholds = thresholds; + 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()); + testSmells.add(new AssertionRoulette(thresholds)); + testSmells.add(new ConditionalTestLogic(thresholds)); + testSmells.add(new ConstructorInitialization(thresholds)); + testSmells.add(new DefaultTest(thresholds)); + testSmells.add(new EmptyTest(thresholds)); + testSmells.add(new ExceptionCatchingThrowing(thresholds)); + testSmells.add(new GeneralFixture(thresholds)); + testSmells.add(new MysteryGuest(thresholds)); + testSmells.add(new PrintStatement(thresholds)); + testSmells.add(new RedundantAssertion(thresholds)); + testSmells.add(new SensitiveEquality(thresholds)); + testSmells.add(new VerboseTest(thresholds)); + testSmells.add(new SleepyTest(thresholds)); + testSmells.add(new EagerTest(thresholds)); + testSmells.add(new LazyTest(thresholds)); + testSmells.add(new DuplicateAssert(thresholds)); + testSmells.add(new UnknownTest(thresholds)); + testSmells.add(new IgnoredTest(thresholds)); + testSmells.add(new ResourceOptimism(thresholds)); + testSmells.add(new MagicNumberTest(thresholds)); + testSmells.add(new DependentTest(thresholds)); } public void setTestSmells(List testSmells) { this.testSmells = testSmells; } - /** - * Factory method that provides a new instance of the TestSmellDetector - * - * @return new TestSmellDetector instance - */ - public static TestSmellDetector createTestSmellDetector() { - return new TestSmellDetector(); - } - /** * Provides the names of the smells that are being checked for in the code * @@ -82,7 +69,8 @@ public List getTestSmellNames() { } /** - * 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 */ public TestFile detectSmells(TestFile testFile) throws IOException { CompilationUnit testFileCompilationUnit = null; @@ -101,20 +89,17 @@ public TestFile detectSmells(TestFile testFile) throws IOException { productionFileCompilationUnit = JavaParser.parse(productionFileInputStream); } -// initializeSmells(); for (AbstractSmell smell : 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; - } - - } diff --git a/src/main/kotlin/detection/Detection.kt b/src/main/kotlin/detection/Detection.kt new file mode 100644 index 0000000..7889ed1 --- /dev/null +++ b/src/main/kotlin/detection/Detection.kt @@ -0,0 +1,45 @@ +package detection + +import testsmell.AbstractSmell +import testsmell.TestFile +import testsmell.TestSmellDetector +import thresholds.Thresholds + +/** + * Runs the detection by exploiting the TestSmellDetector class + */ +class Detection(private val project: String, + private val pairs: List, + private val testSmellDetector: TestSmellDetector, + val threshold: Thresholds) { + + /** + * Analyze the given pairs and return a list of DetectionResult + */ + fun detectSmells(getSmellValue: (AbstractSmell) -> Int): List { + val resultList = mutableListOf() + for (pair in pairs) { + val testFile = TestFile(project, pair.testClassPath, pair.productionClassPath) + val tempFile: TestFile = testSmellDetector.detectSmells(testFile) + + val smellLists: List = testSmellDetector.testSmellNames + val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } + val outputs: List> = smellLists.flatMap { name -> + smellValues.map { name to it } + } + + val detectionResult = DetectionResult( + application = project, + testFileName = tempFile.testFileName, + testFilePath = tempFile.testFilePath, + productionFilePath = tempFile.productionFilePath, + relativeTestFilePath = tempFile.relativeTestFilePath, + relativeProductionFilePath = tempFile.relativeProductionFilePath, + numberOfTestMethods = testFile.numberOfTestMethods, + smellResult = outputs + ) + resultList.add(detectionResult) + } + return resultList + } +} \ No newline at end of file diff --git a/src/main/kotlin/detection/DetectionResult.kt b/src/main/kotlin/detection/DetectionResult.kt new file mode 100644 index 0000000..edd592a --- /dev/null +++ b/src/main/kotlin/detection/DetectionResult.kt @@ -0,0 +1,15 @@ +package detection + +/** + * The possible values we can obtain from a detection run on a given class + */ +data class DetectionResult( + val application: String, + val testFileName: String, + val testFilePath: String, + val productionFilePath: String, + val relativeTestFilePath: String, + val relativeProductionFilePath: String, + val numberOfTestMethods: Int, + val smellResult: List> +) diff --git a/src/main/kotlin/detection/TestProductionPair.kt b/src/main/kotlin/detection/TestProductionPair.kt new file mode 100644 index 0000000..9992b66 --- /dev/null +++ b/src/main/kotlin/detection/TestProductionPair.kt @@ -0,0 +1,8 @@ +package detection + +data class TestProductionPair( + val testClass: String = "", + val productionClass: String = "", + val testClassPath: String, + val productionClassPath: String +) diff --git a/src/main/kotlin/io/InputData.kt b/src/main/kotlin/io/InputData.kt new file mode 100644 index 0000000..72908d4 --- /dev/null +++ b/src/main/kotlin/io/InputData.kt @@ -0,0 +1,10 @@ +package io + +/** + * Represents the information we need in input to perform the analysis + */ +data class InputData( + val application: String, + val testPath: String, + val productionPath: String +) diff --git a/src/main/kotlin/testsmell/Runner.kt b/src/main/kotlin/testsmell/Runner.kt new file mode 100644 index 0000000..cd36ce8 --- /dev/null +++ b/src/main/kotlin/testsmell/Runner.kt @@ -0,0 +1,48 @@ +package testsmell + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.file +import com.github.doyaaaaaken.kotlincsv.dsl.csvReader +import io.InputData +import thresholds.DefaultThresholds +import thresholds.SpadiniThresholds +import thresholds.Thresholds +import java.io.File + +class DetectorRunner : CliktCommand() { + private val inputFile: File? by option("-f", "--file", help = "The csv input file").file() + val thresholds: String by option("-t", "--thresholds", help = "The threshold to use for the detection") + .choice("default", "spadini").default("default") + val granularity: String by option("-g", "--granularity", help = "Boolean value of numerical for the detection") + .choice("boolean", "numerical").default("boolean") + + override fun run() { + inputFile?.let { + val inputData: List = readInputFile() + val thresholdStrategy: Thresholds = if (thresholds == "default") DefaultThresholds() else SpadiniThresholds() + val granularityFunction: ((TestFile) -> Int) = { + if (granularity == "boolean") { + it.numberOfTestMethods + } else { + it.numberOfTestMethods + } + } + } ?: println("No input file specified") + } + + /** + * Reads the input file and returns a list of the files to analyze + */ + private fun readInputFile(): List { + val rows: List> = csvReader().readAll(inputFile!!) + val inputData = mutableListOf() + for (row in rows) + inputData.add(InputData(application = row[0], testPath = row[1], productionPath = row[2])) + return inputData + } +} + +fun main(args: Array) = DetectorRunner().main(args) \ No newline at end of file diff --git a/src/main/kotlin/thresholds/Thresholds.kt b/src/main/kotlin/thresholds/Thresholds.kt new file mode 100644 index 0000000..7e0e474 --- /dev/null +++ b/src/main/kotlin/thresholds/Thresholds.kt @@ -0,0 +1,52 @@ +package thresholds + +abstract class Thresholds { + + abstract val eagerTest: Int + abstract val assertionRoulette: Int + abstract val verboseTest: Int + abstract val conditionalTestLogic: Int + abstract val magicNumberTest: Int + abstract val generalFixture: Int + abstract val mysteryGuest: Int + abstract val resourceOptimism: Int + abstract val sleepyTest: Int +} + +/** Default thresholds as the original interpretation on Van Deursen et.atl + * + */ +open class DefaultThresholds : Thresholds() { + override val eagerTest: Int + get() = 1 + override val assertionRoulette: Int + get() = 1 + override val verboseTest: Int + get() = 1 + override val conditionalTestLogic: Int + get() = 0 + override val magicNumberTest: Int + get() = 0 + override val generalFixture: Int + get() = 0 + override val mysteryGuest: Int + get() = 0 + override val resourceOptimism: Int + get() = 0 + override val sleepyTest: Int + get() = 0 +} + +/** + * Thresholds for the test smell detection proposed by Spadini at.al. in the paper + * "Investigating severity thresholds for test smells" + */ +class SpadiniThresholds : DefaultThresholds() { + override val eagerTest: Int + get() = 4 + override val assertionRoulette: Int + get() = 3 + override val verboseTest: Int + get() = 13 +} + From bdfa6bf42c38058bc54135962c508780b77bbb3e Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Wed, 2 Dec 2020 10:32:48 +0100 Subject: [PATCH 06/19] refactor of the Abstract Smell with the usage of the thresholds and the two variants of getting the value --- src/main/java/testsmell/AbstractSmell.java | 28 ++++++++++---- src/main/java/testsmell/SmellyElement.java | 2 +- src/main/java/testsmell/TestClass.java | 2 +- src/main/java/testsmell/TestMethod.java | 4 +- .../testsmell/smell/AssertionRoulette.java | 30 +++++---------- .../testsmell/smell/ConditionalTestLogic.java | 36 +++++++----------- .../smell/ConstructorInitialization.java | 32 ++++++---------- .../java/testsmell/smell/DefaultTest.java | 15 +++----- .../java/testsmell/smell/DependentTest.java | 11 +----- .../java/testsmell/smell/DuplicateAssert.java | 21 +++-------- src/main/java/testsmell/smell/EagerTest.java | 37 +++++-------------- src/main/java/testsmell/smell/EmptyTest.java | 20 ++-------- .../smell/ExceptionCatchingThrowing.java | 18 +++------ .../java/testsmell/smell/GeneralFixture.java | 18 +++------ .../java/testsmell/smell/IgnoredTest.java | 18 +++------ src/main/java/testsmell/smell/LazyTest.java | 15 ++------ .../java/testsmell/smell/MagicNumberTest.java | 13 +------ .../java/testsmell/smell/MysteryGuest.java | 21 ++++------- .../java/testsmell/smell/PrintStatement.java | 17 ++------- .../testsmell/smell/RedundantAssertion.java | 17 ++------- .../testsmell/smell/ResourceOptimism.java | 19 ++-------- .../testsmell/smell/SensitiveEquality.java | 18 ++------- src/main/java/testsmell/smell/SleepyTest.java | 20 ++++------ .../java/testsmell/smell/UnknownTest.java | 18 ++------- .../java/testsmell/smell/VerboseTest.java | 20 ++++------ src/main/kotlin/thresholds/Thresholds.kt | 15 ++++++++ 26 files changed, 163 insertions(+), 322 deletions(-) diff --git a/src/main/java/testsmell/AbstractSmell.java b/src/main/java/testsmell/AbstractSmell.java index e0cfe0d..da3d73b 100644 --- a/src/main/java/testsmell/AbstractSmell.java +++ b/src/main/java/testsmell/AbstractSmell.java @@ -4,16 +4,17 @@ import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; public abstract class AbstractSmell { protected Thresholds thresholds; - protected List smellyElementList; + protected Set smellyElementsSet; public AbstractSmell(Thresholds thresholds) { this.thresholds = thresholds; - this.smellyElementList = new ArrayList<>(); + this.smellyElementsSet = new HashSet<>(); } public abstract String getSmellName(); @@ -21,8 +22,8 @@ public AbstractSmell(Thresholds thresholds) { /** * Return 1 if any of the elements has a smell; 0 otherwise */ - public int getHasSmell() { - boolean isSmelly = smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; + public int hasSmell() { + boolean isSmelly = smellyElementsSet.stream().filter(x -> x.isSmelly()).count() >= 1; return isSmelly ? 1 : 0; } @@ -31,7 +32,18 @@ public abstract void runAnalysis(CompilationUnit testFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException; - public abstract List getSmellyElements(); + /** + * Returns the set of analyzed elements (i.e. test methods) + */ + public Set getSmellyElements() { + return smellyElementsSet; + } - public abstract int getNumberOfSmellyTests(); + /** + * Returns the number of test cases in a test suite (jUnit test file). + * In theory, it counts all the smelly elements (i.e., the methods), that are smelly + */ + public int getNumberOfSmellyTests() { + return smellyElementsSet.stream().filter(element -> element.isSmelly()).collect(Collectors.toList()).size(); + } } diff --git a/src/main/java/testsmell/SmellyElement.java b/src/main/java/testsmell/SmellyElement.java index e56ebe6..c5d4b09 100644 --- a/src/main/java/testsmell/SmellyElement.java +++ b/src/main/java/testsmell/SmellyElement.java @@ -5,7 +5,7 @@ public abstract class SmellyElement { public abstract String getElementName(); - public abstract boolean getHasSmell(); + public abstract boolean isSmelly(); public abstract Map getData(); } diff --git a/src/main/java/testsmell/TestClass.java b/src/main/java/testsmell/TestClass.java index df55952..01c48a9 100644 --- a/src/main/java/testsmell/TestClass.java +++ b/src/main/java/testsmell/TestClass.java @@ -28,7 +28,7 @@ public String getElementName() { } @Override - public boolean getHasSmell() { + public boolean isSmelly() { return hasSmell; } diff --git a/src/main/java/testsmell/TestMethod.java b/src/main/java/testsmell/TestMethod.java index 6074b55..8c2732d 100644 --- a/src/main/java/testsmell/TestMethod.java +++ b/src/main/java/testsmell/TestMethod.java @@ -14,7 +14,7 @@ public TestMethod(String methodName) { data = new HashMap<>(); } - public void setHasSmell(boolean hasSmell) { + public void setSmell(boolean hasSmell) { this.hasSmell = hasSmell; } @@ -28,7 +28,7 @@ public String getElementName() { } @Override - public boolean getHasSmell() { + public boolean isSmelly() { return hasSmell; } diff --git a/src/main/java/testsmell/smell/AssertionRoulette.java b/src/main/java/testsmell/smell/AssertionRoulette.java index 45b5383..c202aeb 100644 --- a/src/main/java/testsmell/smell/AssertionRoulette.java +++ b/src/main/java/testsmell/smell/AssertionRoulette.java @@ -49,20 +49,6 @@ public int getAssertionsCount() { return assertionsCount; } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - @Override - public int getNumberOfSmellyTests() { - return smellyElementList.size(); - } - - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int assertNoMessageCount = 0; @@ -76,19 +62,23 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); + boolean isSmelly = assertNoMessageCount >= thresholds.getAssertionRoulette(); + //the method has a smell if there is more than 1 call to production methods + testMethod.setSmell(isSmelly); // if there is only 1 assert statement in the method, then a explanation message is not needed if (assertCount == 1) - testMethod.setHasSmell(false); - else if (assertNoMessageCount >= thresholds.getAssertionRoulette()) //if there is more than one assert statement, then all the asserts need to have an explanation message - testMethod.setHasSmell(true); + testMethod.setSmell(false); + //if there is more than one assert statement, then all the asserts need to have an explanation message + else if (isSmelly) { + testMethod.setSmell(true); + } testMethod.addDataItem("AssertCount", String.valueOf(assertNoMessageCount)); - - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/ConditionalTestLogic.java b/src/main/java/testsmell/smell/ConditionalTestLogic.java index 645a37d..fd02180 100644 --- a/src/main/java/testsmell/smell/ConditionalTestLogic.java +++ b/src/main/java/testsmell/smell/ConditionalTestLogic.java @@ -5,7 +5,10 @@ import com.github.javaparser.ast.expr.ConditionalExpr; import com.github.javaparser.ast.stmt.*; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; @@ -38,19 +41,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - @Override - public int getNumberOfSmellyTests() { - return 0; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int conditionCount, ifCount, switchCount, forCount, foreachCount, whileCount = 0; @@ -62,15 +52,17 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(conditionCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | - ifCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | - switchCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | - foreachCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | - forCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC | - whileCount > DetectionThresholds.CONDITIONAL_TEST_LOGIC); + boolean isSmelly = conditionCount > thresholds.getConditionalTestLogic() | + ifCount > thresholds.getConditionalTestLogic() | + switchCount > thresholds.getConditionalTestLogic() | + foreachCount > thresholds.getConditionalTestLogic() | + forCount > thresholds.getConditionalTestLogic() | + whileCount > thresholds.getConditionalTestLogic(); + + testMethod.setSmell(isSmelly); testMethod.addDataItem("ConditionCount", String.valueOf(conditionCount)); testMethod.addDataItem("IfCount", String.valueOf(ifCount)); @@ -79,7 +71,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.addDataItem("ForCount", String.valueOf(forCount)); testMethod.addDataItem("WhileCount", String.valueOf(whileCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/ConstructorInitialization.java b/src/main/java/testsmell/smell/ConstructorInitialization.java index 8b13917..82063c2 100644 --- a/src/main/java/testsmell/smell/ConstructorInitialization.java +++ b/src/main/java/testsmell/smell/ConstructorInitialization.java @@ -11,10 +11,8 @@ import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; - /* This class checks if the code file contains a Constructor. Ideally, the test suite should not have a constructor. Initialization of fields should be in the setUP() method If this code detects the existence of a constructor, it sets the class as smelly @@ -27,6 +25,11 @@ public ConstructorInitialization(Thresholds thresholds) { super(thresholds); } + @Override + public int getNumberOfSmellyTests() { + return super.hasSmell(); + } + /** * Checks of 'Constructor Initialization' smell */ @@ -39,33 +42,20 @@ public String getSmellName() { * Analyze the test file for Constructor Initialization smell */ @Override - public void runAnalysis(CompilationUnit testFileCompilationUnit,CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException { + public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit productionFileCompilationUnit, String testFileName, String productionFileName) throws FileNotFoundException { this.testFileName = testFileName; ConstructorInitialization.ClassVisitor classVisitor; classVisitor = new ConstructorInitialization.ClassVisitor(); classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - @Override - public int getNumberOfSmellyTests() { - return 0; - } - private class ClassVisitor extends VoidVisitorAdapter { TestClass testClass; - boolean constructorAllowed=false; + boolean constructorAllowed = false; @Override public void visit(ClassOrInterfaceDeclaration n, Void arg) { - for(int i=0;i getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { TestClass testClass; @@ -52,7 +49,7 @@ public void visit(ClassOrInterfaceDeclaration n, Void arg) { if (n.getNameAsString().equals("ExampleUnitTest") || n.getNameAsString().equals("ExampleInstrumentedTest")) { testClass = new TestClass(n.getNameAsString()); testClass.setHasSmell(true); - smellyElementList.add(testClass); + smellyElementsSet.add(testClass); } super.visit(n, arg); } diff --git a/src/main/java/testsmell/smell/DependentTest.java b/src/main/java/testsmell/smell/DependentTest.java index 9cd202f..36af513 100644 --- a/src/main/java/testsmell/smell/DependentTest.java +++ b/src/main/java/testsmell/smell/DependentTest.java @@ -5,7 +5,6 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.Util; import thresholds.Thresholds; @@ -41,7 +40,7 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit for (TestMethod testMethod : testMethods) { if (testMethod.getCalledMethods().stream().anyMatch(x -> x.getName().equals(testMethods.stream().map(z -> z.getMethodDeclaration().getNameAsString())))) { - smellyElementList.add(new testsmell.TestMethod(testMethod.getMethodDeclaration().getNameAsString())); + smellyElementsSet.add(new testsmell.TestMethod(testMethod.getMethodDeclaration().getNameAsString())); } } @@ -55,14 +54,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit }*/ } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; List calledMethods; diff --git a/src/main/java/testsmell/smell/DuplicateAssert.java b/src/main/java/testsmell/smell/DuplicateAssert.java index ff00785..f4e48ee 100644 --- a/src/main/java/testsmell/smell/DuplicateAssert.java +++ b/src/main/java/testsmell/smell/DuplicateAssert.java @@ -40,15 +40,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; TestMethod testMethod; @@ -61,22 +52,22 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); // if there are duplicate messages, then the smell exists - Set set1 = new HashSet(assertMessage); + Set set1 = new HashSet<>(assertMessage); if (set1.size() < assertMessage.size()) { - testMethod.setHasSmell(true); + testMethod.setSmell(true); } // if there are duplicate assert methods, then the smell exists - Set set2 = new HashSet(assertMethod); + Set set2 = new HashSet<>(assertMethod); if (set2.size() < assertMethod.size()) { - testMethod.setHasSmell(true); + testMethod.setSmell(true); } - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/EagerTest.java b/src/main/java/testsmell/smell/EagerTest.java index 8c07981..91e7dba 100644 --- a/src/main/java/testsmell/smell/EagerTest.java +++ b/src/main/java/testsmell/smell/EagerTest.java @@ -9,7 +9,10 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; @@ -22,14 +25,12 @@ public class EagerTest extends AbstractSmell { private static final String TEST_FILE = "Test"; private static final String PRODUCTION_FILE = "Production"; private String productionClassName; - private List smellyElementList; private List productionMethods; private int eagerCount; public EagerTest(Thresholds thresholds) { super(thresholds); productionMethods = new ArrayList<>(); - smellyElementList = new ArrayList<>(); } /** @@ -40,15 +41,6 @@ public String getSmellName() { return "Eager Test"; } - /** - * Returns true if any of the elements has a smell - * @return - */ - @Override - public int getHasSmell() { - return smellyElementList.stream().filter(x -> x.getHasSmell()).count() >= 1; - } - /** * Analyze the test file for test methods that exhibit the 'Eager Test' smell */ @@ -68,23 +60,10 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit eagerCount = classVisitor.overallEager; } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - public int getEagerCount() { return eagerCount; } - @Override - public int getNumberOfSmellyTests() { - return smellyElementList.size(); - } - /** * Visitor class */ @@ -127,11 +106,13 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(currentMethod.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(eagerCount > DetectionThresholds.EAGER_TEST); //the method has a smell if there is more than 1 call to production methods - smellyElementList.add(testMethod); + boolean isSmelly = eagerCount > thresholds.getEagerTest(); + //the method has a smell if there is more than 1 call to production methods + testMethod.setSmell(isSmelly); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/EmptyTest.java b/src/main/java/testsmell/smell/EmptyTest.java index 7d31cbd..c9af309 100644 --- a/src/main/java/testsmell/smell/EmptyTest.java +++ b/src/main/java/testsmell/smell/EmptyTest.java @@ -4,14 +4,11 @@ import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; /** * This class checks if a test method is empty (i.e. the method does not contain statements in its body) @@ -41,14 +38,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - /** * Visitor class */ @@ -62,17 +51,16 @@ private class ClassVisitor extends VoidVisitorAdapter { public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) //method should not be abstract if (!n.isAbstract()) { if (n.getBody().isPresent()) { //get the total number of statements contained in the method - if (n.getBody().get().getStatements().size() == 0) { - testMethod.setHasSmell(true); //the method has no statements (i.e no body) - } + boolean isSmelly = n.getBody().get().getStatements().size() == thresholds.getEmptyTest(); + testMethod.setSmell(isSmelly); //the method has no statements (i.e no body) } } - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); } } } diff --git a/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java b/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java index 61adb4e..4029b66 100644 --- a/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java +++ b/src/main/java/testsmell/smell/ExceptionCatchingThrowing.java @@ -6,13 +6,11 @@ import com.github.javaparser.ast.stmt.ThrowStmt; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.List; /* This class checks if test methods in the class either catch or throw exceptions. Use Junit's exception handling to automatically pass/fail the test @@ -42,14 +40,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int exceptionCount = 0; @@ -62,16 +52,18 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); if (n.getThrownExceptions().size() >= 1) exceptionCount++; - testMethod.setHasSmell(exceptionCount >= 1); + boolean isSmelly = exceptionCount > thresholds.getExceptionCatchingThrowing(); + + testMethod.setSmell(isSmelly); testMethod.addDataItem("ExceptionCount", String.valueOf(exceptionCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/GeneralFixture.java b/src/main/java/testsmell/smell/GeneralFixture.java index 6d57f5e..2433ce6 100644 --- a/src/main/java/testsmell/smell/GeneralFixture.java +++ b/src/main/java/testsmell/smell/GeneralFixture.java @@ -78,15 +78,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit } } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration methodDeclaration = null; private MethodDeclaration currentMethod = null; @@ -131,10 +122,11 @@ public void visit(MethodDeclaration n, Void arg) { super.visit(n, arg); testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(fixtureCount.size() != setupFields.size()); - smellyElementList.add(testMethod); + boolean isSmelly = fixtureCount.size() != setupFields.size(); + testMethod.setSmell(isSmelly); + smellyElementsSet.add(testMethod); - fixtureCount = new HashSet();; + fixtureCount = new HashSet(); currentMethod = null; } } @@ -144,7 +136,7 @@ public void visit(NameExpr n, Void arg) { if (currentMethod != null) { //check if the variable contained in the current test method is also contained in the setup method if (setupFields.contains(n.getNameAsString())) { - if(!fixtureCount.contains(n.getNameAsString())){ + if (!fixtureCount.contains(n.getNameAsString())) { fixtureCount.add(n.getNameAsString()); } //System.out.println(currentMethod.getNameAsString() + " : " + n.getName().toString()); diff --git a/src/main/java/testsmell/smell/IgnoredTest.java b/src/main/java/testsmell/smell/IgnoredTest.java index 17d24bb..ee588ab 100644 --- a/src/main/java/testsmell/smell/IgnoredTest.java +++ b/src/main/java/testsmell/smell/IgnoredTest.java @@ -38,14 +38,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - /** * Visitor class */ @@ -61,7 +53,7 @@ public void visit(ClassOrInterfaceDeclaration n, Void arg) { if (n.getAnnotationByName("Ignore").isPresent()) { testClass = new TestClass(n.getNameAsString()); testClass.setHasSmell(true); - smellyElementList.add(testClass); + smellyElementsSet.add(testClass); } super.visit(n, arg); } @@ -77,8 +69,8 @@ public void visit(MethodDeclaration n, Void arg) { if (n.getAnnotationByName("Test").isPresent()) { if (n.getAnnotationByName("Ignore").isPresent()) { testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(true); - smellyElementList.add(testMethod); + testMethod.setSmell(true); + smellyElementsSet.add(testMethod); return; } } @@ -88,8 +80,8 @@ public void visit(MethodDeclaration n, Void arg) { if (n.getNameAsString().toLowerCase().startsWith("test")) { if (!n.getModifiers().contains(Modifier.PUBLIC)) { testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(true); - smellyElementList.add(testMethod); + testMethod.setSmell(true); + smellyElementsSet.add(testMethod); return; } } diff --git a/src/main/java/testsmell/smell/LazyTest.java b/src/main/java/testsmell/smell/LazyTest.java index 2c3bd0b..e3a2303 100644 --- a/src/main/java/testsmell/smell/LazyTest.java +++ b/src/main/java/testsmell/smell/LazyTest.java @@ -31,7 +31,6 @@ public class LazyTest extends AbstractSmell { public LazyTest(Thresholds thresholds) { super(thresholds); productionMethods = new ArrayList<>(); - smellyElementList = new ArrayList<>(); calledProductionMethods = new ArrayList<>(); } @@ -67,21 +66,13 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit // If counts don not match, this production method is used by multiple test methods. Hence, there is a Lazy Test smell. // If the counts were equal it means that the production method is only used (called from) inside one test method TestMethod testClass = new TestMethod(method.getTestMethod()); - testClass.setHasSmell(true); - smellyElementList.add(testClass); + testClass.setSmell(true); + smellyElementsSet.add(testClass); } } } } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class MethodUsage { private String testMethod, productionMethod; @@ -138,7 +129,7 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(currentMethod.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); //reset values for next method diff --git a/src/main/java/testsmell/smell/MagicNumberTest.java b/src/main/java/testsmell/smell/MagicNumberTest.java index f365bdc..4f0061c 100644 --- a/src/main/java/testsmell/smell/MagicNumberTest.java +++ b/src/main/java/testsmell/smell/MagicNumberTest.java @@ -10,7 +10,6 @@ import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; import java.util.List; public class MagicNumberTest extends AbstractSmell { @@ -39,14 +38,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; TestMethod testMethod; @@ -58,10 +49,10 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(magicCount >= DetectionThresholds.MAGIC_NUMBER_TEST); + testMethod.setSmell(magicCount >= thresholds.getMagicNumberTest()); testMethod.addDataItem("MagicNumberCount", String.valueOf(magicCount)); smellyElementList.add(testMethod); diff --git a/src/main/java/testsmell/smell/MysteryGuest.java b/src/main/java/testsmell/smell/MysteryGuest.java index 3ff7e52..a30f4ca 100644 --- a/src/main/java/testsmell/smell/MysteryGuest.java +++ b/src/main/java/testsmell/smell/MysteryGuest.java @@ -5,7 +5,10 @@ import com.github.javaparser.ast.expr.AnnotationExpr; import com.github.javaparser.ast.expr.VariableDeclarationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; @@ -24,7 +27,6 @@ public class MysteryGuest extends AbstractSmell { public MysteryGuest(Thresholds thresholds) { super(thresholds); - smellyElementList = new ArrayList<>(); } /** @@ -45,14 +47,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private List mysteryTypes = new ArrayList<>( Arrays.asList( @@ -104,13 +98,14 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(mysteryCount > DetectionThresholds.MYSTERY_GUEST); + boolean isSmelly = mysteryCount > thresholds.getMysteryGuest(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("MysteryCount", String.valueOf(mysteryCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/PrintStatement.java b/src/main/java/testsmell/smell/PrintStatement.java index 707a0c2..b5f7e3a 100644 --- a/src/main/java/testsmell/smell/PrintStatement.java +++ b/src/main/java/testsmell/smell/PrintStatement.java @@ -7,13 +7,11 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.List; /* Test methods should not contain print statements as execution of unit tests is an automated process with little to no human intervention. Hence, print statements are redundant. @@ -43,14 +41,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int printCount = 0; @@ -62,13 +52,14 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(printCount >= 1); + boolean isSmelly = printCount > thresholds.getPrintStatement(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("PrintCount", String.valueOf(printCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/RedundantAssertion.java b/src/main/java/testsmell/smell/RedundantAssertion.java index d277909..257b75a 100644 --- a/src/main/java/testsmell/smell/RedundantAssertion.java +++ b/src/main/java/testsmell/smell/RedundantAssertion.java @@ -7,13 +7,11 @@ import com.github.javaparser.ast.expr.NullLiteralExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.List; /* If a test method contains an assert statement that explicitly returns a true or false, the method is marked as smelly @@ -42,14 +40,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int redundantCount = 0; @@ -61,13 +51,14 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(redundantCount >= 1); + boolean isSmelly = redundantCount > thresholds.getRedundantAssertion(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("RedundantCount", String.valueOf(redundantCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/ResourceOptimism.java b/src/main/java/testsmell/smell/ResourceOptimism.java index 3bb0a01..71a8cd5 100644 --- a/src/main/java/testsmell/smell/ResourceOptimism.java +++ b/src/main/java/testsmell/smell/ResourceOptimism.java @@ -40,15 +40,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int resourceOptimismCount = 0; @@ -64,13 +55,13 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n) || Util.isValidSetupMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(methodVariables.size() > DetectionThresholds.RESOURCE_OPTIMISM || hasSmell == true); + testMethod.setSmell(methodVariables.size() > thresholds.getResourceOptimism() || hasSmell == true); testMethod.addDataItem("ResourceOptimismCount", String.valueOf(resourceOptimismCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; @@ -150,11 +141,7 @@ public void visit(MethodCallExpr n, Void arg) { } } } - - } - - } diff --git a/src/main/java/testsmell/smell/SensitiveEquality.java b/src/main/java/testsmell/smell/SensitiveEquality.java index b0959f6..c3d79aa 100644 --- a/src/main/java/testsmell/smell/SensitiveEquality.java +++ b/src/main/java/testsmell/smell/SensitiveEquality.java @@ -6,14 +6,11 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; public class SensitiveEquality extends AbstractSmell { @@ -39,14 +36,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int sensitiveCount = 0; @@ -58,13 +47,14 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(sensitiveCount >= 1); + boolean isSmelly = sensitiveCount > thresholds.getSensitiveEquality(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("SensitiveCount", String.valueOf(sensitiveCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/SleepyTest.java b/src/main/java/testsmell/smell/SleepyTest.java index b8b1022..f1871c9 100644 --- a/src/main/java/testsmell/smell/SleepyTest.java +++ b/src/main/java/testsmell/smell/SleepyTest.java @@ -5,7 +5,10 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; @@ -39,14 +42,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; private int sleepCount = 0; @@ -58,13 +53,14 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - testMethod.setHasSmell(sleepCount > DetectionThresholds.SLEEPY_TEST); + boolean isSmelly = sleepCount > thresholds.getSleepyTest(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("ThreadSleepCount", String.valueOf(sleepCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/UnknownTest.java b/src/main/java/testsmell/smell/UnknownTest.java index 6dc13fd..e7223e4 100644 --- a/src/main/java/testsmell/smell/UnknownTest.java +++ b/src/main/java/testsmell/smell/UnknownTest.java @@ -42,15 +42,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - - private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; TestMethod testMethod; @@ -58,7 +49,6 @@ private class ClassVisitor extends VoidVisitorAdapter { boolean hasAssert = false; boolean hasExceptionAnnotation = false; - // examine all methods in the test class @Override public void visit(MethodDeclaration n, Void arg) { @@ -77,14 +67,14 @@ public void visit(MethodDeclaration n, Void arg) { } currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) super.visit(n, arg); - // if there are duplicate messages, then the smell exists + // no assertions and no annotation if (!hasAssert && !hasExceptionAnnotation) - testMethod.setHasSmell(true); + testMethod.setSmell(true); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/java/testsmell/smell/VerboseTest.java b/src/main/java/testsmell/smell/VerboseTest.java index e5bf9b1..13b9344 100644 --- a/src/main/java/testsmell/smell/VerboseTest.java +++ b/src/main/java/testsmell/smell/VerboseTest.java @@ -3,7 +3,10 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.SmellyElement; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; @@ -36,14 +39,6 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit classVisitor.visit(testFileCompilationUnit, null); } - /** - * Returns the set of analyzed elements (i.e. test methods) - */ - @Override - public List getSmellyElements() { - return smellyElementList; - } - private class ClassVisitor extends VoidVisitorAdapter { final int MAX_STATEMENTS = 123; private MethodDeclaration currentMethod = null; @@ -56,7 +51,7 @@ public void visit(MethodDeclaration n, Void arg) { if (Util.isValidTestMethod(n)) { currentMethod = n; testMethod = new TestMethod(n.getNameAsString()); - testMethod.setHasSmell(false); //default value is false (i.e. no smell) + testMethod.setSmell(false); //default value is false (i.e. no smell) //method should not be abstract if (!currentMethod.isAbstract()) { @@ -67,10 +62,11 @@ public void visit(MethodDeclaration n, Void arg) { } } } - testMethod.setHasSmell(verboseCount >= DetectionThresholds.VERBOSE_TEST); + boolean isSmelly = verboseCount > thresholds.getVerboseTest(); + testMethod.setSmell(isSmelly); testMethod.addDataItem("VerboseCount", String.valueOf(verboseCount)); - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; diff --git a/src/main/kotlin/thresholds/Thresholds.kt b/src/main/kotlin/thresholds/Thresholds.kt index 7e0e474..73634fd 100644 --- a/src/main/kotlin/thresholds/Thresholds.kt +++ b/src/main/kotlin/thresholds/Thresholds.kt @@ -11,6 +11,11 @@ abstract class Thresholds { abstract val mysteryGuest: Int abstract val resourceOptimism: Int abstract val sleepyTest: Int + abstract val emptyTest: Int + abstract val exceptionCatchingThrowing: Int + abstract val printStatement: Int + abstract val redundantAssertion: Int + abstract val sensitiveEquality: Int } /** Default thresholds as the original interpretation on Van Deursen et.atl @@ -35,6 +40,16 @@ open class DefaultThresholds : Thresholds() { get() = 0 override val sleepyTest: Int get() = 0 + override val emptyTest: Int + get() = 0 + override val exceptionCatchingThrowing: Int + get() = 0 + override val printStatement: Int + get() = 0 + override val redundantAssertion: Int + get() = 0 + override val sensitiveEquality: Int + get() = 0 } /** From 8a6ec212453258650713f89172ff3cb5ba660322 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 10 Dec 2020 17:09:04 +0100 Subject: [PATCH 07/19] concluded the logic of the new implementation --- src/main/kotlin/detection/Detection.kt | 50 ++++++++++++-------------- src/main/kotlin/io/CSVWriter.kt | 32 +++++++++++++++++ src/main/kotlin/testsmell/Runner.kt | 31 +++++++++++----- 3 files changed, 77 insertions(+), 36 deletions(-) create mode 100644 src/main/kotlin/io/CSVWriter.kt diff --git a/src/main/kotlin/detection/Detection.kt b/src/main/kotlin/detection/Detection.kt index 7889ed1..c0ade1c 100644 --- a/src/main/kotlin/detection/Detection.kt +++ b/src/main/kotlin/detection/Detection.kt @@ -3,43 +3,37 @@ package detection import testsmell.AbstractSmell import testsmell.TestFile import testsmell.TestSmellDetector -import thresholds.Thresholds /** * Runs the detection by exploiting the TestSmellDetector class */ class Detection(private val project: String, - private val pairs: List, - private val testSmellDetector: TestSmellDetector, - val threshold: Thresholds) { + private val testClassPath: String, + private val productionClassPath: String, + private val testSmellDetector: TestSmellDetector) { /** - * Analyze the given pairs and return a list of DetectionResult + * Analyze a given pair and return a DetectionResult */ - fun detectSmells(getSmellValue: (AbstractSmell) -> Int): List { - val resultList = mutableListOf() - for (pair in pairs) { - val testFile = TestFile(project, pair.testClassPath, pair.productionClassPath) - val tempFile: TestFile = testSmellDetector.detectSmells(testFile) + fun detectSmells(getSmellValue: (AbstractSmell) -> Int): DetectionResult { + val testFile = TestFile(project, testClassPath, productionClassPath) + val tempFile: TestFile = testSmellDetector.detectSmells(testFile) - val smellLists: List = testSmellDetector.testSmellNames - val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } - val outputs: List> = smellLists.flatMap { name -> - smellValues.map { name to it } - } - - val detectionResult = DetectionResult( - application = project, - testFileName = tempFile.testFileName, - testFilePath = tempFile.testFilePath, - productionFilePath = tempFile.productionFilePath, - relativeTestFilePath = tempFile.relativeTestFilePath, - relativeProductionFilePath = tempFile.relativeProductionFilePath, - numberOfTestMethods = testFile.numberOfTestMethods, - smellResult = outputs - ) - resultList.add(detectionResult) + val smellLists: List = testSmellDetector.testSmellNames + val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } + val outputs: List> = smellLists.flatMap { name -> + smellValues.map { name to it } } - return resultList + + return DetectionResult( + application = project, + testFileName = tempFile.testFileName, + testFilePath = tempFile.testFilePath, + productionFilePath = tempFile.productionFilePath, + relativeTestFilePath = tempFile.relativeTestFilePath, + relativeProductionFilePath = tempFile.relativeProductionFilePath, + numberOfTestMethods = testFile.numberOfTestMethods, + smellResult = outputs + ) } } \ No newline at end of file diff --git a/src/main/kotlin/io/CSVWriter.kt b/src/main/kotlin/io/CSVWriter.kt new file mode 100644 index 0000000..649182e --- /dev/null +++ b/src/main/kotlin/io/CSVWriter.kt @@ -0,0 +1,32 @@ +package io + +import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter +import detection.DetectionResult + +class CSVWriter(private val destinationPath: String = "test-smells.csv") { + + private var flag: Boolean = false + + /** + * Write the results of a detection to the csv file + */ + fun writeResult(result: DetectionResult) { + if (!flag) { + csvWriter().open(destinationPath) { + val header = listOf("App", "TestClass", "TestFilePath", "ProductionFilePath", + "RelativeTestFilePath", "RelativeProductionFilePath", "NumberOfMethods") + val smells = result.smellResult.map { it.first } + writeRow(header.plus(smells)) + flag = true + } + } + val toSave = listOf(result.application, + result.testFileName, result.testFilePath, result.productionFilePath, + result.relativeTestFilePath, result.relativeProductionFilePath, result.numberOfTestMethods) + csvWriter().open(destinationPath, append = true) { + writeRow(toSave.plus(result.smellResult.map { it.second.toString() })) + } + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/testsmell/Runner.kt b/src/main/kotlin/testsmell/Runner.kt index cd36ce8..9f97017 100644 --- a/src/main/kotlin/testsmell/Runner.kt +++ b/src/main/kotlin/testsmell/Runner.kt @@ -6,6 +6,9 @@ import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.file import com.github.doyaaaaaken.kotlincsv.dsl.csvReader +import detection.Detection +import detection.DetectionResult +import io.CSVWriter import io.InputData import thresholds.DefaultThresholds import thresholds.SpadiniThresholds @@ -16,19 +19,31 @@ class DetectorRunner : CliktCommand() { private val inputFile: File? by option("-f", "--file", help = "The csv input file").file() val thresholds: String by option("-t", "--thresholds", help = "The threshold to use for the detection") .choice("default", "spadini").default("default") - val granularity: String by option("-g", "--granularity", help = "Boolean value of numerical for the detection") + private val granularity: String by option("-g", "--granularity", help = "Boolean value of numerical for the detection") .choice("boolean", "numerical").default("boolean") override fun run() { + val thresholdStrategy: Thresholds = if (thresholds == "default") DefaultThresholds() else SpadiniThresholds() + val granularityFunction: ((AbstractSmell) -> Int) = { + if (granularity == "boolean") { + it.hasSmell() + } else { + it.numberOfSmellyTests + } + } + inputFile?.let { val inputData: List = readInputFile() - val thresholdStrategy: Thresholds = if (thresholds == "default") DefaultThresholds() else SpadiniThresholds() - val granularityFunction: ((TestFile) -> Int) = { - if (granularity == "boolean") { - it.numberOfTestMethods - } else { - it.numberOfTestMethods - } + val writer = CSVWriter("test-smells.csv") + for (input in inputData) { + val detection = Detection( + project = input.application, + testClassPath = input.testPath, + productionClassPath = input.productionPath, + testSmellDetector = TestSmellDetector(thresholdStrategy) + ) + val detectedSmell: DetectionResult = detection.detectSmells(granularityFunction) + writer.writeResult(detectedSmell) } } ?: println("No input file specified") } From 09d475c8d0fae3327c7d854a84d3aecea35c6305 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 10 Dec 2020 17:46:33 +0100 Subject: [PATCH 08/19] pom and readme --- README.md | 17 ++++++++++++++--- pom.xml | 2 +- src/test/java/testsmell/TestFileTest.java | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 59ad5d4..a0c59d7 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ Unit test code, just like any other source code, is subject to bad programming p Test smells are defined as bad programming practices in unit test code (such as how test cases are organized, implemented and interact with each other) that indicate potential design problems in the test source code. - - ## Project Overview The purpose of this project is twofold: @@ -15,7 +13,20 @@ The purpose of this project is twofold: 1. Contribute to the list of existing test smells, by proposing new test smells that developers need to be aware of. 2. Provide developers with a tool to automatically detect test smell in their unit test code. - ## More Information Visit the project website: https://testsmells.github.io/ + +## Execution + +Running the jar with `--help` will print its usage. + +``` +Options: + -f, --file PATH The csv input file + -t, --thresholds [default|spadini] + The threshold to use for the detection + -g, --granularity [boolean|numerical] + Boolean value of numerical for the + detection +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 34aa463..5898e80 100644 --- a/pom.xml +++ b/pom.xml @@ -107,7 +107,7 @@ - Main + testsmell.RunnerKt diff --git a/src/test/java/testsmell/TestFileTest.java b/src/test/java/testsmell/TestFileTest.java index 54e0d52..b05a487 100644 --- a/src/test/java/testsmell/TestFileTest.java +++ b/src/test/java/testsmell/TestFileTest.java @@ -10,8 +10,8 @@ class TestFileTest { private String fileTest = "commons-lang," + - "/Users/grano/projects/commons-lang/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java," + - "/Users/grano/projects/commons-lang/src/main/java/org/apache/commons/lang3/RandomStringUtils.java"; + "/Users/giograno/projects/commons-lang/src/test/java/org/apache/commons/lang3/RandomStringUtilsTest.java," + + "/Users/giograno/projects/commons-lang/src/main/java/org/apache/commons/lang3/RandomStringUtils.java"; private String fileTestWindows = "myCoolApp," + "F:\\Apps\\myCoolApp\\code\\test\\GraphTest.java," + "F:\\Apps\\myCoolApp\\code\\src\\Graph.java"; From 45f334b005b113f942096958affdbe03d10301e6 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 10 Dec 2020 18:07:09 +0100 Subject: [PATCH 09/19] fix of the magic number test computation --- .../java/testsmell/smell/MagicNumberTest.java | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/main/java/testsmell/smell/MagicNumberTest.java b/src/main/java/testsmell/smell/MagicNumberTest.java index 4f0061c..6b756ed 100644 --- a/src/main/java/testsmell/smell/MagicNumberTest.java +++ b/src/main/java/testsmell/smell/MagicNumberTest.java @@ -6,15 +6,14 @@ import com.github.javaparser.ast.expr.MethodCallExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; -import testsmell.*; +import testsmell.AbstractSmell; +import testsmell.TestMethod; +import testsmell.Util; import thresholds.Thresholds; import java.io.FileNotFoundException; -import java.util.List; -public class MagicNumberTest extends AbstractSmell { - - private List smellyElementList; +public class MagicNumberTest extends AbstractSmell { public MagicNumberTest(Thresholds thresholds) { super(thresholds); @@ -40,6 +39,7 @@ public void runAnalysis(CompilationUnit testFileCompilationUnit, CompilationUnit private class ClassVisitor extends VoidVisitorAdapter { private MethodDeclaration currentMethod = null; + private MagicNumberTest magicNumberTest; TestMethod testMethod; private int magicCount = 0; @@ -54,8 +54,7 @@ public void visit(MethodDeclaration n, Void arg) { testMethod.setSmell(magicCount >= thresholds.getMagicNumberTest()); testMethod.addDataItem("MagicNumberCount", String.valueOf(magicCount)); - - smellyElementList.add(testMethod); + smellyElementsSet.add(testMethod); //reset values for next method currentMethod = null; @@ -77,27 +76,27 @@ public void visit(MethodCallExpr n, Void arg) { n.getNameAsString().equals("assertNotNull") || n.getNameAsString().equals("assertNull")) { // checks all arguments of the assert method - for (Expression argument:n.getArguments()) { + for (Expression argument : n.getArguments()) { // if the argument is a number - if(Util.isNumber(argument.toString())){ - magicCount++; - } - // if the argument contains an ObjectCreationExpr (e.g. assertEquals(new Integer(2),...) - else if(argument instanceof ObjectCreationExpr){ - for (Expression objectArguments:((ObjectCreationExpr) argument).getArguments()){ - if(Util.isNumber(objectArguments.toString())){ - magicCount++; - } - } - } - // if the argument contains an MethodCallExpr (e.g. assertEquals(someMethod(2),...) - else if(argument instanceof MethodCallExpr){ - for (Expression objectArguments:((MethodCallExpr) argument).getArguments()){ - if(Util.isNumber(objectArguments.toString())){ - magicCount++; - } - } - } + if (Util.isNumber(argument.toString())) { + magicCount++; + } + // if the argument contains an ObjectCreationExpr (e.g. assertEquals(new Integer(2),...) + else if (argument instanceof ObjectCreationExpr) { + for (Expression objectArguments : ((ObjectCreationExpr) argument).getArguments()) { + if (Util.isNumber(objectArguments.toString())) { + magicCount++; + } + } + } + // if the argument contains an MethodCallExpr (e.g. assertEquals(someMethod(2),...) + else if (argument instanceof MethodCallExpr) { + for (Expression objectArguments : ((MethodCallExpr) argument).getArguments()) { + if (Util.isNumber(objectArguments.toString())) { + magicCount++; + } + } + } } } } From 9c11df8859869a35719685fc741db427515ecfb3 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 22 Dec 2020 11:08:48 +0100 Subject: [PATCH 10/19] improved readme --- .gitignore | 3 ++- README.md | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a04adfe..38149ef 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ hs_err_pid* Output_TestSmellDetection* target/ TestSmellDetector.iml -files-smell.csv \ No newline at end of file +files-smell.csv +test-smells.csv \ No newline at end of file diff --git a/README.md b/README.md index a0c59d7..40b1759 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ Visit the project website: https://testsmells.github.io/ Running the jar with `--help` will print its usage. +* A CSV input file always need to be given as parameter, specified with `-f`; +* A detection threshold can also be specified. Possible values are `default` and `spadini`. The flag is `-t`. +By default, the tool uses the thresholds that have been originally implemented; +with `spadini`, sensibility thresholds published by Spadini et.al. will be used. +* One can specify the granularity of the detection. `boolean` will return either true or false, respectively if a +given smell is present or not in the test; `numerical` will return instead the number of smelly instances detected. + ``` Options: -f, --file PATH The csv input file From 34a8af6a6e5136846391d028deb25811302c63cb Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 22 Dec 2020 11:23:13 +0100 Subject: [PATCH 11/19] Fixed the repeated smells in the output file --- src/main/kotlin/detection/Detection.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/detection/Detection.kt b/src/main/kotlin/detection/Detection.kt index c0ade1c..b7771f1 100644 --- a/src/main/kotlin/detection/Detection.kt +++ b/src/main/kotlin/detection/Detection.kt @@ -21,9 +21,8 @@ class Detection(private val project: String, val smellLists: List = testSmellDetector.testSmellNames val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } - val outputs: List> = smellLists.flatMap { name -> - smellValues.map { name to it } - } + + val outputs: List> = smellLists.zip(smellValues) return DetectionResult( application = project, From 493cbfee2f5eb8bedf42359d015f75bc1ffb1325 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 22 Dec 2020 11:52:12 +0100 Subject: [PATCH 12/19] Output changed. when default is used, it prints 'true' or 'false'. when the granularity is used, it prints the number --- src/main/java/testsmell/AbstractSmell.java | 7 +++---- .../java/testsmell/smell/ConstructorInitialization.java | 2 +- src/main/java/testsmell/smell/DefaultTest.java | 2 +- src/main/kotlin/detection/Detection.kt | 6 +++--- src/main/kotlin/detection/DetectionResult.kt | 2 +- src/main/kotlin/testsmell/Runner.kt | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/testsmell/AbstractSmell.java b/src/main/java/testsmell/AbstractSmell.java index da3d73b..7a84ee5 100644 --- a/src/main/java/testsmell/AbstractSmell.java +++ b/src/main/java/testsmell/AbstractSmell.java @@ -22,9 +22,8 @@ public AbstractSmell(Thresholds thresholds) { /** * Return 1 if any of the elements has a smell; 0 otherwise */ - public int hasSmell() { - boolean isSmelly = smellyElementsSet.stream().filter(x -> x.isSmelly()).count() >= 1; - return isSmelly ? 1 : 0; + public boolean hasSmell() { + return smellyElementsSet.stream().filter(SmellyElement::isSmelly).count() >= 1; } public abstract void runAnalysis(CompilationUnit testFileCompilationUnit, @@ -44,6 +43,6 @@ public Set getSmellyElements() { * In theory, it counts all the smelly elements (i.e., the methods), that are smelly */ public int getNumberOfSmellyTests() { - return smellyElementsSet.stream().filter(element -> element.isSmelly()).collect(Collectors.toList()).size(); + return (int) smellyElementsSet.stream().filter(SmellyElement::isSmelly).count(); } } diff --git a/src/main/java/testsmell/smell/ConstructorInitialization.java b/src/main/java/testsmell/smell/ConstructorInitialization.java index 82063c2..6f0d87d 100644 --- a/src/main/java/testsmell/smell/ConstructorInitialization.java +++ b/src/main/java/testsmell/smell/ConstructorInitialization.java @@ -27,7 +27,7 @@ public ConstructorInitialization(Thresholds thresholds) { @Override public int getNumberOfSmellyTests() { - return super.hasSmell(); + return super.getNumberOfSmellyTests(); } /** diff --git a/src/main/java/testsmell/smell/DefaultTest.java b/src/main/java/testsmell/smell/DefaultTest.java index f585aa1..41f98f0 100644 --- a/src/main/java/testsmell/smell/DefaultTest.java +++ b/src/main/java/testsmell/smell/DefaultTest.java @@ -31,7 +31,7 @@ public String getSmellName() { @Override public int getNumberOfSmellyTests() { - return super.hasSmell(); + return super.getNumberOfSmellyTests(); } @Override diff --git a/src/main/kotlin/detection/Detection.kt b/src/main/kotlin/detection/Detection.kt index b7771f1..3986869 100644 --- a/src/main/kotlin/detection/Detection.kt +++ b/src/main/kotlin/detection/Detection.kt @@ -15,14 +15,14 @@ class Detection(private val project: String, /** * Analyze a given pair and return a DetectionResult */ - fun detectSmells(getSmellValue: (AbstractSmell) -> Int): DetectionResult { + fun detectSmells(getSmellValue: (AbstractSmell) -> Any): DetectionResult { val testFile = TestFile(project, testClassPath, productionClassPath) val tempFile: TestFile = testSmellDetector.detectSmells(testFile) val smellLists: List = testSmellDetector.testSmellNames - val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } + val smellValues: List = tempFile.testSmells.map { getSmellValue.invoke(it) } - val outputs: List> = smellLists.zip(smellValues) + val outputs: List> = smellLists.zip(smellValues.map { e -> e.toString() }) return DetectionResult( application = project, diff --git a/src/main/kotlin/detection/DetectionResult.kt b/src/main/kotlin/detection/DetectionResult.kt index edd592a..07dee79 100644 --- a/src/main/kotlin/detection/DetectionResult.kt +++ b/src/main/kotlin/detection/DetectionResult.kt @@ -11,5 +11,5 @@ data class DetectionResult( val relativeTestFilePath: String, val relativeProductionFilePath: String, val numberOfTestMethods: Int, - val smellResult: List> + val smellResult: List> ) diff --git a/src/main/kotlin/testsmell/Runner.kt b/src/main/kotlin/testsmell/Runner.kt index 9f97017..c33c4bc 100644 --- a/src/main/kotlin/testsmell/Runner.kt +++ b/src/main/kotlin/testsmell/Runner.kt @@ -24,7 +24,7 @@ class DetectorRunner : CliktCommand() { override fun run() { val thresholdStrategy: Thresholds = if (thresholds == "default") DefaultThresholds() else SpadiniThresholds() - val granularityFunction: ((AbstractSmell) -> Int) = { + val granularityFunction: ((AbstractSmell) -> Any) = { if (granularity == "boolean") { it.hasSmell() } else { From c30009cba885da5258a22956b10cb31ffc0fea98 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Thu, 7 Jan 2021 09:56:26 +0100 Subject: [PATCH 13/19] added the option to specify the output --- src/main/kotlin/testsmell/Runner.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/testsmell/Runner.kt b/src/main/kotlin/testsmell/Runner.kt index c33c4bc..a024f5f 100644 --- a/src/main/kotlin/testsmell/Runner.kt +++ b/src/main/kotlin/testsmell/Runner.kt @@ -21,6 +21,7 @@ class DetectorRunner : CliktCommand() { .choice("default", "spadini").default("default") private val granularity: String by option("-g", "--granularity", help = "Boolean value of numerical for the detection") .choice("boolean", "numerical").default("boolean") + private val output: String by option("-o", "--output", help = "").default("test-smells.csv") override fun run() { val thresholdStrategy: Thresholds = if (thresholds == "default") DefaultThresholds() else SpadiniThresholds() @@ -34,7 +35,7 @@ class DetectorRunner : CliktCommand() { inputFile?.let { val inputData: List = readInputFile() - val writer = CSVWriter("test-smells.csv") + val writer = CSVWriter(output) for (input in inputData) { val detection = Detection( project = input.application, From 10e7f0671f0400833a8bcc7029d00894dfee1773 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Mon, 18 Jan 2021 18:49:42 +0100 Subject: [PATCH 14/19] update of the readme with the description of the output option --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 40b1759..503760f 100644 --- a/README.md +++ b/README.md @@ -36,4 +36,6 @@ Options: -g, --granularity [boolean|numerical] Boolean value of numerical for the detection + -o, --output TEXT + -h, --help Show this message and exit ``` \ No newline at end of file From 0c670ae11c54ca9dc8c1f98c195a9717263a2428 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 22 Jan 2021 14:47:24 +0100 Subject: [PATCH 15/19] Semplification of the pom file --- pom.xml | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/pom.xml b/pom.xml index 5898e80..fe1dccf 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,10 @@ 1.4.20 1.8 + UTF-8 + 2.22.2 + 3.8.1 + 5.7.0 edu.rit.se.testsmells @@ -15,6 +19,13 @@ jar + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + org.jetbrains.kotlin kotlin-maven-plugin @@ -46,10 +57,11 @@ + org.apache.maven.plugins maven-compiler-plugin - 3.5.1 + ${maven-compiler-plugin.version} 1.8 1.8 @@ -82,18 +94,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 3.0.0-M4 - - - org.junit.vintage - junit-vintage-engine - 5.4.2 - - - org.apache.maven.plugins maven-assembly-plugin @@ -132,22 +132,10 @@ opencsv 3.9 - - junit - junit - 4.12 - test - - - org.junit.jupiter - junit-jupiter-api - 5.4.2 - test - org.junit.jupiter - junit-jupiter-engine - 5.4.2 + junit-jupiter + ${junit-jupiter.version} test From 698151d3077abb1eb9f45739873223cc6d0bbcc9 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Sat, 23 Jan 2021 11:50:04 +0100 Subject: [PATCH 16/19] including some tests --- pom.xml | 7 + src/main/java/testsmell/AbstractSmell.java | 1 - src/main/java/testsmell/smell/EagerTest.java | 1 - .../testsmell/TestDetectionCorrectness.kt | 2158 +++++++++++++++++ 4 files changed, 2165 insertions(+), 2 deletions(-) create mode 100644 src/test/kotlin/testsmell/TestDetectionCorrectness.kt diff --git a/pom.xml b/pom.xml index fe1dccf..a1fa17c 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ 2.22.2 3.8.1 5.7.0 + 1.8 edu.rit.se.testsmells @@ -153,6 +154,12 @@ kotlin-csv-jvm 0.13.0 + + org.mockito + mockito-core + 3.2.4 + test + \ No newline at end of file diff --git a/src/main/java/testsmell/AbstractSmell.java b/src/main/java/testsmell/AbstractSmell.java index 7a84ee5..5ecd65f 100644 --- a/src/main/java/testsmell/AbstractSmell.java +++ b/src/main/java/testsmell/AbstractSmell.java @@ -6,7 +6,6 @@ import java.io.FileNotFoundException; import java.util.HashSet; import java.util.Set; -import java.util.stream.Collectors; public abstract class AbstractSmell { protected Thresholds thresholds; diff --git a/src/main/java/testsmell/smell/EagerTest.java b/src/main/java/testsmell/smell/EagerTest.java index 91e7dba..a782d85 100644 --- a/src/main/java/testsmell/smell/EagerTest.java +++ b/src/main/java/testsmell/smell/EagerTest.java @@ -10,7 +10,6 @@ import com.github.javaparser.ast.expr.NameExpr; import com.github.javaparser.ast.visitor.VoidVisitorAdapter; import testsmell.AbstractSmell; -import testsmell.SmellyElement; import testsmell.TestMethod; import testsmell.Util; import thresholds.Thresholds; diff --git a/src/test/kotlin/testsmell/TestDetectionCorrectness.kt b/src/test/kotlin/testsmell/TestDetectionCorrectness.kt new file mode 100644 index 0000000..9d781d5 --- /dev/null +++ b/src/test/kotlin/testsmell/TestDetectionCorrectness.kt @@ -0,0 +1,2158 @@ +package testsmell + +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.CompilationUnit +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import testsmell.smell.AssertionRoulette +import testsmell.smell.EagerTest +import thresholds.DefaultThresholds +import thresholds.SpadiniThresholds + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestDetectionCorrectness { + + private lateinit var testCompilationUnit: CompilationUnit + private lateinit var productionCompilationUnit: CompilationUnit + private lateinit var testFile: TestFile + private val booleanGranularity: ((AbstractSmell) -> Any) = { it.hasSmell() } + private val numericGranularity: ((AbstractSmell) -> Any) = { it.numberOfSmellyTests } + + @BeforeEach + fun setup() { + testCompilationUnit = JavaParser.parse(fractionTest) + productionCompilationUnit = JavaParser.parse(fractionSource) + testFile = mock(TestFile::class.java) + `when`(testFile.testFileNameWithoutExtension).thenReturn("fake/path") + `when`(testFile.productionFileNameWithoutExtension).thenReturn("fake/path") + } + + @Test + fun `Test assertion roulette boolean`() { + val smell = AssertionRoulette(DefaultThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { booleanGranularity.invoke(it) } + Assertions.assertTrue(values[0] as Boolean) + } + + @Test + fun `Test assertion roulette granular`() { + val smell = AssertionRoulette(DefaultThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { numericGranularity.invoke(it) } + Assertions.assertEquals(24, values[0] as Int) + } + + @Test + fun `Test assertion roulette granular with Spadini`() { + val smell = AssertionRoulette(SpadiniThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { numericGranularity.invoke(it) } + Assertions.assertEquals(23, values[0] as Int) + } + + @Test + fun `Test eager test boolean`() { + val smell = EagerTest(DefaultThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { booleanGranularity.invoke(it) } + Assertions.assertTrue(values[0] as Boolean) + } + + @Test + fun `Test eager test granular`() { + val smell = EagerTest(DefaultThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { numericGranularity.invoke(it) } + Assertions.assertEquals(24, values[0] as Int) + } + + @Test + fun `Test eager test granular spadini threshold`() { + val smell = EagerTest(SpadiniThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + `when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { numericGranularity.invoke(it) } + Assertions.assertEquals(24, values[0] as Int) + } + + @Test + fun `Test number of methods detected`() { + val declaration = testCompilationUnit.types[0] + Assertions.assertEquals(26, declaration.methods.size) + } + + private val fractionSource = """ + /* + * 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.apache.commons.lang3.math; + + import java.math.BigInteger; + + import org.apache.commons.lang3.Validate; + + /** + *

{@code Fraction} is a {@code Number} implementation that + * stores fractions accurately.

+ * + *

This class is immutable, and interoperable with most methods that accept + * a {@code Number}.

+ * + *

Note that this class is intended for common use cases, it is int + * based and thus suffers from various overflow issues. For a BigInteger based + * equivalent, please see the Commons Math BigFraction class.

+ * + * @since 2.0 + */ + public final class Fraction extends Number implements Comparable { + + /** + * Required for serialization support. Lang version 2.0. + * + * @see java.io.Serializable + */ + private static final long serialVersionUID = 65382027393090L; + + /** + * {@code Fraction} representation of 0. + */ + public static final Fraction ZERO = new Fraction(0, 1); + /** + * {@code Fraction} representation of 1. + */ + public static final Fraction ONE = new Fraction(1, 1); + /** + * {@code Fraction} representation of 1/2. + */ + public static final Fraction ONE_HALF = new Fraction(1, 2); + /** + * {@code Fraction} representation of 1/3. + */ + public static final Fraction ONE_THIRD = new Fraction(1, 3); + /** + * {@code Fraction} representation of 2/3. + */ + public static final Fraction TWO_THIRDS = new Fraction(2, 3); + /** + * {@code Fraction} representation of 1/4. + */ + public static final Fraction ONE_QUARTER = new Fraction(1, 4); + /** + * {@code Fraction} representation of 2/4. + */ + public static final Fraction TWO_QUARTERS = new Fraction(2, 4); + /** + * {@code Fraction} representation of 3/4. + */ + public static final Fraction THREE_QUARTERS = new Fraction(3, 4); + /** + * {@code Fraction} representation of 1/5. + */ + public static final Fraction ONE_FIFTH = new Fraction(1, 5); + /** + * {@code Fraction} representation of 2/5. + */ + public static final Fraction TWO_FIFTHS = new Fraction(2, 5); + /** + * {@code Fraction} representation of 3/5. + */ + public static final Fraction THREE_FIFTHS = new Fraction(3, 5); + /** + * {@code Fraction} representation of 4/5. + */ + public static final Fraction FOUR_FIFTHS = new Fraction(4, 5); + + + /** + * The numerator number part of the fraction (the three in three sevenths). + */ + private final int numerator; + /** + * The denominator number part of the fraction (the seven in three sevenths). + */ + private final int denominator; + + /** + * Cached output hashCode (class is immutable). + */ + private transient int hashCode = 0; + /** + * Cached output toString (class is immutable). + */ + private transient String toString = null; + /** + * Cached output toProperString (class is immutable). + */ + private transient String toProperString = null; + + /** + *

Constructs a {@code Fraction} instance with the 2 parts + * of a fraction Y/Z.

+ * + * @param numerator the numerator, for example the three in 'three sevenths' + * @param denominator the denominator, for example the seven in 'three sevenths' + */ + private Fraction(final int numerator, final int denominator) { + this.numerator = numerator; + this.denominator = denominator; + } + + /** + *

Creates a {@code Fraction} instance with the 2 parts + * of a fraction Y/Z.

+ * + *

Any negative signs are resolved to be on the numerator.

+ * + * @param numerator the numerator, for example the three in 'three sevenths' + * @param denominator the denominator, for example the seven in 'three sevenths' + * @return a new fraction instance + * @throws ArithmeticException if the denominator is {@code zero} + * or the denominator is {@code negative} and the numerator is {@code Integer#MIN_VALUE} + */ + public static Fraction getFraction(int numerator, int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (denominator < 0) { + if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate"); + } + numerator = -numerator; + denominator = -denominator; + } + return new Fraction(numerator, denominator); + } + + /** + *

Creates a {@code Fraction} instance with the 3 parts + * of a fraction X Y/Z.

+ * + *

The negative sign must be passed in on the whole number part.

+ * + * @param whole the whole number, for example the one in 'one and three sevenths' + * @param numerator the numerator, for example the three in 'one and three sevenths' + * @param denominator the denominator, for example the seven in 'one and three sevenths' + * @return a new fraction instance + * @throws ArithmeticException if the denominator is {@code zero} + * @throws ArithmeticException if the denominator is negative + * @throws ArithmeticException if the numerator is negative + * @throws ArithmeticException if the resulting numerator exceeds + * {@code Integer.MAX_VALUE} + */ + public static Fraction getFraction(final int whole, final int numerator, final int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (denominator < 0) { + throw new ArithmeticException("The denominator must not be negative"); + } + if (numerator < 0) { + throw new ArithmeticException("The numerator must not be negative"); + } + long numeratorValue; + if (whole < 0) { + numeratorValue = whole * (long) denominator - numerator; + } else { + numeratorValue = whole * (long) denominator + numerator; + } + if (numeratorValue < Integer.MIN_VALUE || numeratorValue > Integer.MAX_VALUE) { + throw new ArithmeticException("Numerator too large to represent as an Integer."); + } + return new Fraction((int) numeratorValue, denominator); + } + + /** + *

Creates a reduced {@code Fraction} instance with the 2 parts + * of a fraction Y/Z.

+ * + *

For example, if the input parameters represent 2/4, then the created + * fraction will be 1/2.

+ * + *

Any negative signs are resolved to be on the numerator.

+ * + * @param numerator the numerator, for example the three in 'three sevenths' + * @param denominator the denominator, for example the seven in 'three sevenths' + * @return a new fraction instance, with the numerator and denominator reduced + * @throws ArithmeticException if the denominator is {@code zero} + */ + public static Fraction getReducedFraction(int numerator, int denominator) { + if (denominator == 0) { + throw new ArithmeticException("The denominator must not be zero"); + } + if (numerator == 0) { + return ZERO; // normalize zero. + } + // allow 2^k/-2^31 as a valid fraction (where k>0) + if (denominator == Integer.MIN_VALUE && (numerator & 1) == 0) { + numerator /= 2; + denominator /= 2; + } + if (denominator < 0) { + if (numerator == Integer.MIN_VALUE || denominator == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate"); + } + numerator = -numerator; + denominator = -denominator; + } + // simplify fraction. + final int gcd = greatestCommonDivisor(numerator, denominator); + numerator /= gcd; + denominator /= gcd; + return new Fraction(numerator, denominator); + } + + /** + *

Creates a {@code Fraction} instance from a {@code double} value.

+ * + *

This method uses the + * continued fraction algorithm, computing a maximum of + * 25 convergents and bounding the denominator by 10,000.

+ * + * @param value the double value to convert + * @return a new fraction instance that is close to the value + * @throws ArithmeticException if {@code |value| > Integer.MAX_VALUE} + * or {@code value = NaN} + * @throws ArithmeticException if the calculated denominator is {@code zero} + * @throws ArithmeticException if the algorithm does not converge + */ + public static Fraction getFraction(double value) { + final int sign = value < 0 ? -1 : 1; + value = Math.abs(value); + if (value > Integer.MAX_VALUE || Double.isNaN(value)) { + throw new ArithmeticException("The value must not be greater than Integer.MAX_VALUE or NaN"); + } + final int wholeNumber = (int) value; + value -= wholeNumber; + + int numer0 = 0; // the pre-previous + int denom0 = 1; // the pre-previous + int numer1 = 1; // the previous + int denom1 = 0; // the previous + int numer2 = 0; // the current, setup in calculation + int denom2 = 0; // the current, setup in calculation + int a1 = (int) value; + int a2 = 0; + double x1 = 1; + double x2 = 0; + double y1 = value - a1; + double y2 = 0; + double delta1, delta2 = Double.MAX_VALUE; + double fraction; + int i = 1; + do { + delta1 = delta2; + a2 = (int) (x1 / y1); + x2 = y1; + y2 = x1 - a2 * y1; + numer2 = a1 * numer1 + numer0; + denom2 = a1 * denom1 + denom0; + fraction = (double) numer2 / (double) denom2; + delta2 = Math.abs(value - fraction); + a1 = a2; + x1 = x2; + y1 = y2; + numer0 = numer1; + denom0 = denom1; + numer1 = numer2; + denom1 = denom2; + i++; + } while (delta1 > delta2 && denom2 <= 10000 && denom2 > 0 && i < 25); + if (i == 25) { + throw new ArithmeticException("Unable to convert double to fraction"); + } + return getReducedFraction((numer0 + wholeNumber * denom0) * sign, denom0); + } + + /** + *

Creates a Fraction from a {@code String}.

+ * + *

The formats accepted are:

+ * + *
    + *
  1. {@code double} String containing a dot
  2. + *
  3. 'X Y/Z'
  4. + *
  5. 'Y/Z'
  6. + *
  7. 'X' (a simple whole number)
  8. + *
+ *

and a .

+ * + * @param str the string to parse, must not be {@code null} + * @return the new {@code Fraction} instance + * @throws NullPointerException if the string is {@code null} + * @throws NumberFormatException if the number format is invalid + */ + public static Fraction getFraction(String str) { + Validate.notNull(str, "str"); + // parse double format + int pos = str.indexOf('.'); + if (pos >= 0) { + return getFraction(Double.parseDouble(str)); + } + + // parse X Y/Z format + pos = str.indexOf(' '); + if (pos > 0) { + final int whole = Integer.parseInt(str.substring(0, pos)); + str = str.substring(pos + 1); + pos = str.indexOf('/'); + if (pos < 0) { + throw new NumberFormatException("The fraction could not be parsed as the format X Y/Z"); + } + final int numer = Integer.parseInt(str.substring(0, pos)); + final int denom = Integer.parseInt(str.substring(pos + 1)); + return getFraction(whole, numer, denom); + } + + // parse Y/Z format + pos = str.indexOf('/'); + if (pos < 0) { + // simple whole number + return getFraction(Integer.parseInt(str), 1); + } + final int numer = Integer.parseInt(str.substring(0, pos)); + final int denom = Integer.parseInt(str.substring(pos + 1)); + return getFraction(numer, denom); + } + + // Accessors + //------------------------------------------------------------------- + + /** + *

Gets the numerator part of the fraction.

+ * + *

This method may return a value greater than the denominator, an + * improper fraction, such as the seven in 7/4.

+ * + * @return the numerator fraction part + */ + public int getNumerator() { + return numerator; + } + + /** + *

Gets the denominator part of the fraction.

+ * + * @return the denominator fraction part + */ + public int getDenominator() { + return denominator; + } + + /** + *

Gets the proper numerator, always positive.

+ * + *

An improper fraction 7/4 can be resolved into a proper one, 1 3/4. + * This method returns the 3 from the proper fraction.

+ * + *

If the fraction is negative such as -7/4, it can be resolved into + * -1 3/4, so this method returns the positive proper numerator, 3.

+ * + * @return the numerator fraction part of a proper fraction, always positive + */ + public int getProperNumerator() { + return Math.abs(numerator % denominator); + } + + /** + *

Gets the proper whole part of the fraction.

+ * + *

An improper fraction 7/4 can be resolved into a proper one, 1 3/4. + * This method returns the 1 from the proper fraction.

+ * + *

If the fraction is negative such as -7/4, it can be resolved into + * -1 3/4, so this method returns the positive whole part -1.

+ * + * @return the whole fraction part of a proper fraction, that includes the sign + */ + public int getProperWhole() { + return numerator / denominator; + } + + // Number methods + //------------------------------------------------------------------- + + /** + *

Gets the fraction as an {@code int}. This returns the whole number + * part of the fraction.

+ * + * @return the whole number fraction part + */ + @Override + public int intValue() { + return numerator / denominator; + } + + /** + *

Gets the fraction as a {@code long}. This returns the whole number + * part of the fraction.

+ * + * @return the whole number fraction part + */ + @Override + public long longValue() { + return (long) numerator / denominator; + } + + /** + *

Gets the fraction as a {@code float}. This calculates the fraction + * as the numerator divided by denominator.

+ * + * @return the fraction as a {@code float} + */ + @Override + public float floatValue() { + return (float) numerator / (float) denominator; + } + + /** + *

Gets the fraction as a {@code double}. This calculates the fraction + * as the numerator divided by denominator.

+ * + * @return the fraction as a {@code double} + */ + @Override + public double doubleValue() { + return (double) numerator / (double) denominator; + } + + // Calculations + //------------------------------------------------------------------- + + /** + *

Reduce the fraction to the smallest values for the numerator and + * denominator, returning the result.

+ * + *

For example, if this fraction represents 2/4, then the result + * will be 1/2.

+ * + * @return a new reduced fraction instance, or this if no simplification possible + */ + public Fraction reduce() { + if (numerator == 0) { + return equals(ZERO) ? this : ZERO; + } + final int gcd = greatestCommonDivisor(Math.abs(numerator), denominator); + if (gcd == 1) { + return this; + } + return getFraction(numerator / gcd, denominator / gcd); + } + + /** + *

Gets a fraction that is the inverse (1/fraction) of this one.

+ * + *

The returned fraction is not reduced.

+ * + * @return a new fraction instance with the numerator and denominator + * inverted. + * @throws ArithmeticException if the fraction represents zero. + */ + public Fraction invert() { + if (numerator == 0) { + throw new ArithmeticException("Unable to invert zero."); + } + if (numerator==Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: can't negate numerator"); + } + if (numerator<0) { + return new Fraction(-denominator, -numerator); + } + return new Fraction(denominator, numerator); + } + + /** + *

Gets a fraction that is the negative (-fraction) of this one.

+ * + *

The returned fraction is not reduced.

+ * + * @return a new fraction instance with the opposite signed numerator + */ + public Fraction negate() { + // the positive range is one smaller than the negative range of an int. + if (numerator==Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: too large to negate"); + } + return new Fraction(-numerator, denominator); + } + + /** + *

Gets a fraction that is the positive equivalent of this one.

+ *

More precisely: {@code (fraction >= 0 ? this : -fraction)}

+ * + *

The returned fraction is not reduced.

+ * + * @return {@code this} if it is positive, or a new positive fraction + * instance with the opposite signed numerator + */ + public Fraction abs() { + if (numerator >= 0) { + return this; + } + return negate(); + } + + /** + *

Gets a fraction that is raised to the passed in power.

+ * + *

The returned fraction is in reduced form.

+ * + * @param power the power to raise the fraction to + * @return {@code this} if the power is one, {@code ONE} if the power + * is zero (even if the fraction equals ZERO) or a new fraction instance + * raised to the appropriate power + * @throws ArithmeticException if the resulting numerator or denominator exceeds + * {@code Integer.MAX_VALUE} + */ + public Fraction pow(final int power) { + if (power == 1) { + return this; + } else if (power == 0) { + return ONE; + } else if (power < 0) { + if (power == Integer.MIN_VALUE) { // MIN_VALUE can't be negated. + return this.invert().pow(2).pow(-(power / 2)); + } + return this.invert().pow(-power); + } else { + final Fraction f = this.multiplyBy(this); + if (power % 2 == 0) { // if even... + return f.pow(power / 2); + } + return f.pow(power / 2).multiplyBy(this); + } + } + + /** + *

Gets the greatest common divisor of the absolute value of + * two numbers, using the "binary gcd" method which avoids + * division and modulo operations. See Knuth 4.5.2 algorithm B. + * This algorithm is due to Josef Stein (1961).

+ * + * @param u a non-zero number + * @param v a non-zero number + * @return the greatest common divisor, never zero + */ + private static int greatestCommonDivisor(int u, int v) { + // From Commons Math: + if (u == 0 || v == 0) { + if (u == Integer.MIN_VALUE || v == Integer.MIN_VALUE) { + throw new ArithmeticException("overflow: gcd is 2^31"); + } + return Math.abs(u) + Math.abs(v); + } + // if either operand is abs 1, return 1: + if (Math.abs(u) == 1 || Math.abs(v) == 1) { + return 1; + } + // keep u and v negative, as negative integers range down to + // -2^31, while positive numbers can only be as large as 2^31-1 + // (i.e. we can't necessarily negate a negative number without + // overflow) + if (u > 0) { + u = -u; + } // make u negative + if (v > 0) { + v = -v; + } // make v negative + // B1. [Find power of 2] + int k = 0; + while ((u & 1) == 0 && (v & 1) == 0 && k < 31) { // while u and v are both even... + u /= 2; + v /= 2; + k++; // cast out twos. + } + if (k == 31) { + throw new ArithmeticException("overflow: gcd is 2^31"); + } + // B2. Initialize: u and v have been divided by 2^k and at least + // one is odd. + int t = (u & 1) == 1 ? v : -(u / 2)/* B3 */; + // t negative: u was odd, v may be even (t replaces v) + // t positive: u was even, v is odd (t replaces u) + do { + /* assert u<0 && v<0; */ + // B4/B3: cast out twos from t. + while ((t & 1) == 0) { // while t is even.. + t /= 2; // cast out twos + } + // B5 [reset max(u,v)] + if (t > 0) { + u = -t; + } else { + v = t; + } + // B6/B3. at this point both u and v should be odd. + t = (v - u) / 2; + // |u| larger: t positive (replace u) + // |v| larger: t negative (replace v) + } while (t != 0); + return -u * (1 << k); // gcd is u*2^k + } + + // Arithmetic + //------------------------------------------------------------------- + + /** + * Multiply two integers, checking for overflow. + * + * @param x a factor + * @param y a factor + * @return the product {@code x*y} + * @throws ArithmeticException if the result can not be represented as + * an int + */ + private static int mulAndCheck(final int x, final int y) { + final long m = (long) x * (long) y; + if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: mul"); + } + return (int) m; + } + + /** + * Multiply two non-negative integers, checking for overflow. + * + * @param x a non-negative factor + * @param y a non-negative factor + * @return the product {@code x*y} + * @throws ArithmeticException if the result can not be represented as + * an int + */ + private static int mulPosAndCheck(final int x, final int y) { + /* assert x>=0 && y>=0; */ + final long m = (long) x * (long) y; + if (m > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: mulPos"); + } + return (int) m; + } + + /** + * Add two integers, checking for overflow. + * + * @param x an addend + * @param y an addend + * @return the sum {@code x+y} + * @throws ArithmeticException if the result can not be represented as + * an int + */ + private static int addAndCheck(final int x, final int y) { + final long s = (long) x + (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: add"); + } + return (int) s; + } + + /** + * Subtract two integers, checking for overflow. + * + * @param x the minuend + * @param y the subtrahend + * @return the difference {@code x-y} + * @throws ArithmeticException if the result can not be represented as + * an int + */ + private static int subAndCheck(final int x, final int y) { + final long s = (long) x - (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: add"); + } + return (int) s; + } + + /** + *

Adds the value of this fraction to another, returning the result in reduced form. + * The algorithm follows Knuth, 4.5.1.

+ * + * @param fraction the fraction to add, must not be {@code null} + * @return a {@code Fraction} instance with the resulting values + * @throws IllegalArgumentException if the fraction is {@code null} + * @throws ArithmeticException if the resulting numerator or denominator exceeds + * {@code Integer.MAX_VALUE} + */ + public Fraction add(final Fraction fraction) { + return addSub(fraction, true /* add */); + } + + /** + *

Subtracts the value of another fraction from the value of this one, + * returning the result in reduced form.

+ * + * @param fraction the fraction to subtract, must not be {@code null} + * @return a {@code Fraction} instance with the resulting values + * @throws IllegalArgumentException if the fraction is {@code null} + * @throws ArithmeticException if the resulting numerator or denominator + * cannot be represented in an {@code int}. + */ + public Fraction subtract(final Fraction fraction) { + return addSub(fraction, false /* subtract */); + } + + /** + * Implement add and subtract using algorithm described in Knuth 4.5.1. + * + * @param fraction the fraction to subtract, must not be {@code null} + * @param isAdd true to add, false to subtract + * @return a {@code Fraction} instance with the resulting values + * @throws IllegalArgumentException if the fraction is {@code null} + * @throws ArithmeticException if the resulting numerator or denominator + * cannot be represented in an {@code int}. + */ + private Fraction addSub(final Fraction fraction, final boolean isAdd) { + Validate.notNull(fraction, "fraction"); + // zero is identity for addition. + if (numerator == 0) { + return isAdd ? fraction : fraction.negate(); + } + if (fraction.numerator == 0) { + return this; + } + // if denominators are randomly distributed, d1 will be 1 about 61% + // of the time. + final int d1 = greatestCommonDivisor(denominator, fraction.denominator); + if (d1 == 1) { + // result is ( (u*v' +/- u'v) / u'v') + final int uvp = mulAndCheck(numerator, fraction.denominator); + final int upv = mulAndCheck(fraction.numerator, denominator); + return new Fraction(isAdd ? addAndCheck(uvp, upv) : subAndCheck(uvp, upv), mulPosAndCheck(denominator, + fraction.denominator)); + } + // the quantity 't' requires 65 bits of precision; see knuth 4.5.1 + // exercise 7. we're going to use a BigInteger. + // t = u(v'/d1) +/- v(u'/d1) + final BigInteger uvp = BigInteger.valueOf(numerator).multiply(BigInteger.valueOf(fraction.denominator / d1)); + final BigInteger upv = BigInteger.valueOf(fraction.numerator).multiply(BigInteger.valueOf(denominator / d1)); + final BigInteger t = isAdd ? uvp.add(upv) : uvp.subtract(upv); + // but d2 doesn't need extra precision because + // d2 = gcd(t,d1) = gcd(t mod d1, d1) + final int tmodd1 = t.mod(BigInteger.valueOf(d1)).intValue(); + final int d2 = tmodd1 == 0 ? d1 : greatestCommonDivisor(tmodd1, d1); + + // result is (t/d2) / (u'/d1)(v'/d2) + final BigInteger w = t.divide(BigInteger.valueOf(d2)); + if (w.bitLength() > 31) { + throw new ArithmeticException("overflow: numerator too large after multiply"); + } + return new Fraction(w.intValue(), mulPosAndCheck(denominator / d1, fraction.denominator / d2)); + } + + /** + *

Multiplies the value of this fraction by another, returning the + * result in reduced form.

+ * + * @param fraction the fraction to multiply by, must not be {@code null} + * @return a {@code Fraction} instance with the resulting values + * @throws NullPointerException if the fraction is {@code null} + * @throws ArithmeticException if the resulting numerator or denominator exceeds + * {@code Integer.MAX_VALUE} + */ + public Fraction multiplyBy(final Fraction fraction) { + Validate.notNull(fraction, "fraction"); + if (numerator == 0 || fraction.numerator == 0) { + return ZERO; + } + // knuth 4.5.1 + // make sure we don't overflow unless the result *must* overflow. + final int d1 = greatestCommonDivisor(numerator, fraction.denominator); + final int d2 = greatestCommonDivisor(fraction.numerator, denominator); + return getReducedFraction(mulAndCheck(numerator / d1, fraction.numerator / d2), + mulPosAndCheck(denominator / d2, fraction.denominator / d1)); + } + + /** + *

Divide the value of this fraction by another.

+ * + * @param fraction the fraction to divide by, must not be {@code null} + * @return a {@code Fraction} instance with the resulting values + * @throws NullPointerException if the fraction is {@code null} + * @throws ArithmeticException if the fraction to divide by is zero + * @throws ArithmeticException if the resulting numerator or denominator exceeds + * {@code Integer.MAX_VALUE} + */ + public Fraction divideBy(final Fraction fraction) { + Validate.notNull(fraction, "fraction"); + if (fraction.numerator == 0) { + throw new ArithmeticException("The fraction to divide by must not be zero"); + } + return multiplyBy(fraction.invert()); + } + + // Basics + //------------------------------------------------------------------- + + /** + *

Compares this fraction to another object to test if they are equal.

. + * + *

To be equal, both values must be equal. Thus 2/4 is not equal to 1/2.

+ * + * @param obj the reference object with which to compare + * @return {@code true} if this object is equal + */ + @Override + public boolean equals(final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Fraction)) { + return false; + } + final Fraction other = (Fraction) obj; + return getNumerator() == other.getNumerator() && getDenominator() == other.getDenominator(); + } + + /** + *

Gets a hashCode for the fraction.

+ * + * @return a hash code value for this object + */ + @Override + public int hashCode() { + if (hashCode == 0) { + // hash code update should be atomic. + hashCode = 37 * (37 * 17 + getNumerator()) + getDenominator(); + } + return hashCode; + } + + /** + *

Compares this object to another based on size.

+ * + *

Note: this class has a natural ordering that is inconsistent + * with equals, because, for example, equals treats 1/2 and 2/4 as + * different, whereas compareTo treats them as equal. + * + * @param other the object to compare to + * @return -1 if this is less, 0 if equal, +1 if greater + * @throws ClassCastException if the object is not a {@code Fraction} + * @throws NullPointerException if the object is {@code null} + */ + @Override + public int compareTo(final Fraction other) { + if (this == other) { + return 0; + } + if (numerator == other.numerator && denominator == other.denominator) { + return 0; + } + + // otherwise see which is less + final long first = (long) numerator * (long) other.denominator; + final long second = (long) other.numerator * (long) denominator; + return Long.compare(first, second); + } + + /** + *

Gets the fraction as a {@code String}.

+ * + *

The format used is 'numerator/denominator' always. + * + * @return a {@code String} form of the fraction + */ + @Override + public String toString() { + if (toString == null) { + toString = getNumerator() + "/" + getDenominator(); + } + return toString; + } + + /** + *

Gets the fraction as a proper {@code String} in the format X Y/Z.

+ * + *

The format used in 'wholeNumber numerator/denominator'. + * If the whole number is zero it will be omitted. If the numerator is zero, + * only the whole number is returned.

+ * + * @return a {@code String} form of the fraction + */ + public String toProperString() { + if (toProperString == null) { + if (numerator == 0) { + toProperString = "0"; + } else if (numerator == denominator) { + toProperString = "1"; + } else if (numerator == -1 * denominator) { + toProperString = "-1"; + } else if ((numerator > 0 ? -numerator : numerator) < -denominator) { + // note that we do the magnitude comparison test above with + // NEGATIVE (not positive) numbers, since negative numbers + // have a larger range. otherwise numerator==Integer.MIN_VALUE + // is handled incorrectly. + final int properNumerator = getProperNumerator(); + if (properNumerator == 0) { + toProperString = Integer.toString(getProperWhole()); + } else { + toProperString = getProperWhole() + " " + properNumerator + "/" + getDenominator(); + } + } else { + toProperString = getNumerator() + "/" + getDenominator(); + } + } + return toProperString; + } + } + """.trimIndent() + + private val fractionTest = """ + /* + * 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.apache.commons.lang3.math; + + import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.junit.jupiter.api.Assertions.assertNotEquals; + import static org.junit.jupiter.api.Assertions.assertSame; + import static org.junit.jupiter.api.Assertions.assertThrows; + import static org.junit.jupiter.api.Assertions.assertTrue; + + import org.junit.jupiter.api.Test; + + /** + * Test cases for the {@link Fraction} class + */ + public class FractionTest { + + private static final int SKIP = 500; //53 + + //-------------------------------------------------------------------------- + @Test + public void testConstants() { + assertEquals(0, Fraction.ZERO.getNumerator()); + assertEquals(1, Fraction.ZERO.getDenominator()); + + assertEquals(1, Fraction.ONE.getNumerator()); + assertEquals(1, Fraction.ONE.getDenominator()); + + assertEquals(1, Fraction.ONE_HALF.getNumerator()); + assertEquals(2, Fraction.ONE_HALF.getDenominator()); + + assertEquals(1, Fraction.ONE_THIRD.getNumerator()); + assertEquals(3, Fraction.ONE_THIRD.getDenominator()); + + assertEquals(2, Fraction.TWO_THIRDS.getNumerator()); + assertEquals(3, Fraction.TWO_THIRDS.getDenominator()); + + assertEquals(1, Fraction.ONE_QUARTER.getNumerator()); + assertEquals(4, Fraction.ONE_QUARTER.getDenominator()); + + assertEquals(2, Fraction.TWO_QUARTERS.getNumerator()); + assertEquals(4, Fraction.TWO_QUARTERS.getDenominator()); + + assertEquals(3, Fraction.THREE_QUARTERS.getNumerator()); + assertEquals(4, Fraction.THREE_QUARTERS.getDenominator()); + + assertEquals(1, Fraction.ONE_FIFTH.getNumerator()); + assertEquals(5, Fraction.ONE_FIFTH.getDenominator()); + + assertEquals(2, Fraction.TWO_FIFTHS.getNumerator()); + assertEquals(5, Fraction.TWO_FIFTHS.getDenominator()); + + assertEquals(3, Fraction.THREE_FIFTHS.getNumerator()); + assertEquals(5, Fraction.THREE_FIFTHS.getDenominator()); + + assertEquals(4, Fraction.FOUR_FIFTHS.getNumerator()); + assertEquals(5, Fraction.FOUR_FIFTHS.getDenominator()); + } + + @Test + public void testFactory_int_int() { + Fraction f; + + // zero + f = Fraction.getFraction(0, 1); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction(0, 2); + assertEquals(0, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + // normal + f = Fraction.getFraction(1, 1); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction(2, 1); + assertEquals(2, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction(23, 345); + assertEquals(23, f.getNumerator()); + assertEquals(345, f.getDenominator()); + + // improper + f = Fraction.getFraction(22, 7); + assertEquals(22, f.getNumerator()); + assertEquals(7, f.getDenominator()); + + // negatives + f = Fraction.getFraction(-6, 10); + assertEquals(-6, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + f = Fraction.getFraction(6, -10); + assertEquals(-6, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + f = Fraction.getFraction(-6, -10); + assertEquals(6, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + // zero denominator + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(2, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-3, 0)); + + // very large: can't represent as unsimplified fraction, although + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(4, Integer.MIN_VALUE)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, Integer.MIN_VALUE)); + } + + @Test + public void testFactory_int_int_int() { + Fraction f; + + // zero + f = Fraction.getFraction(0, 0, 2); + assertEquals(0, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction(2, 0, 2); + assertEquals(4, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction(0, 1, 2); + assertEquals(1, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + // normal + f = Fraction.getFraction(1, 1, 2); + assertEquals(3, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + // negatives + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, -6, -10)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, -6, -10)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, -6, -10)); + + // negative whole + f = Fraction.getFraction(-1, 6, 10); + assertEquals(-16, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-1, -6, 10)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-1, 6, -10)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-1, -6, -10)); + + // zero denominator + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(0, 1, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, 2, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-1, -3, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MAX_VALUE, 1, 2)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-Integer.MAX_VALUE, 1, 2)); + + // very large + f = Fraction.getFraction(-1, 0, Integer.MAX_VALUE); + assertEquals(-Integer.MAX_VALUE, f.getNumerator()); + assertEquals(Integer.MAX_VALUE, f.getDenominator()); + + // negative denominators not allowed in this constructor. + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(0, 4, Integer.MIN_VALUE)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(1, 1, Integer.MAX_VALUE)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(-1, 2, Integer.MAX_VALUE)); + } + + @Test + public void testReducedFactory_int_int() { + Fraction f; + + // zero + f = Fraction.getReducedFraction(0, 1); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // normal + f = Fraction.getReducedFraction(1, 1); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getReducedFraction(2, 1); + assertEquals(2, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // improper + f = Fraction.getReducedFraction(22, 7); + assertEquals(22, f.getNumerator()); + assertEquals(7, f.getDenominator()); + + // negatives + f = Fraction.getReducedFraction(-6, 10); + assertEquals(-3, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f = Fraction.getReducedFraction(6, -10); + assertEquals(-3, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f = Fraction.getReducedFraction(-6, -10); + assertEquals(3, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + // zero denominator + assertThrows(ArithmeticException.class, () -> Fraction.getReducedFraction(1, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getReducedFraction(2, 0)); + assertThrows(ArithmeticException.class, () -> Fraction.getReducedFraction(-3, 0)); + + // reduced + f = Fraction.getReducedFraction(0, 2); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getReducedFraction(2, 2); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getReducedFraction(2, 4); + assertEquals(1, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getReducedFraction(15, 10); + assertEquals(3, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getReducedFraction(121, 22); + assertEquals(11, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + // Extreme values + // OK, can reduce before negating + f = Fraction.getReducedFraction(-2, Integer.MIN_VALUE); + assertEquals(1, f.getNumerator()); + assertEquals(-(Integer.MIN_VALUE / 2), f.getDenominator()); + + // Can't reduce, negation will throw + assertThrows(ArithmeticException.class, () -> Fraction.getReducedFraction(-7, Integer.MIN_VALUE)); + + // LANG-662 + f = Fraction.getReducedFraction(Integer.MIN_VALUE, 2); + assertEquals(Integer.MIN_VALUE / 2, f.getNumerator()); + assertEquals(1, f.getDenominator()); + } + + @Test + public void testFactory_double() { + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Double.NaN)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Double.POSITIVE_INFINITY)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Double.NEGATIVE_INFINITY)); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction((double) Integer.MAX_VALUE + 1)); + + // zero + Fraction f = Fraction.getFraction(0.0d); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // one + f = Fraction.getFraction(1.0d); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // one half + f = Fraction.getFraction(0.5d); + assertEquals(1, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + // negative + f = Fraction.getFraction(-0.875d); + assertEquals(-7, f.getNumerator()); + assertEquals(8, f.getDenominator()); + + // over 1 + f = Fraction.getFraction(1.25d); + assertEquals(5, f.getNumerator()); + assertEquals(4, f.getDenominator()); + + // two thirds + f = Fraction.getFraction(0.66666d); + assertEquals(2, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + // small + f = Fraction.getFraction(1.0d/10001d); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // normal + Fraction f2 = null; + for (int i = 1; i <= 100; i++) { // denominator + for (int j = 1; j <= i; j++) { // numerator + f = Fraction.getFraction((double) j / (double) i); + + f2 = Fraction.getReducedFraction(j, i); + assertEquals(f2.getNumerator(), f.getNumerator()); + assertEquals(f2.getDenominator(), f.getDenominator()); + } + } + // save time by skipping some tests! ( + for (int i = 1001; i <= 10000; i+=SKIP) { // denominator + for (int j = 1; j <= i; j++) { // numerator + f = Fraction.getFraction((double) j / (double) i); + f2 = Fraction.getReducedFraction(j, i); + assertEquals(f2.getNumerator(), f.getNumerator()); + assertEquals(f2.getDenominator(), f.getDenominator()); + } + } + } + + @Test + public void testFactory_String() { + assertThrows(NullPointerException.class, () -> Fraction.getFraction(null)); + } + + + @Test + public void testFactory_String_double() { + Fraction f; + + f = Fraction.getFraction("0.0"); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction("0.2"); + assertEquals(1, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f = Fraction.getFraction("0.5"); + assertEquals(1, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction("0.66666"); + assertEquals(2, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2.3R")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2147483648")); // too big + assertThrows(NumberFormatException.class, () -> Fraction.getFraction(".")); + } + + @Test + public void testFactory_String_proper() { + Fraction f; + + f = Fraction.getFraction("0 0/1"); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction("1 1/5"); + assertEquals(6, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f = Fraction.getFraction("7 1/2"); + assertEquals(15, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction("1 2/4"); + assertEquals(6, f.getNumerator()); + assertEquals(4, f.getDenominator()); + + f = Fraction.getFraction("-7 1/2"); + assertEquals(-15, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction("-1 2/4"); + assertEquals(-6, f.getNumerator()); + assertEquals(4, f.getDenominator()); + + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2 3")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("a 3")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2 b/4")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2 ")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction(" 3")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction(" ")); + } + + @Test + public void testFactory_String_improper() { + Fraction f; + + f = Fraction.getFraction("0/1"); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction("1/5"); + assertEquals(1, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f = Fraction.getFraction("1/2"); + assertEquals(1, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + f = Fraction.getFraction("2/3"); + assertEquals(2, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + f = Fraction.getFraction("7/3"); + assertEquals(7, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + f = Fraction.getFraction("2/4"); + assertEquals(2, f.getNumerator()); + assertEquals(4, f.getDenominator()); + + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2/d")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2e/3")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("2/")); + assertThrows(NumberFormatException.class, () -> Fraction.getFraction("/")); + } + + @Test + public void testGets() { + Fraction f; + + f = Fraction.getFraction(3, 5, 6); + assertEquals(23, f.getNumerator()); + assertEquals(3, f.getProperWhole()); + assertEquals(5, f.getProperNumerator()); + assertEquals(6, f.getDenominator()); + + f = Fraction.getFraction(-3, 5, 6); + assertEquals(-23, f.getNumerator()); + assertEquals(-3, f.getProperWhole()); + assertEquals(5, f.getProperNumerator()); + assertEquals(6, f.getDenominator()); + + f = Fraction.getFraction(Integer.MIN_VALUE, 0, 1); + assertEquals(Integer.MIN_VALUE, f.getNumerator()); + assertEquals(Integer.MIN_VALUE, f.getProperWhole()); + assertEquals(0, f.getProperNumerator()); + assertEquals(1, f.getDenominator()); + } + + @Test + public void testConversions() { + Fraction f; + + f = Fraction.getFraction(3, 7, 8); + assertEquals(3, f.intValue()); + assertEquals(3L, f.longValue()); + assertEquals(3.875f, f.floatValue(), 0.00001f); + assertEquals(3.875d, f.doubleValue(), 0.00001d); + } + + @Test + public void testReduce() { + Fraction f; + + f = Fraction.getFraction(50, 75); + Fraction result = f.reduce(); + assertEquals(2, result.getNumerator()); + assertEquals(3, result.getDenominator()); + + f = Fraction.getFraction(-2, -3); + result = f.reduce(); + assertEquals(2, result.getNumerator()); + assertEquals(3, result.getDenominator()); + + f = Fraction.getFraction(2, -3); + result = f.reduce(); + assertEquals(-2, result.getNumerator()); + assertEquals(3, result.getDenominator()); + + f = Fraction.getFraction(-2, 3); + result = f.reduce(); + assertEquals(-2, result.getNumerator()); + assertEquals(3, result.getDenominator()); + assertSame(f, result); + + f = Fraction.getFraction(2, 3); + result = f.reduce(); + assertEquals(2, result.getNumerator()); + assertEquals(3, result.getDenominator()); + assertSame(f, result); + + f = Fraction.getFraction(0, 1); + result = f.reduce(); + assertEquals(0, result.getNumerator()); + assertEquals(1, result.getDenominator()); + assertSame(f, result); + + f = Fraction.getFraction(0, 100); + result = f.reduce(); + assertEquals(0, result.getNumerator()); + assertEquals(1, result.getDenominator()); + assertSame(result, Fraction.ZERO); + + f = Fraction.getFraction(Integer.MIN_VALUE, 2); + result = f.reduce(); + assertEquals(Integer.MIN_VALUE / 2, result.getNumerator()); + assertEquals(1, result.getDenominator()); + } + + @Test + public void testInvert() { + Fraction f; + + f = Fraction.getFraction(50, 75); + f = f.invert(); + assertEquals(75, f.getNumerator()); + assertEquals(50, f.getDenominator()); + + f = Fraction.getFraction(4, 3); + f = f.invert(); + assertEquals(3, f.getNumerator()); + assertEquals(4, f.getDenominator()); + + f = Fraction.getFraction(-15, 47); + f = f.invert(); + assertEquals(-47, f.getNumerator()); + assertEquals(15, f.getDenominator()); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(0, 3).invert()); + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MIN_VALUE, 1).invert()); + + f = Fraction.getFraction(Integer.MAX_VALUE, 1); + f = f.invert(); + assertEquals(1, f.getNumerator()); + assertEquals(Integer.MAX_VALUE, f.getDenominator()); + } + + @Test + public void testNegate() { + Fraction f; + + f = Fraction.getFraction(50, 75); + f = f.negate(); + assertEquals(-50, f.getNumerator()); + assertEquals(75, f.getDenominator()); + + f = Fraction.getFraction(-50, 75); + f = f.negate(); + assertEquals(50, f.getNumerator()); + assertEquals(75, f.getDenominator()); + + // large values + f = Fraction.getFraction(Integer.MAX_VALUE-1, Integer.MAX_VALUE); + f = f.negate(); + assertEquals(Integer.MIN_VALUE+2, f.getNumerator()); + assertEquals(Integer.MAX_VALUE, f.getDenominator()); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MIN_VALUE, 1).negate()); + } + + @Test + public void testAbs() { + Fraction f; + + f = Fraction.getFraction(50, 75); + f = f.abs(); + assertEquals(50, f.getNumerator()); + assertEquals(75, f.getDenominator()); + + f = Fraction.getFraction(-50, 75); + f = f.abs(); + assertEquals(50, f.getNumerator()); + assertEquals(75, f.getDenominator()); + + f = Fraction.getFraction(Integer.MAX_VALUE, 1); + f = f.abs(); + assertEquals(Integer.MAX_VALUE, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f = Fraction.getFraction(Integer.MAX_VALUE, -1); + f = f.abs(); + assertEquals(Integer.MAX_VALUE, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MIN_VALUE, 1).abs()); + } + + @Test + public void testPow() { + Fraction f; + + f = Fraction.getFraction(3, 5); + assertEquals(Fraction.ONE, f.pow(0)); + + f = Fraction.getFraction(3, 5); + assertSame(f, f.pow(1)); + assertEquals(f, f.pow(1)); + + f = Fraction.getFraction(3, 5); + f = f.pow(2); + assertEquals(9, f.getNumerator()); + assertEquals(25, f.getDenominator()); + + f = Fraction.getFraction(3, 5); + f = f.pow(3); + assertEquals(27, f.getNumerator()); + assertEquals(125, f.getDenominator()); + + f = Fraction.getFraction(3, 5); + f = f.pow(-1); + assertEquals(5, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + f = Fraction.getFraction(3, 5); + f = f.pow(-2); + assertEquals(25, f.getNumerator()); + assertEquals(9, f.getDenominator()); + + // check unreduced fractions stay that way. + f = Fraction.getFraction(6, 10); + assertEquals(Fraction.ONE, f.pow(0)); + + f = Fraction.getFraction(6, 10); + assertEquals(f, f.pow(1)); + assertNotEquals(f.pow(1), Fraction.getFraction(3, 5)); + + f = Fraction.getFraction(6, 10); + f = f.pow(2); + assertEquals(9, f.getNumerator()); + assertEquals(25, f.getDenominator()); + + f = Fraction.getFraction(6, 10); + f = f.pow(3); + assertEquals(27, f.getNumerator()); + assertEquals(125, f.getDenominator()); + + f = Fraction.getFraction(6, 10); + f = f.pow(-1); + assertEquals(10, f.getNumerator()); + assertEquals(6, f.getDenominator()); + + f = Fraction.getFraction(6, 10); + f = f.pow(-2); + assertEquals(25, f.getNumerator()); + assertEquals(9, f.getDenominator()); + + // zero to any positive power is still zero. + f = Fraction.getFraction(0, 1231); + f = f.pow(1); + assertEquals(0, f.compareTo(Fraction.ZERO)); + assertEquals(0, f.getNumerator()); + assertEquals(1231, f.getDenominator()); + f = f.pow(2); + assertEquals(0, f.compareTo(Fraction.ZERO)); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // zero to negative powers should throw an exception + final Fraction fr = f; + assertThrows(ArithmeticException.class, () -> fr.pow(-1)); + assertThrows(ArithmeticException.class, () -> fr.pow(Integer.MIN_VALUE)); + + // one to any power is still one. + f = Fraction.getFraction(1, 1); + f = f.pow(0); + assertEquals(f, Fraction.ONE); + f = f.pow(1); + assertEquals(f, Fraction.ONE); + f = f.pow(-1); + assertEquals(f, Fraction.ONE); + f = f.pow(Integer.MAX_VALUE); + assertEquals(f, Fraction.ONE); + f = f.pow(Integer.MIN_VALUE); + assertEquals(f, Fraction.ONE); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MAX_VALUE, 1).pow(2)); + + // Numerator growing too negative during the pow operation. + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(Integer.MIN_VALUE, 1).pow(3)); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(65536, 1).pow(2)); + } + + @Test + public void testAdd() { + Fraction f; + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(1, 5); + f = f1.add(f2); + assertEquals(4, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(2, 5); + f = f1.add(f2); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(3, 5); + f = f1.add(f2); + assertEquals(6, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(-4, 5); + f = f1.add(f2); + assertEquals(-1, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MAX_VALUE - 1, 1); + f2 = Fraction.ONE; + f = f1.add(f2); + assertEquals(Integer.MAX_VALUE, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(1, 2); + f = f1.add(f2); + assertEquals(11, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + f1 = Fraction.getFraction(3, 8); + f2 = Fraction.getFraction(1, 6); + f = f1.add(f2); + assertEquals(13, f.getNumerator()); + assertEquals(24, f.getDenominator()); + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(1, 5); + f = f1.add(f2); + assertSame(f2, f); + f = f2.add(f1); + assertSame(f2, f); + + f1 = Fraction.getFraction(-1, 13*13*2*2); + f2 = Fraction.getFraction(-2, 13*17*2); + final Fraction fr = f1.add(f2); + assertEquals(13*13*17*2*2, fr.getDenominator()); + assertEquals(-17 - 2*13*2, fr.getNumerator()); + + assertThrows(NullPointerException.class, () -> fr.add(null)); + + // if this fraction is added naively, it will overflow. + // check that it doesn't. + f1 = Fraction.getFraction(1, 32768*3); + f2 = Fraction.getFraction(1, 59049); + f = f1.add(f2); + assertEquals(52451, f.getNumerator()); + assertEquals(1934917632, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MIN_VALUE, 3); + f2 = Fraction.ONE_THIRD; + f = f1.add(f2); + assertEquals(Integer.MIN_VALUE+1, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MAX_VALUE - 1, 1); + f2 = Fraction.ONE; + f = f1.add(f2); + assertEquals(Integer.MAX_VALUE, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + final Fraction overflower = f; + assertThrows(ArithmeticException.class, () -> overflower.add(Fraction.ONE)); // should overflow + + // denominator should not be a multiple of 2 or 3 to trigger overflow + assertThrows( + ArithmeticException.class, + () -> Fraction.getFraction(Integer.MIN_VALUE, 5).add(Fraction.getFraction(-1, 5))); + + final Fraction maxValue = Fraction.getFraction(-Integer.MAX_VALUE, 1); + assertThrows(ArithmeticException.class, () -> maxValue.add(maxValue)); + + final Fraction negativeMaxValue = Fraction.getFraction(-Integer.MAX_VALUE, 1); + assertThrows(ArithmeticException.class, () -> negativeMaxValue.add(negativeMaxValue)); + + final Fraction f3 = Fraction.getFraction(3, 327680); + final Fraction f4 = Fraction.getFraction(2, 59049); + assertThrows(ArithmeticException.class, () -> f3.add(f4)); // should overflow + } + + @Test + public void testSubtract() { + Fraction f; + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(1, 5); + f = f1.subtract(f2); + assertEquals(2, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(7, 5); + f2 = Fraction.getFraction(2, 5); + f = f1.subtract(f2); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(3, 5); + f = f1.subtract(f2); + assertEquals(0, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(-4, 5); + f = f1.subtract(f2); + assertEquals(7, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(4, 5); + f = f1.subtract(f2); + assertEquals(-4, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(-4, 5); + f = f1.subtract(f2); + assertEquals(4, f.getNumerator()); + assertEquals(5, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(1, 2); + f = f1.subtract(f2); + assertEquals(1, f.getNumerator()); + assertEquals(10, f.getDenominator()); + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(1, 5); + f = f2.subtract(f1); + assertSame(f2, f); + + final Fraction fr = f; + assertThrows(NullPointerException.class, () -> fr.subtract(null)); + + // if this fraction is subtracted naively, it will overflow. + // check that it doesn't. + f1 = Fraction.getFraction(1, 32768*3); + f2 = Fraction.getFraction(1, 59049); + f = f1.subtract(f2); + assertEquals(-13085, f.getNumerator()); + assertEquals(1934917632, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MIN_VALUE, 3); + f2 = Fraction.ONE_THIRD.negate(); + f = f1.subtract(f2); + assertEquals(Integer.MIN_VALUE+1, f.getNumerator()); + assertEquals(3, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MAX_VALUE, 1); + f2 = Fraction.ONE; + f = f1.subtract(f2); + assertEquals(Integer.MAX_VALUE-1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + // Should overflow + assertThrows( + ArithmeticException.class, + () -> Fraction.getFraction(1, Integer.MAX_VALUE).subtract(Fraction.getFraction(1, Integer.MAX_VALUE - 1))); + f = f1.subtract(f2); + + // denominator should not be a multiple of 2 or 3 to trigger overflow + assertThrows( + ArithmeticException.class, + () -> Fraction.getFraction(Integer.MIN_VALUE, 5).subtract(Fraction.getFraction(1, 5))); + + assertThrows( + ArithmeticException.class, () -> Fraction.getFraction(Integer.MIN_VALUE, 1).subtract(Fraction.ONE)); + + assertThrows( + ArithmeticException.class, + () -> Fraction.getFraction(Integer.MAX_VALUE, 1).subtract(Fraction.ONE.negate())); + + // Should overflow + assertThrows( + ArithmeticException.class, + () -> Fraction.getFraction(3, 327680).subtract(Fraction.getFraction(2, 59049))); + } + + @Test + public void testMultiply() { + Fraction f; + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(2, 5); + f = f1.multiplyBy(f2); + assertEquals(6, f.getNumerator()); + assertEquals(25, f.getDenominator()); + + f1 = Fraction.getFraction(6, 10); + f2 = Fraction.getFraction(6, 10); + f = f1.multiplyBy(f2); + assertEquals(9, f.getNumerator()); + assertEquals(25, f.getDenominator()); + f = f.multiplyBy(f2); + assertEquals(27, f.getNumerator()); + assertEquals(125, f.getDenominator()); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(-2, 5); + f = f1.multiplyBy(f2); + assertEquals(-6, f.getNumerator()); + assertEquals(25, f.getDenominator()); + + f1 = Fraction.getFraction(-3, 5); + f2 = Fraction.getFraction(-2, 5); + f = f1.multiplyBy(f2); + assertEquals(6, f.getNumerator()); + assertEquals(25, f.getDenominator()); + + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(2, 7); + f = f1.multiplyBy(f2); + assertSame(Fraction.ZERO, f); + + f1 = Fraction.getFraction(2, 7); + f2 = Fraction.ONE; + f = f1.multiplyBy(f2); + assertEquals(2, f.getNumerator()); + assertEquals(7, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MAX_VALUE, 1); + f2 = Fraction.getFraction(Integer.MIN_VALUE, Integer.MAX_VALUE); + f = f1.multiplyBy(f2); + assertEquals(Integer.MIN_VALUE, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + final Fraction fr = f; + assertThrows(NullPointerException.class, () -> fr.multiplyBy(null)); + + final Fraction fr1 = Fraction.getFraction(1, Integer.MAX_VALUE); + assertThrows(ArithmeticException.class, () -> fr1.multiplyBy(fr1)); + + final Fraction fr2 = Fraction.getFraction(1, -Integer.MAX_VALUE); + assertThrows(ArithmeticException.class, () -> fr2.multiplyBy(fr2)); + } + + @Test + public void testDivide() { + Fraction f; + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(2, 5); + f = f1.divideBy(f2); + assertEquals(3, f.getNumerator()); + assertEquals(2, f.getDenominator()); + + assertThrows(ArithmeticException.class, () -> Fraction.getFraction(3, 5).divideBy(Fraction.ZERO)); + + f1 = Fraction.getFraction(0, 5); + f2 = Fraction.getFraction(2, 7); + f = f1.divideBy(f2); + assertSame(Fraction.ZERO, f); + + f1 = Fraction.getFraction(2, 7); + f2 = Fraction.ONE; + f = f1.divideBy(f2); + assertEquals(2, f.getNumerator()); + assertEquals(7, f.getDenominator()); + + f1 = Fraction.getFraction(1, Integer.MAX_VALUE); + f = f1.divideBy(f1); + assertEquals(1, f.getNumerator()); + assertEquals(1, f.getDenominator()); + + f1 = Fraction.getFraction(Integer.MIN_VALUE, Integer.MAX_VALUE); + f2 = Fraction.getFraction(1, Integer.MAX_VALUE); + final Fraction fr = f1.divideBy(f2); + assertEquals(Integer.MIN_VALUE, fr.getNumerator()); + assertEquals(1, fr.getDenominator()); + + assertThrows(NullPointerException.class, () -> fr.divideBy(null)); + + final Fraction smallest = Fraction.getFraction(1, Integer.MAX_VALUE); + assertThrows(ArithmeticException.class, () -> smallest.divideBy(smallest.invert())); // Should overflow + + final Fraction negative = Fraction.getFraction(1, -Integer.MAX_VALUE); + assertThrows(ArithmeticException.class, () -> negative.divideBy(negative.invert())); // Should overflow + } + + @Test + public void testEquals() { + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + assertNotEquals(null, f1); + assertNotEquals(f1, new Object()); + assertNotEquals(f1, Integer.valueOf(6)); + + f1 = Fraction.getFraction(3, 5); + f2 = Fraction.getFraction(2, 5); + assertNotEquals(f1, f2); + assertEquals(f1, f1); + assertEquals(f2, f2); + + f2 = Fraction.getFraction(3, 5); + assertEquals(f1, f2); + + f2 = Fraction.getFraction(6, 10); + assertNotEquals(f1, f2); + } + + @Test + public void testHashCode() { + final Fraction f1 = Fraction.getFraction(3, 5); + Fraction f2 = Fraction.getFraction(3, 5); + + assertEquals(f1.hashCode(), f2.hashCode()); + + f2 = Fraction.getFraction(2, 5); + assertTrue(f1.hashCode() != f2.hashCode()); + + f2 = Fraction.getFraction(6, 10); + assertTrue(f1.hashCode() != f2.hashCode()); + } + + @Test + public void testCompareTo() { + Fraction f1; + Fraction f2; + + f1 = Fraction.getFraction(3, 5); + assertEquals(0, f1.compareTo(f1)); + + final Fraction fr = f1; + assertThrows(NullPointerException.class, () -> fr.compareTo(null)); + + f2 = Fraction.getFraction(2, 5); + assertTrue(f1.compareTo(f2) > 0); + assertEquals(0, f2.compareTo(f2)); + + f2 = Fraction.getFraction(4, 5); + assertTrue(f1.compareTo(f2) < 0); + assertEquals(0, f2.compareTo(f2)); + + f2 = Fraction.getFraction(3, 5); + assertEquals(0, f1.compareTo(f2)); + assertEquals(0, f2.compareTo(f2)); + + f2 = Fraction.getFraction(6, 10); + assertEquals(0, f1.compareTo(f2)); + assertEquals(0, f2.compareTo(f2)); + + f2 = Fraction.getFraction(-1, 1, Integer.MAX_VALUE); + assertTrue(f1.compareTo(f2) > 0); + assertEquals(0, f2.compareTo(f2)); + + } + + @Test + public void testToString() { + Fraction f; + + f = Fraction.getFraction(3, 5); + final String str = f.toString(); + assertEquals("3/5", str); + assertSame(str, f.toString()); + + f = Fraction.getFraction(7, 5); + assertEquals("7/5", f.toString()); + + f = Fraction.getFraction(4, 2); + assertEquals("4/2", f.toString()); + + f = Fraction.getFraction(0, 2); + assertEquals("0/2", f.toString()); + + f = Fraction.getFraction(2, 2); + assertEquals("2/2", f.toString()); + + f = Fraction.getFraction(Integer.MIN_VALUE, 0, 1); + assertEquals("-2147483648/1", f.toString()); + + f = Fraction.getFraction(-1, 1, Integer.MAX_VALUE); + assertEquals("-2147483648/2147483647", f.toString()); + } + + @Test + public void testNoEager() { + Fraction f; + f = Fraction.getFraction(3, 5); + } + + @Test + public void testToProperString() { + Fraction f; + + f = Fraction.getFraction(3, 5); + final String str = f.toProperString(); + assertEquals("3/5", str); + assertSame(str, f.toProperString()); + + f = Fraction.getFraction(7, 5); + assertEquals("1 2/5", f.toProperString()); + + f = Fraction.getFraction(14, 10); + assertEquals("1 4/10", f.toProperString()); + + f = Fraction.getFraction(4, 2); + assertEquals("2", f.toProperString()); + + f = Fraction.getFraction(0, 2); + assertEquals("0", f.toProperString()); + + f = Fraction.getFraction(2, 2); + assertEquals("1", f.toProperString()); + + f = Fraction.getFraction(-7, 5); + assertEquals("-1 2/5", f.toProperString()); + + f = Fraction.getFraction(Integer.MIN_VALUE, 0, 1); + assertEquals("-2147483648", f.toProperString()); + + f = Fraction.getFraction(-1, 1, Integer.MAX_VALUE); + assertEquals("-1 1/2147483647", f.toProperString()); + + assertEquals("-1", Fraction.getFraction(-1).toProperString()); + } + } + """.trimIndent() +} \ No newline at end of file From bd194683eba56023153c3529632d9f8e15dc9f7c Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Sat, 23 Jan 2021 12:12:05 +0100 Subject: [PATCH 17/19] removed useless addition --- src/test/kotlin/testsmell/TestDetectionCorrectness.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/test/kotlin/testsmell/TestDetectionCorrectness.kt b/src/test/kotlin/testsmell/TestDetectionCorrectness.kt index 9d781d5..e44e60a 100644 --- a/src/test/kotlin/testsmell/TestDetectionCorrectness.kt +++ b/src/test/kotlin/testsmell/TestDetectionCorrectness.kt @@ -94,7 +94,7 @@ class TestDetectionCorrectness { @Test fun `Test number of methods detected`() { val declaration = testCompilationUnit.types[0] - Assertions.assertEquals(26, declaration.methods.size) + Assertions.assertEquals(25, declaration.methods.size) } private val fractionSource = """ @@ -2112,12 +2112,6 @@ class TestDetectionCorrectness { assertEquals("-2147483648/2147483647", f.toString()); } - @Test - public void testNoEager() { - Fraction f; - f = Fraction.getFraction(3, 5); - } - @Test public void testToProperString() { Fraction f; From 5f46e44e7666690e5464eb8baebefb39dd8ecf21 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Sat, 23 Jan 2021 12:14:19 +0100 Subject: [PATCH 18/19] improvement on the README --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 503760f..777c786 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Running the jar with `--help` will print its usage. * A CSV input file always need to be given as parameter, specified with `-f`; * A detection threshold can also be specified. Possible values are `default` and `spadini`. The flag is `-t`. By default, the tool uses the thresholds that have been originally implemented; -with `spadini`, sensibility thresholds published by Spadini et.al. will be used. +with `spadini`, sensibility thresholds published by [Spadini et.al.] will be used. * One can specify the granularity of the detection. `boolean` will return either true or false, respectively if a given smell is present or not in the test; `numerical` will return instead the number of smelly instances detected. @@ -38,4 +38,6 @@ Options: detection -o, --output TEXT -h, --help Show this message and exit -``` \ No newline at end of file +``` + +[Spadini et.al.]: https://dl.acm.org/doi/abs/10.1145/3379597.3387453 \ No newline at end of file From f240ed81bd09a183763a40af3452b246b8d07dc6 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Fri, 12 Mar 2021 10:20:02 +0100 Subject: [PATCH 19/19] Test for calls into assertions into the detection of eager --- .../testsmell/TestAssertionsDetection.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/test/kotlin/testsmell/TestAssertionsDetection.kt diff --git a/src/test/kotlin/testsmell/TestAssertionsDetection.kt b/src/test/kotlin/testsmell/TestAssertionsDetection.kt new file mode 100644 index 0000000..18dcede --- /dev/null +++ b/src/test/kotlin/testsmell/TestAssertionsDetection.kt @@ -0,0 +1,86 @@ +package testsmell + +import com.github.javaparser.JavaParser +import com.github.javaparser.ast.CompilationUnit +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock +import testsmell.smell.EagerTest +import thresholds.DefaultThresholds + +class TestAssertionsDetection { + + private lateinit var testCompilationUnit: CompilationUnit + private lateinit var productionCompilationUnit: CompilationUnit + private lateinit var testFile: TestFile + private val booleanGranularity: ((AbstractSmell) -> Any) = { it.hasSmell() } + + @BeforeEach + fun setup() { + testCompilationUnit = JavaParser.parse(simpleTest) + productionCompilationUnit = JavaParser.parse(simpleClass) + testFile = mock(TestFile::class.java) + Mockito.`when`(testFile.testFileNameWithoutExtension).thenReturn("fake/path") + Mockito.`when`(testFile.productionFileNameWithoutExtension).thenReturn("fake/path") + } + + /** + * Check whether production calls made in the assertions count toward detection of + * eagerness. In theory, they should. This might not apply (or carefully considered) in the + * context of generated tests, where the assertions are placed at the end of the search process. + */ + @Test + fun `Assertions counting into eager detection`() { + val smell = EagerTest(DefaultThresholds()) + smell.runAnalysis(testCompilationUnit, productionCompilationUnit, + testFile.testFileNameWithoutExtension, testFile.productionFileNameWithoutExtension) + Mockito.`when`(testFile.testSmells).thenReturn(listOf(smell)) + val values = testFile.testSmells.map { booleanGranularity.invoke(it) } + Assertions.assertTrue(values[0] as Boolean) + } + + private val simpleClass = """ + public class Calculator { + + private int numberOne; + private int numberTwo; + + public Calculator(int numberOne, int numberTwo) { + this.numberOne = numberOne; + this.numberTwo = numberTwo; + } + + public int sum() { + return numberOne + numberTwo; + } + + public int sub() { + return numberOne - numberTwo; + } + + public int mul() { + return numberOne * numberTwo; + } + } + """.trimIndent() + + private val simpleTest = """ + import static org.junit.jupiter.api.Assertions.assertEquals; + + import org.junit.jupiter.api.Test; + + public class CalculatorTest { + + @Test + public void testDummy() { + Calculator calc = new Calculator(10, 5); + int m = calc.mul(); + assertEquals(m, 50); + assertEquals(calc.sum(), 15); + assertEquals(calc.sub(), 5); + } + } + """.trimIndent() +} \ No newline at end of file