Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add junit test for text fields check #12057

Merged
merged 13 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,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,259 @@
package org.jabref.model.entry.field;

import java.io.IOException;
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.logging.Level;
import java.util.logging.Logger;
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 FieldEditorTextAreaTest {

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;
private static final Logger LOGGER = Logger.getLogger(FieldEditorTextAreaTest.class.getName());

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

/**
* 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 fieldEditorTextAreaTest() throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More self-descriptive test name please. I think fieldEditorsMatchMultilineProperty could be a good name.

// get all field editors and their properties in FieldEditors.java
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this comment - it is exactly the method name (which is good)

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().orElse(null);
if (cu == null) {
throw new RuntimeException("Failed to analyze " + filePath);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No RuntimeExeptions in tests. Use normal Exceptions. And use throws clause.

NullPointerExceptions are also OK.

The text Failed to analyze is too generic. Is it because of a logic issue? The NullPointerException is more specific than a general failure

}

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 hold 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() {
final String filePath = "src/main/java/org/jabref/gui/fieldeditors/FieldEditors.java";
Map<Path, List<FieldProperty>> result = new HashMap<>();

try {
CompilationUnit cu = PARSER.parse(Paths.get(filePath)).getResult().orElse(null);
if (cu == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

throw new RuntimeException("Failed to analyze FieldEditors.java");
}

// locate getForField method in FieldEditors.java
MethodDeclaration getForFieldCall = cu.findAll(MethodDeclaration.class).stream()
.filter(methodDeclaration -> "getForField".equals(methodDeclaration.getNameAsString()))
.findFirst()
.orElseThrow(() -> new RuntimeException("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);
try {
FieldProperty property = FieldProperty.valueOf(propertyName);
properties.add(property);
} catch (IllegalArgumentException e) {
LOGGER.warning("Unknown FieldProperty: " + propertyName);
}
}
// match `== StandardField.XXX`
Matcher standardFieldMatcher = STANDARD_FIELD_PATTERN.matcher(condition);
if (standardFieldMatcher.find()) {
String fieldName = standardFieldMatcher.group(1);
try {
StandardField standardField = StandardField.valueOf(fieldName);
properties.addAll(standardField.getProperties());
} catch (IllegalArgumentException e) {
LOGGER.warning("Unknown StandardField: " + fieldName);
}
}
// match `== InternalField.XXX`
Matcher internalFieldMatcher = INTERNAL_FIELD_PATTERN.matcher(condition);
if (internalFieldMatcher.find()) {
String fieldName = internalFieldMatcher.group(1);
try {
InternalField internalField = InternalField.valueOf(fieldName);
properties.addAll(internalField.getProperties());
} catch (IllegalArgumentException e) {
LOGGER.warning("Unknown InternalField: " + fieldName);
}
}

// get this if statement's return statement
ReturnStmt returnStatement = ifStmt.getThenStmt().stream()
.filter(ReturnStmt.class::isInstance)
.map(ReturnStmt.class::cast)
.findFirst()
.orElse(null);
if (returnStatement != null) {
// get the creation expression in the return statement
ObjectCreationExpr creationExpr = returnStatement.stream()
.filter(ObjectCreationExpr.class::isInstance)
.map(ObjectCreationExpr.class::cast)
.findFirst()
.orElse(null);
if (creationExpr != null) {
// get the created class name
String createdClassName = creationExpr.getTypeAsString().replace("<>", "");
// get the exact java file path from import statement
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);
});
}
}
});
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error parsing file: " + filePath, e);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are crafting a JUnit test - just let exceptions flow - no need to handle or log them.


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();
}
}
Loading