@@ -18,9 +18,8 @@ import org.junit.jupiter.api.Order
1818import org.junit.jupiter.api.Test
1919import org.junit.jupiter.api.TestMethodOrder
2020import org.reflections.Reflections
21- import org.reflections.scanners.MethodParameterScanner
22- import org.reflections.scanners.SubTypesScanner
23- import org.reflections.util.ClasspathHelper
21+ import org.reflections.scanners.Scanners
22+ import org.reflections.scanners.Scanners.SubTypes
2423import org.reflections.util.ConfigurationBuilder
2524import org.reflections.util.FilterBuilder
2625import uk.co.jemos.podam.api.PodamFactoryImpl
@@ -36,21 +35,53 @@ import kotlin.reflect.jvm.javaGetter
3635@TestMethodOrder(OrderAnnotation ::class )
3736class DtoContractUnitTest {
3837
39- private val convertersReflections =
38+ private val convertersReflections = lazy {
4039 Reflections (
4140 ConfigurationBuilder ()
42- .filterInputsBy(FilterBuilder ().includePackage(" com.ecwid.apiclient.v3.converter" ))
43- .setUrls(ClasspathHelper .forPackage(" " ))
44- .setScanners(MethodParameterScanner ())
41+ .forPackage(" com.ecwid.apiclient.v3.converter" )
42+ .filterInputsBy(
43+ FilterBuilder ()
44+ .includePattern(" com\\ .ecwid\\ .apiclient\\ .v3\\ .converter\\ ..*" )
45+ )
46+ .setScanners(Scanners .MethodsReturn )
47+ )
48+ }
49+
50+ private val apiRequestClassesReflections = lazy {
51+ Reflections (
52+ ConfigurationBuilder ()
53+ .forPackage(ApiRequest ::class .java.packageName)
54+ .filterInputsBy(
55+ FilterBuilder ()
56+ .includePattern(ApiRequest ::class .java.packageName + " \\ ..*" )
57+ )
58+ .setScanners(SubTypes .filterResultsBy { true })
4559 )
60+ }
61+
62+ private val getDtoClassesToCheck = lazy {
63+ apiRequestClassesReflections
64+ .value
65+ .getSubTypesOf(Object ::class .java)
66+ .filterNot { clazz -> clazz.isInterface || clazz.isAnonymousClass }
67+ .filterNot { clazz ->
68+ try {
69+ clazz.kotlin.isCompanion
70+ } catch (ignore: UnsupportedOperationException ) {
71+ // Filtering file facades classes (*Kt classes) and synthetic classes (i.e. when-mappings classes)
72+ true
73+ }
74+ }
75+ .sortedBy { clazz -> clazz.canonicalName }
76+ }
4677
4778 @Test
4879 @Order(0 )
4980 fun `test all DTOs marked as data classes` () {
50- val dtoClasses = getDtoClassesToCheck()
51- assertFalse(dtoClasses .isEmpty())
81+ val dtoClassesToCheck = getDtoClassesToCheck.value
82+ assertFalse(dtoClassesToCheck .isEmpty())
5283
53- val problemDtoClasses = dtoClasses .filter { dtoClass ->
84+ val problemDtoClasses = dtoClassesToCheck .filter { dtoClass ->
5485 ! dtoClass.kotlin.isData && isDtoShouldBeMarkedAsDataClass(dtoClass)
5586 }
5687 assertTrue(problemDtoClasses.isEmpty()) {
@@ -61,11 +92,11 @@ class DtoContractUnitTest {
6192 @Test
6293 @Order(1 )
6394 fun `test all data classes DTOs has default constructor` () {
64- val dtoDataClasses = getDtoClassesToCheck()
95+ val dtoClassesToCheck = getDtoClassesToCheck.value
6596 .filter { dtoClass -> dtoClass.kotlin.isData }
66- assertFalse(dtoDataClasses .isEmpty())
97+ assertFalse(dtoClassesToCheck .isEmpty())
6798
68- val problemDtoClasses = dtoDataClasses .filter { dtoDataClass ->
99+ val problemDtoClasses = dtoClassesToCheck .filter { dtoDataClass ->
69100 val constructors = dtoDataClass.constructors
70101 val hasZeroArgConstructor = constructors.any { constructor -> constructor .parameters.isEmpty() }
71102 ! hasZeroArgConstructor && isDtoShouldHaveZeroArgConstructor(constructors)
@@ -80,11 +111,11 @@ class DtoContractUnitTest {
80111 @Test
81112 @Order(2 )
82113 fun `test all data classes DTOs has only val parameters in their primary constructors` () {
83- val dtoDataClasses = getDtoClassesToCheck()
114+ val dtoClassesToCheck = getDtoClassesToCheck.value
84115 .filter { dtoClass -> dtoClass.kotlin.isData }
85- assertFalse(dtoDataClasses .isEmpty())
116+ assertFalse(dtoClassesToCheck .isEmpty())
86117
87- val problemDtoClasses = dtoDataClasses .filter { dtoDataClass ->
118+ val problemDtoClasses = dtoClassesToCheck .filter { dtoDataClass ->
88119 isPrimaryConstructorHasMutableProperties(dtoDataClass)
89120 }
90121 assertTrue(problemDtoClasses.isEmpty()) {
@@ -104,12 +135,12 @@ class DtoContractUnitTest {
104135 ApiResultDTO ::class .java
105136 )
106137
107- val dtoDataClasses = getDtoClassesToCheck()
138+ val dtoClassesToCheck = getDtoClassesToCheck.value
108139 .filterNot { dtoClass -> dtoClass.isEnum }
109140 .filterNot { dtoClass -> dtoClass.packageName.startsWith(" com.ecwid.apiclient.v3.dto.common" ) }
110- assertFalse(dtoDataClasses .isEmpty())
141+ assertFalse(dtoClassesToCheck .isEmpty())
111142
112- val problemDtoClasses = dtoDataClasses
143+ val problemDtoClasses = dtoClassesToCheck
113144 .filterNot { dtoClass -> dtoClass.isClassifiedDTOOrEnclosingClass(* dtoMarkerInterfaces) }
114145 assertTrue(problemDtoClasses.isEmpty()) {
115146 val interfacesStr = dtoMarkerInterfaces.joinToString(separator = " , " ) { int -> int.simpleName }
@@ -121,7 +152,7 @@ class DtoContractUnitTest {
121152 @Test
122153 @Order(4 )
123154 fun `test all DTOs marked as 'preferably having non-nullable fields' have only non-nullable fields or fields added to exclusion list` () {
124- val dtoDataClasses = getDtoClassesToCheck()
155+ val dtoClassesToCheck = getDtoClassesToCheck.value
125156 .filter { dtoClass ->
126157 dtoClass.isClassifiedDTOOrEnclosingClass(
127158 ApiFetchedDTO ::class .java,
@@ -130,14 +161,14 @@ class DtoContractUnitTest {
130161 )
131162 }
132163 .filterNot { dtoClass -> dtoClass.kotlin.visibility == KVisibility .PRIVATE }
133- assertFalse(dtoDataClasses .isEmpty())
164+ assertFalse(dtoClassesToCheck .isEmpty())
134165
135166 val allowedOrIgnoredNullableProperties = nullablePropertyRules
136167 .filter { rule -> rule is AllowNullable || rule is IgnoreNullable }
137168 .map { rule -> rule.property }
138169 .toSet()
139170
140- val nullableProperties = dtoDataClasses
171+ val nullableProperties = dtoClassesToCheck
141172 .flatMap { dtoDataClass ->
142173 getPrimaryConstructorProperties(dtoDataClass)
143174 .filter { property -> property.returnType.isMarkedNullable }
@@ -170,20 +201,20 @@ class DtoContractUnitTest {
170201 @Test
171202 @Order(6 )
172203 fun `test all DTOs marked as 'preferably having nullable fields' have only nullable fields or fields added to exclusion list` () {
173- val dtoDataClasses = getDtoClassesToCheck()
204+ val dtoClassesToCheck = getDtoClassesToCheck.value
174205 .filter { dtoClass ->
175206 dtoClass.isClassifiedDTOOrEnclosingClass(
176207 ApiUpdatedDTO ::class .java
177208 )
178209 }
179- assertFalse(dtoDataClasses .isEmpty())
210+ assertFalse(dtoClassesToCheck .isEmpty())
180211
181212 val allowedOrIgnoredNonnullProperties = nonnullPropertyRules
182213 .filter { rule -> rule is AllowNonnull || rule is IgnoreNonnull }
183214 .map { rule -> rule.property }
184215 .toSet()
185216
186- val nonnullProperties = dtoDataClasses
217+ val nonnullProperties = dtoClassesToCheck
187218 .flatMap { dtoDataClass ->
188219 getPrimaryConstructorProperties(dtoDataClass)
189220 .filterNot { property -> property.returnType.isMarkedNullable }
@@ -216,7 +247,9 @@ class DtoContractUnitTest {
216247 @Test
217248 @Order(8 )
218249 fun `test fetched and updated DTOs correctly linked to each other` () {
219- val dtoClassesToCheck = getDtoClassesToCheck()
250+ val dtoClassesToCheck = getDtoClassesToCheck.value
251+ assertFalse(dtoClassesToCheck.isEmpty())
252+
220253 val fetchedDTOClassesToModifyKindMap = getFetchedDTOClassesToModifyKindMap(dtoClassesToCheck)
221254 val updatedDTOClassesToModifyKindMap = getUpdatedDTOClassesToModifyKindMap(dtoClassesToCheck)
222255
@@ -226,6 +259,7 @@ class DtoContractUnitTest {
226259 ApiFetchedDTO .ModifyKind .ReadOnly -> {
227260 // No UpdatedDTO to check
228261 }
262+
229263 is ApiFetchedDTO .ModifyKind .ReadWrite -> {
230264 val updatedDTOClass = kind.updatedDTOClass
231265 val updatedDtoModifyKind = updatedDTOClassesToModifyKindMap[updatedDTOClass]
@@ -236,6 +270,7 @@ class DtoContractUnitTest {
236270 " Classes ${dtoClass.qualifiedName} and ${updatedDTOClass.qualifiedName} does not links to each other"
237271 )
238272 }
273+
239274 null -> {
240275 fail(" Impossible situation" )
241276 }
@@ -254,9 +289,11 @@ class DtoContractUnitTest {
254289 ApiFetchedDTO .ModifyKind .ReadOnly -> {
255290 fail(" Updatable class ${dtoClass.qualifiedName} links to class ${fetchedDTOClass.qualifiedName} which is marked as read-only " )
256291 }
292+
257293 is ApiFetchedDTO .ModifyKind .ReadWrite -> {
258294 // Backlink was checked before
259295 }
296+
260297 null -> {
261298 fail(" Impossible situation" )
262299 }
@@ -269,7 +306,8 @@ class DtoContractUnitTest {
269306 @Test
270307 @Order(9 )
271308 fun `test fetched and updated DTOs fields list and their types are synchronized` () {
272- val dtoClassesToCheck = getDtoClassesToCheck()
309+ val dtoClassesToCheck = getDtoClassesToCheck.value
310+ assertFalse(dtoClassesToCheck.isEmpty())
273311
274312 val fetchedUpdatedDTOs = dtoClassesToCheck
275313 .filter { dtoClass ->
@@ -315,7 +353,9 @@ class DtoContractUnitTest {
315353 val dataStrategy = DTORandomDataProviderStrategy ()
316354 val factory = PodamFactoryImpl (dataStrategy)
317355
318- val dtoClassesToCheck = getDtoClassesToCheck()
356+ val dtoClassesToCheck = getDtoClassesToCheck.value
357+ assertFalse(dtoClassesToCheck.isEmpty())
358+
319359 val fetchedDTOClassesToModifyKindMap = getFetchedDTOClassesToModifyKindMap(dtoClassesToCheck)
320360
321361 val problemMessages = mutableListOf<String >()
@@ -326,13 +366,14 @@ class DtoContractUnitTest {
326366 // No UpdatedDTO to check
327367 null
328368 }
369+
329370 is ApiFetchedDTO .ModifyKind .ReadWrite -> {
330371 @Suppress(" UNCHECKED_CAST" )
331372 val fetchedDtoKClass = fetchedDtoClass as KClass <out ApiFetchedDTO >
332373 val updatedDTOClass = kind.updatedDTOClass
333374
334375 val toUpdatedMethod: Method ? =
335- findToUpdatedMethod(convertersReflections, fetchedDtoKClass, updatedDTOClass)
376+ findToUpdatedMethod(convertersReflections.value , fetchedDtoKClass, updatedDTOClass)
336377 if (toUpdatedMethod == null ) {
337378 problemMessages + = " Extension function with signature `${fetchedDtoKClass.java.canonicalName} .toUpdated(): ${updatedDTOClass.java.canonicalName} ` is not implemented"
338379 null
@@ -399,9 +440,11 @@ private fun FieldProblem.buildMessage(): String {
399440 FieldProblemKind .FIELD_NOT_FOUND -> {
400441 " All updatable fields from Fetched DTO must have corresponding field in Updated DTO."
401442 }
443+
402444 FieldProblemKind .TYPE_MUST_NOT_BE_REUSED -> {
403445 " Fields with the same name in Fetched and Updated DTOs must not share the same DTO (except for primitives and enums)"
404446 }
447+
405448 FieldProblemKind .FIELD_IS_NOT_MAP ,
406449 FieldProblemKind .FIELD_IS_NOT_LIST ,
407450 FieldProblemKind .PRIMITIVE_FIELDS_INCOMPATIBLE_TYPE -> {
@@ -415,6 +458,7 @@ private fun FieldProblem.buildMessage(): String {
415458 " If this field is read-only in Ecwid API you CAN add it as `ReadOnly()` exclusion to file `NonUpdatablePropertyRules.kt`.\n " +
416459 " You MUST NOT add exclusion with type Ignored() which is used only for old fields until they are fixed."
417460 }
461+
418462 FieldProblemKind .FIELD_IS_NOT_MAP ,
419463 FieldProblemKind .FIELD_IS_NOT_LIST ,
420464 FieldProblemKind .PRIMITIVE_FIELDS_INCOMPATIBLE_TYPE ,
@@ -475,10 +519,12 @@ private fun isDtoShouldBeMarkedAsDataClass(dtoClass: Class<*>): Boolean {
475519 // Sealed classes must not be instantiated by themself but their inheritors must be marked as data classes
476520 false
477521 }
522+
478523 kclass.objectInstance != null -> {
479524 // Singleton classes has no explicit constructor arguments so it cannot be marked as data class
480525 false
481526 }
527+
482528 else -> {
483529 // Classes that has only one zero-arg constructor cannot be marked as data class
484530 val constructors = dtoClass.constructors
@@ -541,19 +587,6 @@ private fun getUpdatedDTOClassesToModifyKindMap(dtoClassesToCheck: List<Class<*>
541587 }
542588}
543589
544- internal fun getDtoClassesToCheck () = Reflections (ApiRequest ::class .java.packageName, SubTypesScanner (false ))
545- .getSubTypesOf(Object ::class .java)
546- .filterNot { clazz -> clazz.isInterface || clazz.isAnonymousClass }
547- .filterNot { clazz ->
548- try {
549- clazz.kotlin.isCompanion
550- } catch (ignore: UnsupportedOperationException ) {
551- // Filtering file facades classes (*Kt classes) and synthetic classes (i.e. when-mappings classes)
552- true
553- }
554- }
555- .sortedBy { clazz -> clazz.canonicalName }
556-
557590private fun findToUpdatedMethod (
558591 reflections : Reflections ,
559592 fetchedDtoClass : KClass <out ApiFetchedDTO >,
0 commit comments