diff --git a/src/main/java/de/tum/in/test/api/structural/ClassTestProvider.java b/src/main/java/de/tum/in/test/api/structural/ClassTestProvider.java index fcec1da5..35a8bfed 100644 --- a/src/main/java/de/tum/in/test/api/structural/ClassTestProvider.java +++ b/src/main/java/de/tum/in/test/api/structural/ClassTestProvider.java @@ -112,6 +112,15 @@ private static void checkBasicClassProperties(String expectedClassName, Class && !Modifier.isInterface(observedClass.getModifiers())) { fail(THE_TYPE + "'" + expectedClassName + "' is not an interface as it is expected."); } + if (expectedClassPropertiesJSON.has(JSON_PROPERTY_MODIFIERS)) { + JSONArray expectedModifiers = getExpectedJsonProperty(expectedClassPropertiesJSON, JSON_PROPERTY_MODIFIERS); + boolean modifiersAreCorrect = checkModifiers(Modifier.toString(observedClass.getModifiers()).split(" "), + expectedModifiers); + if (!modifiersAreCorrect) { + fail("The modifier(s) (access type, abstract, etc.) of " + expectedClassName + + NOT_IMPLEMENTED_AS_EXPECTED); + } + } } private static boolean checkBooleanOf(JSONObject expectedClassPropertiesJSON, String booleanProperty) { diff --git a/src/main/java/de/tum/in/test/api/structural/StructuralTestProvider.java b/src/main/java/de/tum/in/test/api/structural/StructuralTestProvider.java index b2bccb36..fd8c3d45 100644 --- a/src/main/java/de/tum/in/test/api/structural/StructuralTestProvider.java +++ b/src/main/java/de/tum/in/test/api/structural/StructuralTestProvider.java @@ -10,9 +10,12 @@ import java.net.URL; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.json.JSONArray; import org.json.JSONObject; @@ -38,7 +41,7 @@ * their access modifiers, annotations and types and the declared enum values of * an enum * - * + * * All these elements are tests based on the test.json that specifies the * structural oracle, i.e. how the solution has to look like in terms of * structural elements. Note: the file test.json can be automatically generated @@ -54,7 +57,7 @@ * JUnit) If no attributes and no enums should be tested for correctness, remove * {@link AttributeTestProvider}, otherwise one test will fail (limitation of * JUnit) - * + * * @author Stephan Krusche (krusche@in.tum.de) * @version 5.0 (11.11.2020) */ @@ -112,7 +115,8 @@ protected static Class findClassForTestType(ExpectedClassStructure expectedCl fail(classNameScanMessage); } try { - return Class.forName(expectedClassStructure.getQualifiedClassName()); + return Class.forName(expectedClassStructure.getQualifiedClassName(), false, + StructuralTestProvider.class.getClassLoader()); } catch (@SuppressWarnings("unused") ClassNotFoundException e) { // Note: this error happens when the ClassNameScanner finds the correct file, // e.g. 'Course.java', but the class 'Course' was not yet created correctly in @@ -125,7 +129,7 @@ protected static Class findClassForTestType(ExpectedClassStructure expectedCl /** * get the expected elements or an empty JSON array - * + * * @param element the class, attribute, method or constructor JSON * object element * @param jsonPropertyKey the key used in JSON @@ -152,18 +156,54 @@ protected static boolean checkModifiers(String[] observedModifiers, JSONArray ex if (Arrays.equals(observedModifiers, new String[] { "" }) && expectedModifiers.length() == 0) { return true; } + /* - * If the number of the modifiers does not match, then the modifiers per se do - * not match either. + * Otherwise check if all expected necessary modifiers are contained in the + * array of the observed ones and if any forbidden modifiers were used. */ - if (observedModifiers.length != expectedModifiers.length()) { - return false; + Set modifierSpecifications = new HashSet<>(); + for (int i = 0; i < expectedModifiers.length(); i++) { + modifierSpecifications.add(ModifierSpecification.getModifierForJsonString(expectedModifiers.getString(i))); + } + Set observedModifiersSet = Set.of(observedModifiers); + Set allowedModifiers = modifierSpecifications.stream().map(ModifierSpecification::getModifier) + .collect(Collectors.toSet()); + boolean hasAllNecessaryModifiers = modifierSpecifications.stream().filter(ModifierSpecification::isRequired) + .map(ModifierSpecification::getModifier).allMatch(observedModifiersSet::contains); + boolean hasForbiddenModifier = observedModifiersSet.stream() + .anyMatch(modifier -> !allowedModifiers.contains(modifier)); + + return hasAllNecessaryModifiers && !hasForbiddenModifier; + } + + private static final class ModifierSpecification { + + private final String modifier; + private final boolean optional; + + private ModifierSpecification(String modifier, boolean optional) { + this.modifier = Objects.requireNonNull(modifier); + this.optional = optional; + } + + String getModifier() { + return modifier; + } + + boolean isRequired() { + return !optional; + } + + static ModifierSpecification getModifierForJsonString(String jsonString) { + String[] sections = jsonString.split(":", -1); + if (sections.length == 1) { + return new ModifierSpecification(jsonString, false); + } else if (sections[0].equals("optional")) { + return new ModifierSpecification(sections[1].trim(), true); + } else { + throw new IllegalArgumentException("Invalid entry for modifier: '" + jsonString + "'"); + } } - /* - * Otherwise check if all expected modifiers are contained in the array of the - * observed ones. If at least one isn't, then the modifiers don't match. - */ - return Arrays.asList(observedModifiers).containsAll(expectedModifiers.toList()); } protected static boolean checkAnnotations(Annotation[] observedAnnotations, JSONArray expectedAnnotations) { diff --git a/src/test/java/de/tum/in/test/api/StructuralTest.java b/src/test/java/de/tum/in/test/api/StructuralTest.java index 23d62ec7..00ba419e 100644 --- a/src/test/java/de/tum/in/test/api/StructuralTest.java +++ b/src/test/java/de/tum/in/test/api/StructuralTest.java @@ -21,6 +21,7 @@ class StructuralTest { private final String testAttributesSomeClass = "testAttributes()/dynamic-test:#2"; private final String testAttributesSomeEnum = "testAttributes()/dynamic-test:#3"; private final String testAttributesSomeAbstractClass = "testAttributes()/dynamic-test:#4"; + private final String testAttributesSomeFailingClass = "testAttributes()/dynamic-test:#5"; private final String testClassDoesNotExist = "testClasses()/dynamic-test:#1"; private final String testClassSomeInterface = "testClasses()/dynamic-test:#2"; private final String testClassMisspelledClas = "testClasses()/dynamic-test:#3"; @@ -28,6 +29,7 @@ class StructuralTest { private final String testClassSomeEnum = "testClasses()/dynamic-test:#5"; private final String testClassSomeAbstractClass = "testClasses()/dynamic-test:#6"; private final String testClassMisspelledclass = "testClasses()/dynamic-test:#7"; + private final String testClassSomeFailingClass = "testClasses()/dynamic-test:#8"; private final String testConstructorsSomeClass = "testConstructors()/dynamic-test:#1"; private final String testConstructorsSomeEnum = "testConstructors()/dynamic-test:#2"; private final String testConstructorsSomeAbstractClass = "testConstructors()/dynamic-test:#3"; @@ -62,6 +64,12 @@ void test_testAttributesSomeAbstractClass() { + "of the class 'SomeAbstractClass' are not implemented as expected.")); } + @TestTest + void test_testAttributesSomeFailingClass() { + tests.assertThatEvents().haveExactly(1, testFailedWith(testAttributesSomeFailingClass, + IllegalArgumentException.class, "Invalid entry for modifier: 'penguin: final'")); + } + @TestTest void test_testClassDoesNotExist() { tests.assertThatEvents().haveExactly(1, testFailedWith(testClassDoesNotExist, AssertionFailedError.class, @@ -107,6 +115,12 @@ void test_testClassMisspelledclass() { + "Check for wrong upper case / lower case lettering.")); } + @TestTest + void test_testClassSomeFailingClass() { + tests.assertThatEvents().haveExactly(1, testFailedWith(testClassSomeFailingClass, AssertionFailedError.class, + "The modifier(s) (access type, abstract, etc.) of SomeFailingClass are not implemented as expected.")); + } + @TestTest void test_testConstructorsSomeClass() { tests.assertThatEvents().haveExactly(1, diff --git a/src/test/java/de/tum/in/testuser/subject/structural/SomeClass.java b/src/test/java/de/tum/in/testuser/subject/structural/SomeClass.java index 59bd1d76..0b6dcc09 100644 --- a/src/test/java/de/tum/in/testuser/subject/structural/SomeClass.java +++ b/src/test/java/de/tum/in/testuser/subject/structural/SomeClass.java @@ -11,6 +11,7 @@ public class SomeClass implements SomeInterface { private String someAttribute; private Integer anotherAttribute; private List> doSomethingOperations; + private final int someFinalAttribute = SOME_CONSTANT; public SomeClass() { } diff --git a/src/test/resources/de/tum/in/testuser/test.json b/src/test/resources/de/tum/in/testuser/test.json index 38172677..d3e3f537 100644 --- a/src/test/resources/de/tum/in/testuser/test.json +++ b/src/test/resources/de/tum/in/testuser/test.json @@ -73,12 +73,16 @@ "type" : "int" }, { "name" : "someAttribute", - "modifiers" : [ "private" ], + "modifiers" : [ "private" , "optional: final"], "type" : "String" }, { "name" : "doSomethingOperations", "modifiers" : [ "private" ], "type" : "List>" + }, { + "name" : "someFinalAttribute", + "modifiers" : [ "private" , "optional: final"], + "type" : "int" } ] }, { "class" : { @@ -100,7 +104,8 @@ "class" : { "name" : "SomeAbstractClass", "package" : "de.tum.in.testuser.subject.structural", - "isAbstract" : true + "isAbstract" : true, + "modifiers" : ["public", "abstract"] }, "methods" : [ { "name" : "doNothing", @@ -122,4 +127,15 @@ "package" : "de.tum.in.testuser.subject.structural", "isInterface" : true } +}, { + "class" : { + "name" : "SomeFailingClass", + "package" : "de.tum.in.testuser.subject.structural", + "modifiers" : [ "public", "final" ] + }, + "attributes" : [ { + "name" : "SOME_CONSTANT", + "modifiers" : [ "penguin: final" ], + "type" : "int" + } ] } ]