Hi Quentin,
Thank you for the tutorial link.
I have gone through this previously; however, its very much inline with the other things available on the internet. There is nothing really explaining how to compute a measure at a class level (a percentage value) and return it back to the sensor to push to SonarQube so that percentage displays next to the class it just scanned.
Right now, I’m using the sonar-example-plugin 7.9.* and even that project doesn’t have any examples that do something similar.
Just to show you what I have in a nutshell. Could you tell me how far off I am and provide direction on how I can tie it together to compute a measure per scanned file, return it and save it for SonarQube to display (and also report issues in the middle of the scan).
Note: I haven’t cleaned this up yet to remove what is not needed and to add a logger vs using Sys.out. Please disregard.
@Rule(
key = "AssertToInvokeRatioRule",
name = "Compare Method Invocations to Unit Test Assertions",
description = "For every method invocation in a unit test in a test class, there should be one or more unit test assertions.",
tags = {"bug"}
)
public class AssertToInvokeRule extends BaseTreeVisitor implements JavaFileScanner {
private static int assertionCount = 0;
private static int methodInvocationCount = 0;
private static int methodTestedClassCount = 0;
private static final String VERIFY = "verify";
private static final String ASSERT_NAME = "assert";
private static final TypeCriteria ORG_MOCKITO_MOCKITO = TypeCriteria.is("org.mockito.Mockito");
private static final TypeCriteria ORG_ASSERTJ_ASSERTIONS = TypeCriteria.is("org.assertj.core.api.Assertions");
private static final TypeCriteria ORG_ASSERTJ_FAIL = TypeCriteria.is("org.assertj.core.api.Fail");
private static final TypeCriteria ANY_TYPE = TypeCriteria.anyType();
private static final NameCriteria ANY_NAME = NameCriteria.any();
private static final NameCriteria STARTS_WITH_FAIL = NameCriteria.startsWith("fail");
private static final NameCriteria STARTS_WITH_ASSERT = NameCriteria.startsWith(ASSERT_NAME);
private final Deque<Boolean> methodContainsAssertion = new ArrayDeque<>();
private final Deque<Boolean> inUnitTest = new ArrayDeque<>();
private final Deque<Boolean> methodContainsTestedClass = new ArrayDeque<>();
private JavaFileScannerContext context;
@Override
public void scanFile(final JavaFileScannerContext context) {
this.context = context;
inUnitTest.push(false);
methodContainsAssertion.push(false);
methodContainsTestedClass.push(false);
scan(context.getTree());
inUnitTest.pop();
methodContainsAssertion.pop();
methodContainsTestedClass.pop();
double assertToInvokeRatioResult = getAssertToInvokeRatioResult(assertionCount, methodTestedClassCount);
DecimalFormat df = new DecimalFormat("#");
System.out.println("********CLASS LEVEL ASSERT TO INVOKE RATIO: " + df.format(assertToInvokeRatioResult) + "%");
if (assertToInvokeRatioResult<80) {
context.reportIssue(this,context.getTree(),"Total class level method invocation count is greater than test assertion by more than 20%");
}
}
private double getAssertToInvokeRatioResult(int assertCount, int methodTestedCount) {
double assertToInvokeRatioResult;
if (assertCount >= methodTestedCount) {
assertToInvokeRatioResult = 100;
} else {
assertToInvokeRatioResult = (double) 100 * assertCount / methodTestedCount;
}
return assertToInvokeRatioResult;
}
@Override
public void visitMethod(MethodTree methodTree) {
if (ModifiersUtils.hasModifier(methodTree.modifiers(), Modifier.ABSTRACT)) {
return;
}
boolean isUnitTest = isUnitTest(methodTree);
inUnitTest.push(isUnitTest);
double methodLevelRatioResult;
int tempAssertCount = assertionCount;
int tempMethodInvokeCount = methodTestedClassCount;
methodContainsAssertion.push(false);
methodContainsTestedClass.push(false);
super.visitMethod(methodTree);
methodContainsAssertion.pop();
methodContainsTestedClass.pop();
inUnitTest.pop();
if(isUnitTest){
tempAssertCount = assertionCount - tempAssertCount;
tempMethodInvokeCount = methodTestedClassCount - tempMethodInvokeCount;
methodLevelRatioResult = getAssertToInvokeRatioResult(tempAssertCount, tempMethodInvokeCount);
DecimalFormat df = new DecimalFormat("##");
System.out.println("********METHOD LEVEL ASSERT TO INVOKE RATIO:: " + df.format(methodLevelRatioResult) + "%");
if(methodLevelRatioResult < 80) {
//context.addIssueOnProject((JavaCheck) methodTree, " Method invocation count is greater than test assertion by more than 20%");
context.reportIssue(this, methodTree.simpleName(), "Method level invocation count is greater than test assertion by more than 20%");
}
}
}
@Override
public void visitMethodInvocation(MethodInvocationTree mit) {
super.visitMethodInvocation(mit);
String fullyQualifiedNameOfExpression = new String();
if (!mit.methodSelect().is(Tree.Kind.IDENTIFIER)) {
MemberSelectExpressionTree memberSelectExpressionTree = ((MemberSelectExpressionTree) mit.methodSelect());
fullyQualifiedNameOfExpression = memberSelectExpressionTree.expression().symbolType().fullyQualifiedName();
}
if (inUnitTest.peek() && ASSERTION_INVOCATION_MATCHERS.anyMatch(mit)) {
assertionCount = assertionCount +1;
}
//if method is a UT and not an assertion, then increment methodinvocation count
if(inUnitTest.peek() && !ASSERTION_INVOCATION_MATCHERS.anyMatch(mit)) {
methodInvocationCount = methodInvocationCount +1;
List<String> classPath = Arrays.asList(fullyQualifiedNameOfExpression.split("\\."));
if(classPath.get(0).equals("com") && classPath.get(1).equals("aString")){
if (getTestedClassMethodInvocationMatchers(fullyQualifiedNameOfExpression).anyMatch(mit)){
methodTestedClassCount = methodTestedClassCount +1;
}
}
}
}
…
That logic, I need it to run in a sensor, and I need the computed ratio to return back to sensor at the class level and am incredibly stuck. Unable to find any examples anywhere to assist. The API is also undocumented.
@Override
public void execute(SensorContext context) {
FileSystem fs = context.fileSystem();
Iterable<InputFile> javaFiles = fs.inputFiles(fs.predicates().hasLanguage("java"));
for (InputFile javaFile : javaFiles) {
NewIssue newIssue = context.newIssue()
.forRule(AssertToInvokeRulesDefinition.RULE_ON_ASSERT_TO_INVOKE_RATIO);
etc.....
}
}
}
Any advice you can give would be really apperciated. I’m not sure if I’m on the right track, or not. Just feeling lost and at the moment, I’m at the point where I don’t have any idea how to figure out where to go with this. I’ve scoured the internet in search of any explanation and example that could speak to what I’m trying to accomplish.