Skip to content

Commit

Permalink
Add structural and behavior test files from Artemis (Merge PR #62)
Browse files Browse the repository at this point in the history
Co-authored-by: Stephan Krusche <s.krusche@tum.de>
Co-authored-by: Stefan Klöss-Schuster <stefan.kloess-Schuster@tum.de>
  • Loading branch information
3 people committed Nov 17, 2020
2 parents a9ae6f6 + ba07481 commit 3675b57
Show file tree
Hide file tree
Showing 29 changed files with 2,739 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/reports
!/.settings/org.eclipse.*
!/.settings/eclipse*rules*
/artemis-java-test-sandbox.iml
/.idea
3 changes: 2 additions & 1 deletion .settings/eclipse-rules.importorder
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
3=com
4=net
5=ch
6=de.tum
6=info
7=de.tum
2 changes: 1 addition & 1 deletion .settings/org.eclipse.jdt.ui.prefs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ eclipse.preferences.version=1
formatter_profile=_Eclipse adjusted
formatter_settings_version=19
org.eclipse.jdt.ui.ignorelowercasenames=true
org.eclipse.jdt.ui.importorder=java;javax;org;com;net;ch;de.tum;
org.eclipse.jdt.ui.importorder=java;javax;org;com;net;ch;info;de.tum;
org.eclipse.jdt.ui.ondemandthreshold=99
org.eclipse.jdt.ui.staticondemandthreshold=2
org.eclipse.jdt.ui.text.custom_code_templates=
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@
<artifactId>assertj-core</artifactId>
<version>3.18.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20201115</version>
</dependency>
<dependency>
<groupId>info.debatty</groupId>
<artifactId>java-string-similarity</artifactId>
<version>2.0.0</version>
</dependency>
<!-- For testing we use a test framework testing framework -->
<dependency>
<groupId>org.junit.platform</groupId>
Expand Down
31 changes: 21 additions & 10 deletions src/main/java/de/tum/in/test/api/jupiter/JupiterContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;

import org.apiguardian.api.API;
import org.apiguardian.api.API.Status;
Expand All @@ -22,39 +25,47 @@ public class JupiterContext extends TestContext {

@Override
public Optional<Method> testMethod() {
return extensionContext.getTestMethod();
return findOptionalsInHierarchy(ExtensionContext::getTestMethod).findFirst();
}

@Override
public Optional<Class<?>> testClass() {
return extensionContext.getTestClass();
return findOptionalsInHierarchy(ExtensionContext::getTestClass).findFirst();
}

@Override
public Optional<Object> testInstance() {
return extensionContext.getTestInstance();
return findOptionalsInHierarchy(ExtensionContext::getTestInstance).findFirst();
}

@Override
public Optional<AnnotatedElement> annotatedElement() {
return extensionContext.getElement();
return findOptionalsInHierarchy(ExtensionContext::getElement).findFirst();
}

@Override
public String displayName() {
return extensionContext.getDisplayName();
}

@Override
public Optional<TestType> findTestType() {
return TestContextUtils.findAnnotationIn(this, JupiterArtemisTest.class).map(JupiterArtemisTest::value);
}

public ExtensionContext getExtensionContext() {
return extensionContext;
}

public static JupiterContext of(ExtensionContext extensionContext) {
return new JupiterContext(extensionContext);
private <T> Stream<T> findOptionalsInHierarchy(Function<ExtensionContext, Optional<T>> getter) {
return hierarchy().map(getter).filter(Optional::isPresent).map(Optional::get);
}

@Override
public Optional<TestType> findTestType() {
return TestContextUtils.findAnnotationIn(this, JupiterArtemisTest.class).map(JupiterArtemisTest::value);
private Stream<ExtensionContext> hierarchy() {
return Stream.iterate(extensionContext, Objects::nonNull, ec -> ec.getParent().orElse(null));
}

public static JupiterContext of(ExtensionContext extensionContext) {
return new JupiterContext(extensionContext);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class SecurityConstants {
static final Set<String> STACK_WHITELIST = Set.of("java.", "org.junit.", "jdk.", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
"org.eclipse.", "com.intellij", "org.assertj", "org.opentest4j.", // $NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
"com.sun.", "sun.", "org.apache.", "de.tum.in.test.", "net.jqwik", "ch.qos.logback", "org.jacoco", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
SECURITY_PACKAGE_NAME);
"javax.", "org.json", SECURITY_PACKAGE_NAME); //$NON-NLS-1$ //$NON-NLS-2$
static final Set<String> STACK_BLACKLIST = Set.of(BlacklistedInvoker.class.getName());

static final Set<String> PACKAGE_USE_BLACKLIST = Set.of(SECURITY_PACKAGE_NAME, "de.tum.in.test.api.internal", //$NON-NLS-1$
Expand Down
237 changes: 237 additions & 0 deletions src/main/java/de/tum/in/test/api/structural/AttributeTestProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package de.tum.in.test.api.structural;

import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;

/**
* This test evaluates if the specified attributes in the structure oracle are
* correctly implemented with the expected type, visibility modifiers and
* annotations, based on its definition in the structure oracle (test.json).
*
* @author Stephan Krusche (krusche@in.tum.de)
* @version 5.0 (11.11.2020)
*/
public abstract class AttributeTestProvider extends StructuralTestProvider {

private static final Pattern PACKAGE_NAME_IN_GENERIC_TYPE = Pattern.compile("(?:[^\\[\\]<>?,\\s.]++\\.)++");

/**
* This method collects the classes in the structure oracle file for which
* attributes are specified. These classes are then transformed into JUnit 5
* dynamic tests.
*
* @return A dynamic test container containing the test for each class which is
* then executed by JUnit.
* @throws URISyntaxException an exception if the URI of the class name cannot
* be generated (which seems to be unlikely)
*/
protected DynamicContainer generateTestsForAllClasses() throws URISyntaxException {
List<DynamicNode> tests = new ArrayList<>();
if (structureOracleJSON == null) {
fail("The AttributeTest test can only run if the structural oracle (test.json) is present. If you do not provide it, delete AttributeTest.java!");
}
for (int i = 0; i < structureOracleJSON.length(); i++) {
JSONObject expectedClassJSON = structureOracleJSON.getJSONObject(i);
// Only test the classes that have attributes defined in the oracle.
if (expectedClassJSON.has(JSON_PROPERTY_CLASS) && (expectedClassJSON.has(JSON_PROPERTY_ATTRIBUTES)
|| expectedClassJSON.has(JSON_PROPERTY_ENUM_VALUES))) {
JSONObject expectedClassPropertiesJSON = expectedClassJSON.getJSONObject(JSON_PROPERTY_CLASS);
String expectedClassName = expectedClassPropertiesJSON.getString(JSON_PROPERTY_NAME);
String expectedPackageName = expectedClassPropertiesJSON.getString(JSON_PROPERTY_PACKAGE);
ExpectedClassStructure expectedClassStructure = new ExpectedClassStructure(expectedClassName,
expectedPackageName, expectedClassJSON);
tests.add(dynamicTest("testAttributes[" + expectedClassName + "]",
() -> testAttributes(expectedClassStructure)));
}
}
if (tests.isEmpty()) {
fail("No tests for attributes available in the structural oracle (test.json). Either provide attributes information or delete AttributeTest.java!");
}
/*
* Using a custom URI here to workaround surefire rendering the JUnit XML
* without the correct test names.
*/
return dynamicContainer(getClass().getName(), new URI(getClass().getName()), tests.stream());
}

/**
* This method gets passed the expected class structure generated by the method
* {@link #generateTestsForAllClasses()}, checks if the class is found at all in
* the assignment and then proceeds to check its attributes.
*
* @param expectedClassStructure The class structure that we expect to find and
* test against.
*/
protected void testAttributes(ExpectedClassStructure expectedClassStructure) {
String expectedClassName = expectedClassStructure.getExpectedClassName();
Class<?> observedClass = findClassForTestType(expectedClassStructure, "attribute");
if (observedClass == null) {
fail(THE_CLASS + expectedClassName + " was not found for attribute test");
return;
}
if (expectedClassStructure.hasProperty(JSON_PROPERTY_ATTRIBUTES)) {
JSONArray expectedAttributes = expectedClassStructure.getPropertyAsJsonArray(JSON_PROPERTY_ATTRIBUTES);
checkAttributes(expectedClassName, observedClass, expectedAttributes);
}
if (expectedClassStructure.hasProperty(JSON_PROPERTY_ENUM_VALUES)) {
JSONArray expectedEnumValues = expectedClassStructure.getPropertyAsJsonArray(JSON_PROPERTY_ENUM_VALUES);
checkEnumValues(expectedClassName, observedClass, expectedEnumValues);
}
}

/**
* This method checks if a observed class' attributes match the expected ones
* defined in the structure oracle.
*
* @param expectedClassName The simple name of the class, mainly used for error
* messages.
* @param observedClass The class that needs to be checked as a Class
* object.
* @param expectedAttributes The information on the expected attributes
* contained in a JSON array. This information
* consists of the name, the type and the visibility
* modifiers of each attribute.
*/
protected void checkAttributes(String expectedClassName, Class<?> observedClass, JSONArray expectedAttributes) {
for (int i = 0; i < expectedAttributes.length(); i++) {
JSONObject expectedAttribute = expectedAttributes.getJSONObject(i);
String expectedName = expectedAttribute.getString(JSON_PROPERTY_NAME);
String expectedTypeName = expectedAttribute.getString(JSON_PROPERTY_TYPE);
JSONArray expectedModifiers = getExpectedJsonProperty(expectedAttribute, JSON_PROPERTY_MODIFIERS);
JSONArray expectedAnnotations = getExpectedJsonProperty(expectedAttribute, JSON_PROPERTY_ANNOTATIONS);

// We check for each expected attribute if the name and the type is right.
var nameIsCorrect = false;
var typeIsCorrect = false;
var modifiersAreCorrect = false;
var annotationsAreCorrect = false;

for (Field observedAttribute : observedClass.getDeclaredFields()) {
if (expectedName.equals(observedAttribute.getName())) {
nameIsCorrect = true;
typeIsCorrect = checkType(observedAttribute, expectedTypeName);
modifiersAreCorrect = checkModifiers(Modifier.toString(observedAttribute.getModifiers()).split(" "),
expectedModifiers);
annotationsAreCorrect = checkAnnotations(observedAttribute.getAnnotations(), expectedAnnotations);

// If all are correct, then we found our attribute and we can break the loop
if (typeIsCorrect && modifiersAreCorrect && annotationsAreCorrect) {
break;
}
}
// TODO: we should also take wrong case and typos into account (the else case)
}
checkAttributeCorrectness(nameIsCorrect, typeIsCorrect, modifiersAreCorrect, annotationsAreCorrect,
expectedName, expectedClassName);
}
}

private static void checkAttributeCorrectness(boolean nameIsCorrect, boolean typeIsCorrect,
boolean modifiersAreCorrect, boolean annotationsAreCorrect, String expectedName, String expectedClassName) {
String expectedAttributeInformation = "the expected attribute '" + expectedName + "' of the class '"
+ expectedClassName + "'";
if (!nameIsCorrect) {
fail("The name of " + expectedAttributeInformation + " is not implemented as expected.");
}
if (!typeIsCorrect) {
fail("The type of " + expectedAttributeInformation + " is not implemented as expected.");
}
if (!modifiersAreCorrect) {
fail("The modifier(s) (access type, abstract, etc.) of " + expectedAttributeInformation
+ NOT_IMPLEMENTED_AS_EXPECTED);
}
if (!annotationsAreCorrect) {
fail("The annotation(s) of " + expectedAttributeInformation + NOT_IMPLEMENTED_AS_EXPECTED);
}
}

/**
* This method checks if the observed enum values match the expected ones
* defined in the structure oracle.
*
* @param expectedClassName The simple name of the class, mainly used for error
* messages.
* @param observedClass The enum that needs to be checked as a Class
* object.
* @param expectedEnumValues The information on the expected enum values
* contained in a JSON array. This information
* consists of the name of each enum value.
*/
protected void checkEnumValues(String expectedClassName, Class<?> observedClass, JSONArray expectedEnumValues) {
Object[] observedEnumValues = observedClass.getEnumConstants();
if (observedEnumValues == null) {
fail(THE_ENUM + "'" + expectedClassName
+ "' does not contain any enum constants. Make sure to implement them.");
return;
}
if (expectedEnumValues.length() != observedEnumValues.length) {
fail(THE_ENUM + "'" + expectedClassName
+ "' does not contain all the expected enum values. Make sure to implement the missing enums.");
return;
}
for (int i = 0; i < expectedEnumValues.length(); i++) {
String expectedEnumValue = expectedEnumValues.getString(i);
boolean enumValueExists = false;
for (Object observedEnumValue : observedEnumValues) {
if (expectedEnumValue.equals(((Enum<?>) observedEnumValue).name())) {
enumValueExists = true;
break;
}
}
if (!enumValueExists) {
fail(THE_CLASS + "'" + expectedClassName + "' does not include the enum value: " + expectedEnumValue
+ ". Make sure to implement it as expected.");
}
}
}

/**
* This method checks if the type of an observed attribute matches the expected
* one. It first checks if the type of the attribute is a generic one or not. In
* the first case, it sees if the main and the generic types match, otherwise it
* only looks up the simple name of the attribute.
*
* @param observedAttribute The observed attribute we need to check.
* @param expectedTypeName The name of the expected type.
* @return True, if the types match, false otherwise.
*/
protected boolean checkType(Field observedAttribute, String expectedTypeName) {
boolean expectedTypeIsGeneric = expectedTypeName.contains("<") && expectedTypeName.contains(">");
if (expectedTypeIsGeneric) {
boolean mainTypeIsRight;
boolean genericTypeIsRight = false;

String expectedMainTypeName = expectedTypeName.split("<")[0];
String observedMainTypeName = observedAttribute.getType().getSimpleName();
mainTypeIsRight = expectedMainTypeName.equals(observedMainTypeName);

if (observedAttribute.getGenericType() instanceof ParameterizedType) {
Type observedGenericType = observedAttribute.getGenericType();
// this removes all package names, see section 4.5.1 of the JLS
String observedGenericTypeName = PACKAGE_NAME_IN_GENERIC_TYPE.matcher(observedGenericType.toString())
.replaceAll("");
genericTypeIsRight = expectedTypeName.equals(observedGenericTypeName);
}

return mainTypeIsRight && genericTypeIsRight;
}
String observedTypeName = observedAttribute.getType().getSimpleName();
return expectedTypeName.equals(observedTypeName);
}
}
Loading

0 comments on commit 3675b57

Please sign in to comment.