Skip to content

Commit b095a91

Browse files
committed
Release 0.5.6
Added Generics to some APIs Signed-off-by: Gopal S Akshintala <[email protected]>
1 parent bdd8bd1 commit b095a91

File tree

15 files changed

+96
-48
lines changed

15 files changed

+96
-48
lines changed

README.adoc

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ endif::[]
1818
:pmtemplates: src/integrationTest/resources/pm-templates
1919
:imagesdir: docs/images
2020
:prewrap!:
21-
:revoman-version: 0.5.5
21+
:revoman-version: 0.5.6
2222

2323
'''
2424

@@ -127,7 +127,7 @@ In the case of collision between variable keys, the precedence order to derive a
127127
You can _kick off_ this *Template-Driven Testing* by invoking `ReVoman.revUp()`,
128128
supplying your Postman templates and environments, and all your customizations through a configuration:
129129

130-
[source,java,indent=0,options="nowrap"]
130+
[source,java,indent=0,tabsize=2,options="nowrap"]
131131
----
132132
final var rundown =
133133
ReVoman.revUp(
@@ -150,7 +150,7 @@ by supplying the template and environment path:
150150

151151
ifdef::env-github[]
152152

153-
[source,java,indent=0,options="nowrap"]
153+
[source,java,indent=0,tabsize=2,options="nowrap"]
154154
.link:{integrationtestdir}/com/salesforce/revoman/integration/restfulapidev/RestfulAPIDevTest.java[RestfulAPIDevTest.java, tag=revoman-simple-demo]
155155
----
156156
@Test
@@ -162,26 +162,29 @@ void restfulApiDev() {
162162
.templatePath(PM_COLLECTION_PATH) // <2>
163163
.environmentPath(PM_ENVIRONMENT_PATH) // <3>
164164
.off());
165-
assertThat(rundown.stepReports).hasSize(3); // <4>
165+
assertThat(rundown.firstUnIgnoredUnsuccessfulStepReport()).isNull(); // <4>
166+
assertThat(rundown.stepReports).hasSize(3); // <5>
166167
}
167168
----
168169
<1> `revUp` is the method to call passing a configuration, built as below
169170
<2> Supply an exported Postman collection JSON file path
170171
<3> Supply an exported Postman environment JSON file path
171-
<4> Run more assertions on the <<Rundown>>
172+
<4> Assert that the execution doesn't have any failures
173+
<5> Run more assertions on the <<Rundown>>
172174

173175
endif::[]
174176
ifndef::env-github[]
175177

176-
[source,java,indent=0,options="nowrap"]
178+
[source,java,indent=0,tabsize=2,options="nowrap"]
177179
.link:{integrationtestdir}/com/salesforce/revoman/integration/restfulapidev/RestfulAPIDevTest.java[RestfulAPIDevTest.java,tag=revoman-simple-demo]
178180
----
179181
include::{integrationtestdir}/com/salesforce/revoman/integration/restfulapidev/RestfulAPIDevTest.java[tag=revoman-simple-demo]
180182
----
181183
<1> `revUp` is the method to call passing a configuration, built as below
182184
<2> Supply an exported Postman collection JSON file path
183185
<3> Supply an exported Postman environment JSON file path
184-
<4> Run more assertions on the <<Rundown>>
186+
<4> Assert that the execution doesn't have any failures
187+
<5> Run more assertions on the <<Rundown>>
185188

186189
endif::[]
187190

@@ -191,7 +194,7 @@ After all this, you receive back a detailed *Rundown* in return.
191194
It contains everything you need to know about what happened in an execution,
192195
such that you can seamlessly run more assertions on top of the run.
193196

194-
[source,kotlin,indent=0,options="nowrap"]
197+
[source,kotlin,indent=0,tabsize=2,options="nowrap"]
195198
----
196199
Rundown(
197200
val stepReports: List<StepReport>,
@@ -222,7 +225,7 @@ you can add more _bells and whistles_ 🔔:
222225

223226
ifdef::env-github[]
224227

225-
[source,java,indent=0,options="nowrap"]
228+
[source,java,indent=0,tabsize=2,options="nowrap"]
226229
.link:{integrationtestdir}/com/salesforce/revoman/integration/core/pq/PQE2EWithSMTest.java[PQE2EWithSMTest.java, tag=pq-e2e-with-revoman-config-demo]
227230
----
228231
final var pqRundown =
@@ -313,7 +316,7 @@ assertThat(pqRundown.mutableEnv)
313316
endif::[]
314317
ifndef::env-github[]
315318

316-
[source,java,indent=0,options="nowrap"]
319+
[source,java,indent=0,tabsize=2,options="nowrap"]
317320
.link:{integrationtestdir}/com/salesforce/revoman/integration/core/pq/PQE2EWithSMTest.java[PQE2ETest.java, tag=pq-e2e-with-revoman-config-demo]
318321
----
319322
include::{integrationtestdir}/com/salesforce/revoman/integration/core/pq/PQE2EWithSMTest.java[tag=pq-e2e-with-revoman-config-demo]
@@ -378,8 +381,20 @@ image:pq-exe-logging.gif[Monitor Execution]
378381
* There may be a POJO that inherits or contains legacy types that are hard or impossible to serialize. ReṼoman lets you serialize only types that matter, through `globalSkipTypes`, where you can filter out these legacy types from Marshalling/Unmarshalling
379382
* The JSON structure may not align with the POJO, and you may need a _Custom Type Adapter_ for Marshalling/Unmarshalling. Moshi has it covered for you with its advanced adapter mechanism and ReṼoman accepts Moshi adapters via:
380383
** `requestConfig` — For types present as part of request payload for qualified Steps
381-
** `responseConfig` — For types present as part of response payload for qualified Steps
382-
** `globalCustomTypeAdapters` — For types present as part of request payload anywhere
384+
** `responseConfig` — For types present as part of response payload for qualified Steps. You can configure separate adapters for success and error response. Use bundled static factory methods like `unmarshallSuccessResponse()` and `unmarshallErrorResponse()` for expressiveness.
385+
386+
[TIP]
387+
====
388+
Success or Error response is determined by default with HTTP Status Code (SUCCESSFUL = `200 <=statusCode <=299`). There may be a scenario
389+
that you cannot depend on HTTP Status Code to distinguish between Success or Error.
390+
In such a case,
391+
you can leverage a bundled Moshi factory link:{sourcedir}/com/salesforce/revoman/input/json/factories/DiMorphicAdapter.kt[DiMorphicAdapter]
392+
that deals with it dynamically.
393+
Refer link:{sourcedir}/com/salesforce/revoman/input/json/adapters/CompositeGraphResponse.kt[CompositeGraphResponse]
394+
for an example usage
395+
====
396+
397+
** `globalCustomTypeAdapters` — For types present as part of request/response payload anywhere
383398
* ReṼoman also comes bundled with link:{sourcedir}/com/salesforce/revoman/input/json/JsonReaderUtils.kt[JSON Reader utils] and link:{sourcedir}/com/salesforce/revoman/input/json/JsonWriterUtils.kt[JSON Writer utils] to help build Moshi adapters.
384399

385400
TIP: Refer link:{integrationtestdir}/com/salesforce/revoman/integration/core/pq/adapters/ConnectInputRepWithGraphAdapter.java[ConnectInputRepWithGraphAdapter]
@@ -418,15 +433,15 @@ ReṼoman comes
418433
bundled with some predicates under the namespace `PreTxnStepPick.PickUtils`/`PostTxnStepPick.PickUtils` e.g `beforeStepContainingURIPathOfAny`,
419434
`afterStepName` etc. If those don't fit your needs, you can write your own custom predicates like below:
420435

421-
[source,java,indent=0,options="nowrap"]
436+
[source,java,indent=0,tabsize=2,options="nowrap"]
422437
----
423438
final var preTxnStepPick = (currentStep, requestInfo, rundown) -> LOGGER.info("Picked `preLogHook` before stepName: {}", currentStep)
424439
final var postTxnStepPick = (stepReport, rundown) -> LOGGER.info("Picked `postLogHook` after stepName: {}", stepReport.step.displayName)
425440
----
426441

427442
Add them to the config as below:
428443

429-
[source,java,indent=0,options="nowrap"]
444+
[source,java,indent=0,tabsize=2,options="nowrap"]
430445
----
431446
.hooks(
432447
pre(
@@ -481,7 +496,7 @@ npm install moment
481496
----
482497

483498
.Use inside pre-req and post-res scripts
484-
[source,javascript,indent=0,options="nowrap"]
499+
[source,javascript,indent=0,tabsize=2,options="nowrap"]
485500
----
486501
var moment = require("moment")
487502
var _ = require('lodash')

buildSrc/src/main/kotlin/Config.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
* ************************************************************************************************
77
*/
88
const val GROUP_ID = "com.salesforce.revoman"
9-
const val VERSION = "0.5.5"
9+
const val VERSION = "0.5.6"
1010
const val ARTIFACT_ID = "revoman"
1111
const val STAGING_PROFILE_ID = "1ea0a23e61ba7d"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* ************************************************************************************************
3+
* Copyright (c) 2023, Salesforce, Inc. All rights reserved. SPDX-License-Identifier: Apache License
4+
* Version 2.0 For full license text, see the LICENSE file in the repo root or
5+
* http://www.apache.org/licenses/LICENSE-2.0
6+
* ************************************************************************************************
7+
*/
8+
package com.salesforce.revoman.integration.pokemon
9+
10+
import com.squareup.moshi.Json
11+
import com.squareup.moshi.JsonClass
12+
13+
@JsonClass(generateAdapter = true)
14+
data class AllPokemon(
15+
@Json(name = "count") val count: Int,
16+
@Json(name = "next") val next: String,
17+
@Json(name = "previous") val previous: Any?,
18+
@Json(name = "results") val results: List<Result>,
19+
) {
20+
@JsonClass(generateAdapter = true)
21+
data class Result(@Json(name = "name") val name: String, @Json(name = "url") val url: String)
22+
}

src/integrationTest/java/com/salesforce/revoman/integration/pokemon/PokemonTest.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static com.google.common.truth.Truth.assertThat;
1111
import static com.salesforce.revoman.input.config.HookConfig.post;
1212
import static com.salesforce.revoman.input.config.HookConfig.pre;
13+
import static com.salesforce.revoman.input.config.ResponseConfig.unmarshallResponse;
1314
import static com.salesforce.revoman.input.config.StepPick.PostTxnStepPick.afterStepContainingHeader;
1415
import static com.salesforce.revoman.input.config.StepPick.PostTxnStepPick.afterStepContainingURIPathOfAny;
1516
import static com.salesforce.revoman.input.config.StepPick.PostTxnStepPick.afterStepName;
@@ -89,17 +90,19 @@ public void accept(
8990
Mockito.spy(
9091
new PostStepHook() {
9192
@Override
92-
public void accept(@NotNull StepReport ignore2, @NotNull Rundown rundown) {
93+
public void accept(@NotNull StepReport stepReport, @NotNull Rundown rundown) {
9394
assertThat(rundown.mutableEnv).containsEntry("limit", String.valueOf(newLimit));
94-
assertThat(rundown.mutableEnv).containsEntry("pokemonName", "bulbasaur");
95+
final var results =
96+
stepReport.responseInfo.get().<AllPokemon>getTypedTxnObj().getResults();
97+
assertThat(results.size()).isEqualTo(newLimit);
9598
}
9699
});
97100
//noinspection Convert2Lambda
98101
final var postHookAfterURIPath =
99102
Mockito.spy(
100103
new PostStepHook() {
101104
@Override
102-
public void accept(@NotNull StepReport stepReport, @NotNull Rundown ignore) {
105+
public void accept(@NotNull StepReport stepReport, @NotNull Rundown rundown) {
103106
LOGGER.info(
104107
"Picked `postHookAfterURIPath` after stepName: {} with raw URI: {}",
105108
stepReport.step.displayName,
@@ -111,6 +114,7 @@ public void accept(@NotNull StepReport stepReport, @NotNull Rundown ignore) {
111114
Kick.configure()
112115
.templatePath(PM_COLLECTION_PATH)
113116
.environmentPath(PM_ENVIRONMENT_PATH)
117+
.responseConfig(unmarshallResponse(afterStepName("all-pokemon"), AllPokemon.class))
114118
.hooks(
115119
pre(beforeStepName("all-pokemon"), preHook),
116120
post(afterStepName("all-pokemon"), postHook),
@@ -121,11 +125,7 @@ public void accept(@NotNull StepReport stepReport, @NotNull Rundown ignore) {
121125
.haltOnAnyFailure(true)
122126
.off());
123127

124-
Mockito.verify(preHook, times(1)).accept(any(), any(), any());
125-
Mockito.verify(postHook, times(1)).accept(any(), any());
126-
Mockito.verify(postHookAfterURIPath, times(1)).accept(any(), any());
127-
Mockito.verify(preLogHook, times(1)).accept(any(), any(), any());
128-
Mockito.verify(postLogHook, times(1)).accept(any(), any());
128+
assertThat(pokeRundown.firstUnIgnoredUnsuccessfulStepReport()).isNull();
129129
assertThat(pokeRundown.stepReports).hasSize(5);
130130
assertThat(pokeRundown.mutableEnv)
131131
.containsExactlyEntriesIn(
@@ -139,5 +139,10 @@ public void accept(@NotNull StepReport stepReport, @NotNull Rundown ignore) {
139139
"gender", "female",
140140
"ability", "stench",
141141
"nature", "hardy"));
142+
Mockito.verify(preHook, times(1)).accept(any(), any(), any());
143+
Mockito.verify(postHook, times(1)).accept(any(), any());
144+
Mockito.verify(postHookAfterURIPath, times(1)).accept(any(), any());
145+
Mockito.verify(preLogHook, times(1)).accept(any(), any(), any());
146+
Mockito.verify(postLogHook, times(1)).accept(any(), any());
142147
}
143148
}

src/integrationTest/java/com/salesforce/revoman/integration/restfulapidev/RestfulAPIDevTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ void restfulApiDev() {
3131
.environmentPath(PM_ENVIRONMENT_PATH) // <3>
3232
.nodeModulesRelativePath("js")
3333
.off());
34-
assertThat(rundown.stepReports).hasSize(3); // <4>
34+
assertThat(rundown.firstUnIgnoredUnsuccessfulStepReport()).isNull(); // <4>
35+
assertThat(rundown.stepReports).hasSize(3); // <5>
3536
}
3637
// end::revoman-simple-demo[]
3738
}

src/integrationTest/kotlin/com/salesforce/revoman/integration/restfulapidev/RestfulAPIDevKtTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ class RestfulAPIDevKtTest {
2323
.nodeModulesRelativePath("js")
2424
.off()
2525
)
26-
assertThat(rundown.stepReports).hasSize(3)
2726
assertThat(rundown.firstUnsuccessfulStepReport).isNull()
27+
assertThat(rundown.stepReports).hasSize(3)
2828
}
2929

3030
companion object {

src/main/kotlin/com/salesforce/revoman/input/config/KickDef.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ internal interface KickDef {
6767
fun requestConfig(): Set<RequestConfig>
6868

6969
@Value.Derived
70-
fun customTypeAdaptersFromRequestConfig(): Map<Type, List<Either<JsonAdapter<Any>, Factory>>> =
70+
fun customTypeAdaptersFromRequestConfig():
71+
Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> =
7172
requestConfig()
7273
.filter { it.customTypeAdapter != null }
7374
.groupBy({ it.objType }, { it.customTypeAdapter!! })
@@ -79,7 +80,8 @@ internal interface KickDef {
7980
responseConfig().groupBy { it.ifSuccess }
8081

8182
@Value.Derived
82-
fun customTypeAdaptersFromResponseConfig(): Map<Type, List<Either<JsonAdapter<Any>, Factory>>> =
83+
fun customTypeAdaptersFromResponseConfig():
84+
Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> =
8385
responseConfig()
8486
.filter { it.customTypeAdapter != null }
8587
.groupBy({ it.objType }, { it.customTypeAdapter!! })

src/main/kotlin/com/salesforce/revoman/input/config/RequestConfig.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ data class RequestConfig
1818
internal constructor(
1919
val preTxnStepPick: PreTxnStepPick,
2020
val objType: Type,
21-
val customTypeAdapter: Either<JsonAdapter<Any>, JsonAdapter.Factory>? = null,
21+
val customTypeAdapter: Either<JsonAdapter<out Any>, JsonAdapter.Factory>? = null,
2222
) {
2323
companion object {
2424
@JvmStatic
@@ -29,7 +29,7 @@ internal constructor(
2929
fun unmarshallRequest(
3030
preTxnStepPick: PreTxnStepPick,
3131
requestType: Type,
32-
customTypeAdapter: JsonAdapter<Any>,
32+
customTypeAdapter: JsonAdapter<out Any>,
3333
): RequestConfig = RequestConfig(preTxnStepPick, requestType, left(customTypeAdapter))
3434

3535
@JvmStatic

src/main/kotlin/com/salesforce/revoman/input/config/ResponseConfig.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal constructor(
1919
val postTxnStepPick: PostTxnStepPick,
2020
val ifSuccess: Boolean?,
2121
val objType: Type,
22-
val customTypeAdapter: Either<JsonAdapter<Any>, JsonAdapter.Factory>? = null,
22+
val customTypeAdapter: Either<JsonAdapter<out Any>, JsonAdapter.Factory>? = null,
2323
) {
2424
companion object {
2525
@JvmStatic
@@ -30,7 +30,7 @@ internal constructor(
3030
fun unmarshallResponse(
3131
postTxnStepPick: PostTxnStepPick,
3232
successType: Type,
33-
customTypeAdapter: JsonAdapter<Any>,
33+
customTypeAdapter: JsonAdapter<out Any>,
3434
): ResponseConfig = ResponseConfig(postTxnStepPick, null, successType, left(customTypeAdapter))
3535

3636
@JvmStatic

src/main/kotlin/com/salesforce/revoman/input/json/JsonPojoUtils.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ fun <PojoT : Any> jsonFileToPojo(
3333
pojoType: Type,
3434
jsonFilePath: String,
3535
customAdapters: List<Any> = emptyList(),
36-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
36+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
3737
skipTypes: Set<Class<out Any>> = emptySet(),
3838
): PojoT? {
3939
val jsonAdapter = initMoshi<PojoT>(customAdapters, customAdaptersWithType, skipTypes, pojoType)
@@ -52,7 +52,7 @@ fun <PojoT : Any> jsonFileToPojo(jsonFile: JsonFile<PojoT>): PojoT? =
5252
inline fun <reified PojoT : Any> jsonFileToPojo(
5353
jsonFilePath: String,
5454
customAdapters: List<Any> = emptyList(),
55-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
55+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
5656
skipTypes: Set<Class<out Any>> = emptySet(),
5757
): PojoT? =
5858
jsonFileToPojo(PojoT::class.java, jsonFilePath, customAdapters, customAdaptersWithType, skipTypes)
@@ -73,7 +73,7 @@ fun <PojoT : Any> jsonToPojo(
7373
pojoType: Type,
7474
jsonStr: String,
7575
customAdapters: List<Any> = emptyList(),
76-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
76+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
7777
skipTypes: Set<Class<out Any>> = emptySet(),
7878
): PojoT? {
7979
val jsonAdapter = initMoshi<PojoT>(customAdapters, customAdaptersWithType, skipTypes, pojoType)
@@ -92,7 +92,7 @@ fun <PojoT : Any> jsonToPojo(jsonString: JsonString<PojoT>): PojoT? =
9292
inline fun <reified PojoT : Any> jsonToPojo(
9393
jsonStr: String,
9494
customAdapters: List<Any> = emptyList(),
95-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
95+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
9696
skipTypes: Set<Class<out Any>> = emptySet(),
9797
): PojoT? =
9898
jsonToPojo(PojoT::class.java, jsonStr, customAdapters, customAdaptersWithType, skipTypes)
@@ -114,7 +114,7 @@ fun <PojoT : Any> pojoToJson(
114114
pojoType: Type,
115115
pojo: PojoT,
116116
customAdapters: List<Any> = emptyList(),
117-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
117+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
118118
skipTypes: Set<Class<out Any>> = emptySet(),
119119
indent: String? = " ",
120120
): String? {
@@ -135,7 +135,7 @@ fun <PojoT : Any> pojoToJson(config: Pojo<PojoT>): String? =
135135
inline fun <reified PojoT : Any> pojoToJson(
136136
pojo: PojoT,
137137
customAdapters: List<Any> = emptyList(),
138-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
138+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
139139
skipTypes: Set<Class<out Any>> = emptySet(),
140140
indent: String? = " ",
141141
): String? =
@@ -144,7 +144,7 @@ inline fun <reified PojoT : Any> pojoToJson(
144144
@SuppressWarnings("kotlin:S3923")
145145
private fun <PojoT : Any> initMoshi(
146146
customAdapters: List<Any> = emptyList(),
147-
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<Any>, Factory>>> = emptyMap(),
147+
customAdaptersWithType: Map<Type, List<Either<JsonAdapter<out Any>, Factory>>> = emptyMap(),
148148
skipTypes: Set<Class<out Any>> = emptySet(),
149149
pojoType: Type,
150150
): JsonAdapter<PojoT> =
@@ -159,7 +159,7 @@ internal interface PojoDef<PojoT> {
159159

160160
fun customAdapters(): List<Any>
161161

162-
fun customAdaptersWithType(): Map<Type, List<Either<JsonAdapter<Any>, Factory>>>
162+
fun customAdaptersWithType(): Map<Type, List<Either<JsonAdapter<out Any>, Factory>>>
163163

164164
fun skipTypes(): Set<Class<out Any>>
165165

@@ -186,7 +186,7 @@ internal interface JsonConfig<PojoT> {
186186

187187
fun customAdapters(): List<Any>
188188

189-
fun customAdaptersWithType(): Map<Type, List<Either<JsonAdapter<Any>, Factory>>>
189+
fun customAdaptersWithType(): Map<Type, List<Either<JsonAdapter<out Any>, Factory>>>
190190

191191
fun skipTypes(): Set<Class<out Any>>
192192
}

0 commit comments

Comments
 (0)