Architecture¶
flowchart TB
subgraph EX["Example exercise (test/)"]
direction LR
TM["TestManager<br/>@LevenshteinTest + @Policy"]
POL[("SecurityPolicy.yaml")]
subgraph WR["wrappers"]
direction TB
DW[DrivableWrapper]
AW[AbstrWrapper]
CRW[CarWrapper]
end
TM -- uses --> WR
TM -. "@Policy" .-> POL
end
subgraph FW["Framework (framework/, published)"]
direction LR
LT["@LevenshteinTest<br/>@HiddenLevenshteinTest"]
SL[StructuralLevenshtein]
CW["ClassWrapper<T>"]
MW["AttributeWrapper<br/>MethodWrapper<br/>ConstructorWrapper"]
W["Wrapper<T>"]
WP["WrapperProperty<T>"]
U["Utils · LevenshteinSettings<br/>Messages"]
CW --> W
MW --> W
W --> WP
end
subgraph ST["Student code (assignment/src), supervised by Ares 2"]
direction LR
D["«interface»<br/>Driveable"]
AV["«abstract»<br/>AbstractVehicle"]
C[Car]
C -- extends --> AV
C -. implements .-> D
end
TM -. annotated with .-> LT
TM -- generates tests --> SL
WR -- extend --> CW
WR == "verify (fuzzy)" ==> ST
classDef fw fill:#2C3E50,stroke:#1a252f,color:#ffffff
classDef ex fill:#27AE60,stroke:#1e8449,color:#ffffff
classDef tests fill:#F39C12,stroke:#b9770e,color:#000000
classDef student fill:#E74C3C,stroke:#a93226,color:#ffffff
class LT,SL,W,CW,MW,WP,U fw
class DW,AW,CRW ex
class TM,POL tests
class D,AV,C student
Every expected class of the student code has a wrapper (a ClassWrapper subclass) that describes it and
finds the student's actual class and members, even with small naming deviations. TestManager generates
the structural tests from the wrappers and uses them for the behavioural tests, under Ares 2 supervision
(@LevenshteinTest + @Policy). Detailed class diagrams of the framework and of the example wrappers follow
below.
io.github.valentinherrmann.levenshtein (framework, published)
* Wrapper<T>: base of all wrappers (name, modifiers, existence, messages)
* ClassWrapper<T>: classes, abstract classes and interfaces, including superclass, interfaces and instantiation
* AttributeWrapper<T,V>, MethodWrapper<T,R>, ConstructorWrapper<T>: members
* GenericClassWrapper<T>: wraps an already loaded class (actual superclass and interfaces)
* TypeRef (package-private): an expected member type, a fixed Class or a student class given by its
ClassWrapper (or a supplier of one), resolved when the member is checked; ClassWrapper.reference(name, package)
refers to a student class by name only
* WrapperProperty<T>: expected vs. actual value plus existence
* StructuralLevenshtein: JUnit DynamicTest factory
* LevenshteinTest / HiddenLevenshteinTest: composed Ares 2 annotations
* LevenshteinSettings: runtime configuration (thresholds, language, getter/setter comparison)
* Messages: German/English feedback
* Utils: Levenshtein distance, type compatibility, safeCast
example-maven/ and example-gradle/ (not published): the same example exercise as two standalone
projects, one per build tool. Their assignment/ and test/ folders are identical (CI checks this) except
for one line in SecurityPolicy.yaml.
* assignment/src/.../vehicles: the "student" solution (Car, AbstractVehicle, Driveable), explained
in SIMPLIFIED_CONCEPTS.md
* test/.../tests: TestManager (tests), TestAbstr/TestImpl/TestInterface (test logic),
Constants (Exercise Variants), wrappers/*, SecurityPolicy.yaml
Class diagrams¶
The diagrams are Mermaid and render directly on GitHub and on the docs site; keep them in sync with the code when public signatures change.
Framework: wrappers (io.github.valentinherrmann.levenshtein)¶
Every wrapper describes one expected element of the student code and looks it up once, on first use.
WrapperProperty stores the expected and the actual value of each part (name, modifiers, types, ...) and
its Existence; the worst part decides the overall state.
classDiagram
direction TB
class Wrapper~T~ {
<<abstract>>
#WrapperProperty~String~ name
#WrapperProperty~String~ modifiers
#Existence existence
-ClassWrapper~T~ parentClassWrapper
+verifyExistence(boolean throwAssertion)*
+getOverallExistence() Existence
+verifyModifiers(int modifierBitmask)
+getParentClassWrapper() ClassWrapper~T~
+getExpectedName() String
+getActualName() String
+getActualModifiers() String
+expectedToString()* String
+actualToString()* String
#findWithDeviation()*
#parseExistence()*
#ensureChecked()
#usability() Existence
}
class ClassWrapper~T~ {
<<abstract>>
-String expectedPackage
-Class~T~ clazz
#T obj
~WrapperProperty superClassWrapper
~WrapperProperty interfaceWrappers
+getClazz() Class~T~
+reference(String name, String package)$ ClassWrapper~V~
+getExpectedPackage() String
+getObj(boolean forceNew)* Object
+getObj() Object
+newObj() Object
+getObj(boolean forceNew, ConstructorWrapper ctor, Object... args) T
+setCachedObj(Object obj)
+getDynamicSubclassObj(ConstructorWrapper ctor, Object... args) Object
+newObjWithoutConstructor() Object
+newObjOverriding(Map overrides) Object
+mock() Mock~T~
+verifySuperClass()
+verifyInterfaces()
+getAttributeWrappers() List~Wrapper~T~~
+getMethodWrappers() List~Wrapper~T~~
+getConstructorWrappers() List~Wrapper~T~~
+testGetter(AttributeWrapper attribute, MethodWrapper getter)
+testGettersSetters(AttributeWrapper attribute, MethodWrapper... gettersSetters)
+testGettersSetters(AttributeWrapper attribute, Predicate setterCondition, MethodWrapper... gettersSetters)
+testGettersSetters(AttributeWrapper attribute, List sampleValues, MethodWrapper... gettersSetters)
+testGettersSetters(AttributeWrapper attribute, List sampleValues, Predicate setterCondition, MethodWrapper... gettersSetters)
}
class GenericClassWrapper~T~ {
+GenericClassWrapper(Class~T~ clz)
+getObj(boolean forceNew) Object
}
class AttributeWrapper["AttributeWrapper<T, V>"] {
-Field field
-WrapperProperty type
-TypeRef typeRef
+getExpectedType() Class
+getValue() V
+getValue(Object obj) V
+setValue(Object value)
+setValue(Object obj, Object value)
}
class MethodWrapper["MethodWrapper<T, R>"] {
-TypeRef[] paramRefs
-TypeRef returnRef
-Class[] paramTypes
-WrapperProperty params
-WrapperProperty returnType
-Method method
+invoke(Object... params) R
+invokeOnSpecificObject(Object obj, Object... params) R
+invokeWithContext(Supplier~String~ context, Object... params) R
+invokeOnSpecificObjectWithContext(Supplier~String~ context, Object obj, Object... params) R
+invokeExpectingException(Class~X~ expected, Object obj, Object... params) X
+getParamTypes() Class[]
+getExpectedReturnType() Class
}
class ConstructorWrapper~T~ {
-TypeRef[] paramRefs
-Class[] paramTypes
-WrapperProperty params
~Constructor~T~ constructor
+invoke(Object... args) T
+invokeExpectingException(Class~X~ expected, Object... args) X
+getParamTypes() Class[]
+getActualParamTypes() Class[]
}
class Mock~T~ {
-ClassWrapper~T~ classWrapper
+override(MethodWrapper method, Function function) Mock~T~
+returning(MethodWrapper method, Object value) Mock~T~
+othersReturnDefaults() Mock~T~
+obj() Object
+calls(MethodWrapper method) List~Object[]~
+assertCalled(MethodWrapper method, int times)
+assertCalled(MethodWrapper method, int times, Supplier~String~ context)
+assertNotCalled(MethodWrapper method)
+resetCalls()
}
class TypeRef {
<<interface>>
+resolve() Class
+expectedSimpleName() String
}
class WrapperProperty~T~ {
~T expected
~T actual
~Existence existence
+getExpected() T
+getActual() T
+getExistence() Existence
}
class Existence {
<<enumeration>>
UNCHECKED
EXACT
DEVIATES
MISSING
+severity() int
+worst(Existence a, Existence b)$ Existence
}
ClassWrapper --|> Wrapper
GenericClassWrapper --|> ClassWrapper
AttributeWrapper --|> Wrapper
MethodWrapper --|> Wrapper
ConstructorWrapper --|> Wrapper
Wrapper *-- WrapperProperty : name, modifiers
ClassWrapper *-- WrapperProperty : superClassWrapper, interfaceWrappers
AttributeWrapper *-- WrapperProperty : type
MethodWrapper *-- WrapperProperty : params, returnType
ConstructorWrapper *-- WrapperProperty : params
AttributeWrapper *-- TypeRef : typeRef
MethodWrapper *-- TypeRef : returnRef, paramRefs
ConstructorWrapper *-- TypeRef : paramRefs
TypeRef --> ClassWrapper : resolves via getClazz()
WrapperProperty --> Existence : existence
ClassWrapper ..> Mock : mock()
Mock --> MethodWrapper : overrides, calls
Wrapper --> ClassWrapper : parentClassWrapper
Framework: test generation, configuration and annotations¶
classDiagram
direction LR
class StructuralLevenshtein {
<<final>>
+structuralTestFactory(DetailLevel level, ClassWrapper... classWrappers)$ List~DynamicTest~
+structuralTestFactory(DetailLevel level, boolean skipEmpty, ClassWrapper... classWrappers)$ List~DynamicTest~
+structuralTestTemplate(List wrappers)$
}
class DetailLevel {
<<enumeration>>
ONE_FOR_EVERYTHING
ONE_PER_CLASS
ONE_PER_MEMBER_CATEGORY
ONE_PER_MEMBER
}
class Utils {
<<final>>
+levenshteinDistance(String s1, String s2)$ int
+levenshteinDistancePercent(String expected, String actual)$ double
+isNameWithinDeviation(String expected, String actual, int threshold)$ boolean
+safeCast(Object val, Class castTo)$ Object
+verifyType(WrapperProperty typeProperty, Class actualType)$
+isNumericDeviation(Class expected, Class actual)$ boolean
+canContain(Class actualType, Class expectedType)$ boolean
+unwrapPrimitive(Class type)$ Class
+toWrapperType(Class type)$ Class
}
class LevenshteinSettings {
<<final>>
+getLanguage()$ Language
+setLanguage(Language language)$
+setClassNameDeviationThreshold(int percent)$
+setMethodNameDeviationThreshold(int percent)$
+setAttributeNameDeviationThreshold(int percent)$
+reset()$
}
class Language {
<<enumeration>>
DEUTSCH
ENGLISH
}
class Messages {
<<enumeration>>
WRAPPER_DEVIATION
CLASS_NOT_IMPLEMENTED
...
+get() String
+format(Object... args) String
}
class Invocations {
<<package-private>>
~invoke(Method method, Object target, Object... args)$ Object
~newInstance(Constructor ctor, Object... args)$ Object
~rethrowIfCritical(Throwable t)$
~describe(Throwable t)$ String
~checkArguments(Class[] parameterTypes, Object[] args, String signature)$ Object[]
~describeArguments(Object[] args)$ String
~describeValue(Object value)$ String
}
class LevenshteinTest {
<<annotation>>
}
class HiddenLevenshteinTest {
<<annotation>>
}
class ReflectionTestUtils {
<<Ares 2>>
}
StructuralLevenshtein ..> DetailLevel
LevenshteinSettings ..> Language
Messages ..> LevenshteinSettings : reads language
Invocations ..> ReflectionTestUtils : rethrowing invocations
note for LevenshteinTest "= @Public + @StrictTimeout(5) + @MirrorOutput"
note for HiddenLevenshteinTest "= @Hidden + @StrictTimeout(5) + @MirrorOutput"
Example exercise: wrappers (io.github.valentinherrmann.example.tests.wrappers)¶
One wrapper per expected class of the student code. Each wrapper declares its members as
AttributeWrapper, MethodWrapper and ConstructorWrapper fields (found by ClassWrapper via
reflection for the structural tests) and exposes them through accessors for the behavioural tests.
Expected names and types come from Constants (Exercise Variants pattern).
classDiagram
direction BT
class ClassWrapper~T~ {
<<abstract, framework>>
+getObj(boolean forceNew)* Object
}
class DrivableWrapper~T~ {
-AttributeWrapper maxSpeed
-MethodWrapper startMethod
-MethodWrapper getSpeedMethod
+DrivableWrapper()
+maxSpeed() AttributeWrapper
+startMethod() MethodWrapper
+getSpeedMethod() MethodWrapper
+getObj(boolean forceNew) Object
}
class AbstrWrapper~T~ {
-AttributeWrapper manufacturer
-AttributeWrapper year
-ConstructorWrapper constructor
-MethodWrapper getManufacturer
-MethodWrapper getYear
-MethodWrapper calculateCost
-MethodWrapper getInfo
+AbstrWrapper()
+manufacturer() AttributeWrapper
+year() AttributeWrapper
+constructor() ConstructorWrapper
+getManufacturer() MethodWrapper
+getYear() MethodWrapper
+calculateCost() MethodWrapper
+getInfo() MethodWrapper
+getObj(boolean forceNew) T
+setObj(Object obj, boolean force)
}
class CarWrapper~T~ {
-AttributeWrapper price
-AttributeWrapper speed
-ConstructorWrapper constructor_full
-ConstructorWrapper constructor_default
-MethodWrapper getPrice
-MethodWrapper start
-MethodWrapper getSpeed
-MethodWrapper calculateCost
-MethodWrapper calculateCostYears
-MethodWrapper getInfo
+CarWrapper(ClassWrapper superClassWrapper, ClassWrapper... interfaceWrappers)
+price() AttributeWrapper
+speed() AttributeWrapper
+constructor_full() ConstructorWrapper
+constructor_default() ConstructorWrapper
+getPrice() MethodWrapper
+startMethod() MethodWrapper
+getSpeed() MethodWrapper
+calculateCost() MethodWrapper
+calculateCostYears() MethodWrapper
+getInfo() MethodWrapper
+getObj(boolean forceNew) Object
}
DrivableWrapper --|> ClassWrapper
AbstrWrapper --|> ClassWrapper
CarWrapper --|> ClassWrapper
CarWrapper ..> AbstrWrapper : superClassWrapper
CarWrapper ..> DrivableWrapper : interfaceWrappers
note for DrivableWrapper "Wraps the interface Driveable:\nMAX_SPEED, start(), getSpeed()"
note for AbstrWrapper "Wraps the abstract class AbstractVehicle:\nmanufacturer, year, abstract calculateCost(), getInfo()"
note for CarWrapper "Wraps Car (extends AbstractVehicle, implements Driveable):\nconstructor and method overloading, overriding"
Each wrapper verifies existence, names, types and modifiers of its class with Levenshtein distance tolerance; see How Matching Works.