Architecture¶
How the framework, an exam's tests and the student code fit together, which class does what, and how a wrapper gets from "expected" to "found". For the exact signatures of every public class and method see the API Reference (Javadoc); the diagrams on this page show the parts you work with, not every method.
Overview¶
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["Failure · Mock<br/>LevenshteinSettings · Messages · Utils"]
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. The wrapper
finds the student's actual class and members, even with small naming deviations. TestManager generates the
structural tests from the wrappers and uses the wrappers for the behavioural tests. Every test runs under Ares 2
supervision (@LevenshteinTest + @Policy).
What happens in a test run¶
- Building the wrappers (static field or
@BeforeAll): each wrapper stores only what is expected: names, types, modifiers. Nothing is looked up yet, and no student code runs. - First use, inside a supervised test: the wrapper looks up its element once
(How Matching Works) and stores each part as a
WrapperProperty(expected, actual,EXACT/DEVIATES/MISSING). The worst part is the wrapper's verdict. - Structural tests (
StructuralLevenshtein.structuralTestFactory) fail for every wrapper that is notEXACTand print itsDEVIATION/MISSINGmessage. - Behavioural tests call the student's element through the wrapper (
invoke,getValue,newObj, ...). ADEVIATESelement is used as it is; aMISSINGone fails the call. Exceptions of student code become a failure block (Failure). - Between tests,
StaticStateReset(part of@LevenshteinTest) resets attributes that arestaticalthough the test expects instance attributes, so tests do not influence each other.
Packages and classes¶
io.github.valentinherrmann.levenshtein (framework, published)
| Class | Role |
|---|---|
Wrapper<T> |
Base of all wrappers: expected name and modifiers, verdict, messages, missing/unexpected modifiers |
ClassWrapper<T> |
A class, abstract class or interface: superclass, interfaces, instantiation (newObj, newObjWithoutConstructor, newObjOverriding, mock), testGettersSetters, ClassWrapper.reference(name, package) |
AttributeWrapper<T,V>, MethodWrapper<T,R>, ConstructorWrapper<T> |
Members: lookup, getValue/setValue, invoke, invokeExpectingException |
GenericClassWrapper<T> |
Wraps a class that is already loaded; used for the student's actual superclass and interfaces, not for expected classes |
WrapperProperty<T> |
Expected vs. actual value of one part plus its Existence |
StructuralLevenshtein |
JUnit DynamicTest factory for the structural tests (DetailLevel) |
Failure |
The structured failure block (Method:, Scenario:, Input:, Expected:, Actual:), also for your own checks |
Mock<T> |
Test-owned overrides and call records on an object of a student class |
LevenshteinTest / HiddenLevenshteinTest |
Composed annotations: @Public/@Hidden + @StrictTimeout(5) + @MirrorOutput + @ExtendWith(StaticStateReset.class) |
StaticStateReset |
JUnit extension that resets unexpectedly static attributes before each test |
LevenshteinSettings |
Runtime configuration: language, thresholds, decimals, getter/setter comparison, unexpected modifiers |
Messages |
German/English feedback texts |
Utils |
Levenshtein distance, type compatibility, safeCast |
TypeRef, Invocations, GettersSettersTest, MockClasses (package-private) |
Member types given as ClassWrapper, calling student code, getter/setter test, generated subclasses |
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 inSIMPLIFIED_CONCEPTS.mdassignment-failing/src: a solution with deliberate defects that shows the framework's messagestest/.../tests:TestManager(tests),TestAbstr/TestImpl/TestInterface(test logic),Constants(Exercise Variants),wrappers/*,SecurityPolicy.yaml
Class diagrams¶
The diagrams are Mermaid and render on GitHub and on the docs site. They show the public API you use in tests; keep them in sync when it changes.
Wrappers¶
Every wrapper describes one expected element of the student code. ClassWrapper holds the member wrappers of its
class; each member wrapper knows its parent ClassWrapper.
classDiagram
direction TB
class Wrapper~T~ {
<<abstract>>
+getOverallExistence() Existence
+getExpectedName() String
+getActualName() String
+getActualModifiers() String
+getMissingModifiers() List~String~
+getUnexpectedModifiers() List~String~
+getParentClassWrapper() ClassWrapper~T~
+verifyExistence(boolean throwAssertion)
}
class ClassWrapper~T~ {
<<abstract>>
+reference(String name, String package)$ ClassWrapper
+getClazz() Class~T~
+getObj(boolean forceNew)* Object
+getObj() Object
+newObj() Object
+getObj(boolean forceNew, ConstructorWrapper ctor, Object... args) T
+setCachedObj(Object obj)
+newObjWithoutConstructor() Object
+newObjOverriding(Map overrides) Object
+mock() Mock~T~
+testGettersSetters(AttributeWrapper attribute, ...)
}
class GenericClassWrapper~T~ {
+GenericClassWrapper(Class~T~ clz)
}
class AttributeWrapper["AttributeWrapper<T, V>"] {
+getExpectedType() Class
+getValue() V
+getValue(Object obj) V
+setValue(Object value)
+setValue(Object obj, Object value)
}
class MethodWrapper["MethodWrapper<T, R>"] {
+invoke(Object... params) R
+invokeOnSpecificObject(Object obj, Object... params) R
+invokeWithContext(Supplier context, Object... params) R
+invokeExpectingException(Class expected, Object obj, Object... params) X
+getParamTypes() Class[]
+getActualParamTypes() Class[]
+getExpectedReturnType() Class
}
class ConstructorWrapper~T~ {
+invoke(Object... args) T
+invokeExpectingException(Class expected, Object... args) X
+getParamTypes() Class[]
+getActualParamTypes() Class[]
}
ClassWrapper --|> Wrapper
GenericClassWrapper --|> ClassWrapper
AttributeWrapper --|> Wrapper
MethodWrapper --|> Wrapper
ConstructorWrapper --|> Wrapper
ClassWrapper o-- AttributeWrapper : fields of the subclass
ClassWrapper o-- MethodWrapper
ClassWrapper o-- ConstructorWrapper
Verdicts and member types¶
Each part of a wrapper (name, modifiers, type, parameters, superclass, ...) is a WrapperProperty with its own
Existence; the worst one is the wrapper's verdict. A member type that is a student class is a TypeRef, resolved
through its ClassWrapper when the member is checked.
classDiagram
direction LR
class Wrapper~T~ {
<<abstract>>
}
class WrapperProperty~T~ {
+getExpected() T
+getActual() T
+getExistence() Existence
}
class Existence {
<<enumeration>>
UNCHECKED
EXACT
DEVIATES
MISSING
+worst(Existence a, Existence b)$ Existence
}
class TypeRef {
<<interface, package-private>>
+resolve() Class
+expectedSimpleName() String
}
class ClassWrapper~T~ {
<<abstract>>
}
Wrapper *-- WrapperProperty : name, modifiers, type, params, ...
WrapperProperty --> Existence
Wrapper *-- TypeRef : attribute, return and parameter types
TypeRef --> ClassWrapper : a fixed Class or a student class
Failures, mocks and settings¶
classDiagram
direction LR
class Failure {
<<final>>
+of(MethodWrapper method)$ Failure
+of(ConstructorWrapper ctor)$ Failure
+of(String member)$ Failure
+scenario(String text) Failure
+input(String text) Failure
+arguments(Object... args) Failure
+expected(Object value) Failure
+actual(Object value) Failure
+thrown(Throwable t) Failure
+cases(int failed, int total) Failure
+error() AssertionError
+format(Object value)$ String
+call(ThrowingSupplier code)$ R
}
class Mock~T~ {
<<final>>
+override(MethodWrapper method, Function function) Mock~T~
+returning(MethodWrapper method, Object value) Mock~T~
+othersReturnDefaults() Mock~T~
+obj() Object
+callCount(MethodWrapper method) long
+calls(MethodWrapper method) List~Object[]~
+assertCalled(MethodWrapper method, int times)
+assertNotCalled(MethodWrapper method)
+resetCalls()
}
class LevenshteinSettings {
<<final>>
+setLanguage(Language language)$
+setClassNameDeviationThreshold(int percent)$
+setMethodNameDeviationThreshold(int percent)$
+setAttributeNameDeviationThreshold(int percent)$
+setDecimals(int decimals)$
+setGettersSettersRequireSameObject(boolean same)$
+setUnexpectedModifiers(UnexpectedModifiers mode)$
+reset()$
}
class Language {
<<enumeration>>
DEUTSCH
ENGLISH
}
class UnexpectedModifiers {
<<enumeration>>
DEVIATE
IGNORE
}
class ClassWrapper~T~ {
<<abstract>>
+mock() Mock~T~
}
class MethodWrapper["MethodWrapper<T, R>"]
ClassWrapper ..> Mock : creates
Mock --> MethodWrapper : overrides, records calls
Failure ..> MethodWrapper : Method line
Failure ..> LevenshteinSettings : language, decimals
LevenshteinSettings ..> Language
LevenshteinSettings ..> UnexpectedModifiers
Test generation and annotations¶
classDiagram
direction LR
class StructuralLevenshtein {
<<final>>
+structuralTestFactory(DetailLevel level, ClassWrapper... wrappers)$ List~DynamicTest~
+structuralTestFactory(DetailLevel level, boolean skipEmpty, ClassWrapper... wrappers)$ List~DynamicTest~
}
class DetailLevel {
<<enumeration>>
ONE_FOR_EVERYTHING
ONE_PER_CLASS
ONE_PER_MEMBER_CATEGORY
ONE_PER_MEMBER
}
class LevenshteinTest {
<<annotation>>
}
class HiddenLevenshteinTest {
<<annotation>>
}
class StaticStateReset {
<<JUnit extension>>
}
StructuralLevenshtein ..> DetailLevel
LevenshteinTest ..> StaticStateReset : @ExtendWith
HiddenLevenshteinTest ..> StaticStateReset : @ExtendWith
note for LevenshteinTest "= @Public + @StrictTimeout(5) + @MirrorOutput\n+ @ExtendWith(StaticStateReset.class)"
note for HiddenLevenshteinTest "= @Hidden + @StrictTimeout(5) + @MirrorOutput\n+ @ExtendWith(StaticStateReset.class)"
Example exercise: wrappers¶
One wrapper per expected class of the student code
(io.github.valentinherrmann.example.tests.wrappers). 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~ {
+maxSpeed() AttributeWrapper
+startMethod() MethodWrapper
+getSpeedMethod() MethodWrapper
+getObj(boolean forceNew) Object
}
class AbstrWrapper~T~ {
+manufacturer() AttributeWrapper
+year() AttributeWrapper
+constructor() ConstructorWrapper
+getManufacturer() MethodWrapper
+getYear() MethodWrapper
+calculateCost() MethodWrapper
+getInfo() MethodWrapper
+getObj(boolean forceNew) T
}
class CarWrapper~T~ {
+CarWrapper(ClassWrapper superClass, ClassWrapper... interfaces)
+price() AttributeWrapper
+speed() AttributeWrapper
+constructor_full() ConstructorWrapper
+constructor_default() ConstructorWrapper
+getPrice() MethodWrapper
+setPrice() 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 "interface Driveable:\nMAX_SPEED, start(), getSpeed()"
note for AbstrWrapper "abstract class AbstractVehicle:\nmanufacturer, year, abstract calculateCost(), getInfo()"
note for CarWrapper "Car extends AbstractVehicle implements Driveable:\noverloaded constructors and calculateCost"