Skip to content

Commit

Permalink
Add junit test for text fields check (#12057)
Browse files Browse the repository at this point in the history
* try implement check

* add FieldEditorTextAreaTest for check if FieldEditor handle FieldProperty.MULTILINE_TEXT properly

* delete unused code

* fix issue 11939: Check FieldEditorFX interface, Check new EditorTextArea creation, Check TextInputControl field

* try fixes code style issue

* try fixes code style issue

* try fixes code style issue use check style plugin

* try fixes open rewrite issue

* 1. Optimize error msg.
2. Rename the test method and file.
3. Use normal exceptions
4. Remove logger

* 1. Add additional Javadoc to indicate potential test fragility
2. Optimize indentation and some comments

---------

Co-authored-by: Ziying Ye <u7790708@anu.edu.au>
  • Loading branch information
ShunL12324 and u7790708 authored Oct 27, 2024
1 parent ce27eb3 commit 28fb91e
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ dependencies {
testImplementation "org.testfx:testfx-core:4.0.16-alpha"
testImplementation "org.testfx:testfx-junit5:4.0.16-alpha"
testImplementation "org.hamcrest:hamcrest-library:3.0"
testImplementation "com.github.javaparser:javaparser-symbol-solver-core:3.26.2"

// recommended by https://github.com/wiremock/wiremock/issues/2149#issuecomment-1835775954
testImplementation 'org.wiremock:wiremock-standalone:3.3.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package org.jabref.model.entry.field;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.ObjectCreationExpr;
import com.github.javaparser.ast.stmt.IfStmt;
import com.github.javaparser.ast.stmt.ReturnStmt;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;

public class FieldEditorsMultilinePropertyTest {

private static final Pattern FIELD_PROPERTY_PATTERN = Pattern.compile("fieldProperties\\.contains\\s*\\(\\s*FieldProperty\\.(\\w+)\\s*\\)");
private static final Pattern STANDARD_FIELD_PATTERN = Pattern.compile("==\\s*StandardField\\.(\\w+)");
private static final Pattern INTERNAL_FIELD_PATTERN = Pattern.compile("==\\s*InternalField\\.(\\w+)");
private static JavaParser PARSER;

@BeforeAll
public static void setUp() {
ParserConfiguration configuration = new ParserConfiguration();
configuration.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21);
PARSER = new JavaParser(configuration);
}

/**
* This test is somewhat fragile, as it depends on the structure of FieldEditors.java.
* If the structure of FieldEditors.java is changed, this test might fail.
* This test performs the following steps:
* 1. Use Java parser to parse FieldEditors.java and check all if statements in the getForField method.
* 2. Match the conditions of if statements to extract the field properties.
* 3. Match the created FieldEditor class name with field properties extracted from step 2. This creates a map where:
* - The key is the file path of the FieldEditor class (for example: ....UrlEditor.java)
* - The value is the list of properties of the FieldEditor class (for example: [FieldProperty.EXTERNAL])
* 4. For every class in the map, when its properties contain MULTILINE_TEXT, check whether it:
* a) Holds a TextInputControl field
* b) Has an EditorTextArea object creation
*/
@Test
public void fieldEditorsMatchMultilineProperty() throws Exception {
Map<Path, List<FieldProperty>> result = getEditorsWithPropertiesInFieldEditors();
for (Map.Entry<Path, List<FieldProperty>> entry : result.entrySet()) {
// Now we have the file path and its properties, going to analyze the target Editor class
Path filePath = entry.getKey();
List<FieldProperty> properties = entry.getValue();
CompilationUnit cu = PARSER.parse(filePath)
.getResult()
.orElseThrow(() ->
new NullPointerException("Failed to parse "
+ filePath
+ ", java parser returned null CompilationUnit"
+ ", please check if the file exists"));

if (!implementedFieldEditorFX(cu)) {
continue; // Make sure the class implements FieldEditorFX interface
}

if (properties.contains(FieldProperty.MULTILINE_TEXT)) {
// If the editor has MULTILINE_TEXT property, we are going to check if the class holds a `TextInputControl` field
// and have performed Text Area creation
assertTrue(holdTextInputControlField(cu) && hasEditorTextAreaCreationExisted(cu),
"Class " + filePath + " should hold a TextInputControl field and have EditorTextArea creation");
}
}
}

/**
* Parse FieldEditors.java to get all field editors and their properties in function getForField
*
* @return a map of field editor file path and its properties
*/
private static Map<Path, List<FieldProperty>> getEditorsWithPropertiesInFieldEditors() throws Exception {
final String filePath = "src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java";
Map<Path, List<FieldProperty>> result = new HashMap<>();
CompilationUnit cu = PARSER.parse(Paths.get(filePath))
.getResult()
.orElseThrow(() ->
new NullPointerException("Failed to parse FieldEditors.java"));

// Locate getForField method in FieldEditors.java
MethodDeclaration getForFieldCall = cu.findAll(MethodDeclaration.class).stream()
.filter(methodDeclaration -> "getForField".equals(methodDeclaration.getNameAsString()))
.findFirst()
.orElseThrow(() -> new Exception("Failed to find getForField method in FieldEditors.java"));

// Analyze all if statements in getForField method
getForFieldCall.findAll(IfStmt.class).forEach(ifStmt -> {
String condition = ifStmt.getCondition().toString();
List<FieldProperty> properties = new ArrayList<>();
// Match `fieldProperties.contains(FieldProperty.XXX)`
Matcher propertyMatcher = FIELD_PROPERTY_PATTERN.matcher(condition);
while (propertyMatcher.find()) {
String propertyName = propertyMatcher.group(1);
FieldProperty property = FieldProperty.valueOf(propertyName);
properties.add(property);
}
// Match `== StandardField.XXX`
Matcher standardFieldMatcher = STANDARD_FIELD_PATTERN.matcher(condition);
if (standardFieldMatcher.find()) {
String fieldName = standardFieldMatcher.group(1);
StandardField standardField = StandardField.valueOf(fieldName);
properties.addAll(standardField.getProperties());
}
// Match `== InternalField.XXX`
Matcher internalFieldMatcher = INTERNAL_FIELD_PATTERN.matcher(condition);
if (internalFieldMatcher.find()) {
String fieldName = internalFieldMatcher.group(1);
InternalField internalField = InternalField.valueOf(fieldName);
properties.addAll(internalField.getProperties());
}

// Check if the return statement contains an object creation
// If so, extract the created class name and its path
ifStmt.getThenStmt().stream()
.filter(ReturnStmt.class::isInstance)
.map(ReturnStmt.class::cast)
.findFirst()
.flatMap(returnStmt ->
// Try to find the object creation in the return statement
returnStmt.stream()
.filter(ObjectCreationExpr.class::isInstance)
.map(ObjectCreationExpr.class::cast)
.findFirst()).ifPresent(creationExpr -> {
String createdClassName = creationExpr.getTypeAsString().replace("<>", "");
cu.findAll(ImportDeclaration.class)
.stream()
.filter(importDeclaration -> importDeclaration.getNameAsString().endsWith(createdClassName))
.findFirst()
.ifPresentOrElse(importDeclaration -> {
String classPath = importDeclaration.getNameAsString();
Path classFilePath = Paths.get("src/main/java/" + classPath.replace(".", "/") + ".java");
result.put(classFilePath, properties);
}, () -> {
Path classFilePath = Paths.get("src/main/java/org/jabref/gui/fieldeditors/" + createdClassName + ".java");
result.put(classFilePath, properties);
});
});
});

return result;
}

/**
* Check if the class implements FieldEditorFX interface
*
* @param cu CompilationUnit
* @return true if the class implements FieldEditorFX interface
*/
private static boolean implementedFieldEditorFX(CompilationUnit cu) {
return cu.findAll(ClassOrInterfaceDeclaration.class).stream()
.anyMatch(classDecl -> classDecl.getImplementedTypes().stream()
.anyMatch(type -> Objects.equals("FieldEditorFX", type.getNameAsString())));
}

/**
* Check if the class has a new EditorTextArea creation
*
* @param cu CompilationUnit
* @return true if the class has a new EditorTextArea creation
*/
private static boolean hasEditorTextAreaCreationExisted(CompilationUnit cu) {
return cu.findAll(ObjectCreationExpr.class).stream()
.anyMatch(creation -> Objects.equals("EditorTextArea", creation.getType().toString()));
}

/**
* Check if the class holds a TextInputControl field
*
* @param cu CompilationUnit
* @return true if the class holds a TextInputControl field
*/
private static boolean holdTextInputControlField(CompilationUnit cu) {
// Since the class implements FieldEditorFX, we are going to check the first parameter when call
// establishBinding method, which should be a TextInputControl
AtomicBoolean hasTextInputControlField = new AtomicBoolean(false);
cu.findAll(MethodCallExpr.class)
.stream()
.filter(methodCallExpr -> "establishBinding".equals(methodCallExpr.getNameAsString()))
.findFirst()
.ifPresent(methodCallExpr -> {
if (!methodCallExpr.getArguments().isEmpty()) {
String firstArgument = methodCallExpr.getArgument(0).toString();
cu.findAll(FieldDeclaration.class)
.stream()
.filter(fieldDeclaration -> fieldDeclaration.getVariables().stream()
.anyMatch(variableDeclarator -> variableDeclarator.getNameAsString().equals(firstArgument)))
.findFirst()
.ifPresent(fieldDeclaration -> {
String classType = fieldDeclaration.getElementType().asString();
if ("TextInputControl".equals(classType)) {
hasTextInputControlField.set(true);
}
});
}
});
return hasTextInputControlField.get();
}

private static boolean holdEditorTextField(CompilationUnit compilationUnit) {
AtomicBoolean hasEditorTextField = new AtomicBoolean(false);
compilationUnit.findAll(MethodCallExpr.class).stream()
.filter(methodCallExpr -> "establishBinding".equals(methodCallExpr.getNameAsString()))
.findFirst()
.ifPresent(establishBindingCall -> {
String firstArg = establishBindingCall.getArgument(0).toString();
compilationUnit.findAll(FieldDeclaration.class).stream()
.filter(field -> field.getVariable(0).getNameAsString().equals(firstArg))
.findFirst()
.ifPresent(fieldDeclaration -> {
String fieldType = fieldDeclaration.getElementType().asString();
if ("EditorTextField".equals(fieldType)) {
hasEditorTextField.set(true);
}
});
});
return hasEditorTextField.get();
}
}

0 comments on commit 28fb91e

Please sign in to comment.