Skip to content

Commit 16983e5

Browse files
committed
Add ktlint and kotest assertions
Signed-off-by: Dmitry Sulman <[email protected]>
1 parent 29895ca commit 16983e5

File tree

10 files changed

+110
-73
lines changed

10 files changed

+110
-73
lines changed

buildSrc/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ dependencies {
1515
implementation(libs.dokka.gradlePlugin)
1616
implementation(libs.dokka.javadocPlugin)
1717
implementation(libs.kotlin.gradlePlugin)
18+
implementation(libs.ktlintPlugin)
1819
}

buildSrc/src/main/kotlin/conventions.gradle.kts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
22

33
plugins {
44
kotlin("jvm")
5-
`maven-publish`
65
id("org.jetbrains.dokka")
76
id("org.jetbrains.dokka-javadoc")
7+
id("org.jlleitschuh.gradle.ktlint")
88
jacoco
9+
`maven-publish`
910
}
1011

1112
group = "io.github.dmitrysulman"
@@ -35,6 +36,7 @@ java {
3536
tasks.build {
3637
dependsOn(tasks.test)
3738
dependsOn(tasks.jacocoTestReport)
39+
dependsOn(tasks.ktlintCheck)
3840
}
3941

4042
tasks.withType<Test>().configureEach {
@@ -102,3 +104,12 @@ tasks.jacocoTestReport {
102104
html.required = true
103105
}
104106
}
107+
108+
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
109+
version = "1.5.0"
110+
additionalEditorconfig.set(
111+
mapOf(
112+
"ktlint_standard_backing-property-naming" to "disabled"
113+
)
114+
)
115+
}

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
dokka = "2.0.0"
33
jreleaser = "1.18.0"
44
junit = "5.12.2"
5+
kotest = "5.9.1"
56
kotlin = "2.1.20"
7+
ktlint = "12.2.0"
68
logbackAccess = "2.0.6"
79
logbackClassic = "1.5.18"
810
reactorNetty = "1.2.6-SNAPSHOT"
@@ -13,7 +15,9 @@ dokka-gradlePlugin = { group = "org.jetbrains.dokka", name = "dokka-gradle-plugi
1315
dokka-javadocPlugin = { group = "org.jetbrains.dokka-javadoc", name = "org.jetbrains.dokka-javadoc.gradle.plugin", version.ref = "dokka" }
1416
junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }
1517
junit-platformLauncher = { group = "org.junit.platform", name = "junit-platform-launcher" }
18+
kotest-assertions-core-jvm = { group = "io.kotest", name = "kotest-assertions-core-jvm", version.ref = "kotest" }
1619
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
20+
ktlintPlugin = { group = "org.jlleitschuh.gradle.ktlint", name = "org.jlleitschuh.gradle.ktlint.gradle.plugin", version.ref = "ktlint" }
1721
logback-access-common = { group = "ch.qos.logback.access", name = "logback-access-common", version.ref = "logbackAccess" }
1822
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logbackClassic" }
1923
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logbackClassic" }

logback-access-reactor-netty/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dependencies {
1010
implementation(libs.slf4j.api)
1111

1212
testImplementation(libs.junit.jupiter)
13+
testImplementation(libs.kotest.assertions.core.jvm)
1314
testImplementation(libs.logback.classic)
1415
testImplementation(libs.logback.core)
1516

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessEvent.kt

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import java.io.Serializable
77
import java.net.InetSocketAddress
88
import java.net.URLDecoder
99
import java.nio.charset.StandardCharsets
10-
import java.util.*
1110
import java.util.Collections.enumeration
11+
import java.util.Enumeration
1212

1313
/**
1414
* Represents an access event for logging HTTP requests and responses in a Reactor Netty server environment.
@@ -31,15 +31,17 @@ class AccessEvent(
3131
@Transient
3232
private val argProvider: AccessLogArgProvider,
3333
context: AccessContext,
34-
) : IAccessEvent, Serializable {
34+
) : IAccessEvent,
35+
Serializable {
3536
private val _timeStamp = System.currentTimeMillis()
3637
private val _sequenceNumber = context.sequenceNumberGenerator?.nextSequenceNumber() ?: 0
3738
private val _elapsedTime = argProvider.duration()
3839
private val _elapsedTimeSeconds = _elapsedTime / 1000
3940
private val _requestUri by lazy { argProvider.uri()?.toString()?.substringBefore("?") ?: NA }
4041
private val _queryString by lazy {
4142
argProvider.uri()?.let { uri ->
42-
uri.indexOf("?")
43+
uri
44+
.indexOf("?")
4345
.takeIf { it != -1 }
4446
?.let { uri.substring(it) }
4547
.orEmpty()
@@ -54,23 +56,24 @@ class AccessEvent(
5456
} ?: NA
5557
}
5658
private val _remoteUser by lazy { argProvider.user() ?: NA }
57-
private val _protocol by lazy { argProvider.protocol() ?: NA }
59+
private val _protocol by lazy { argProvider.protocol() ?: NA }
5860
private val _method by lazy { argProvider.method()?.toString() ?: NA }
5961
private lateinit var _threadName: String
6062
private val _requestParameterMap by lazy {
61-
_queryString.takeIf { it.isNotEmpty() && it != NA }
63+
_queryString
64+
.takeIf { it.isNotEmpty() && it != NA }
6265
?.substring(1)
6366
?.split("&")
6467
?.mapNotNull {
6568
val index = it.indexOf("=")
6669
if (index in 1..it.length - 2) {
6770
it.substring(0, index) to it.substring(index + 1)
68-
} else null
69-
}
70-
?.groupBy({ URLDecoder.decode(it.first, StandardCharsets.UTF_8) }) {
71+
} else {
72+
null
73+
}
74+
}?.groupBy({ URLDecoder.decode(it.first, StandardCharsets.UTF_8) }) {
7175
URLDecoder.decode(it.second, StandardCharsets.UTF_8)
72-
}
73-
?.mapValues { it.value.toTypedArray() }
76+
}?.mapValues { it.value.toTypedArray() }
7477
?: emptyMap()
7578
}
7679
private val _remoteAddr by lazy {
@@ -91,7 +94,9 @@ class AccessEvent(
9194
private val _localPort by lazy { argProvider.connectionInformation()?.hostPort() ?: -1 }
9295
private val _responseHeaderMap by lazy { _serverAdapter.buildResponseHeaderMap() }
9396
private val _requestHeaderMap by lazy {
94-
argProvider.requestHeaderIterator()?.asSequence()
97+
argProvider
98+
.requestHeaderIterator()
99+
?.asSequence()
95100
?.associate { it.key.toString() to it.value.toString() }
96101
?: emptyMap()
97102
}
@@ -184,9 +189,7 @@ class AccessEvent(
184189

185190
override fun getResponseHeader(key: String) = _responseHeaderMap[key] ?: NA
186191

187-
override fun getResponseHeaderMap(): Map<String, String> {
188-
return _responseHeaderMap
189-
}
192+
override fun getResponseHeaderMap(): Map<String, String> = _responseHeaderMap
190193

191194
override fun getResponseHeaderNameList() = _responseHeaderMap.keys.toList()
192195

@@ -200,4 +203,4 @@ class AccessEvent(
200203
@JvmStatic
201204
private val NA_ARRAY = arrayOf(NA)
202205
}
203-
}
206+
}

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessLog.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class AccessLog(
2929
private val accessContext: AccessContext,
3030
private val argProvider: AccessLogArgProvider,
3131
) : ReactorAccessLog("") {
32-
3332
/**
3433
* Logs an access event by creating an instance of [AccessEvent] and processing it through the [AccessContext].
3534
*
@@ -56,4 +55,4 @@ class AccessLog(
5655
@JvmStatic
5756
private val logger = LoggerFactory.getLogger(AccessLog::class.java)
5857
}
59-
}
58+
}

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyAccessLogFactory.kt

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ package io.github.dmitrysulman.logback.access.reactor.netty
22

33
import ch.qos.logback.access.common.joran.JoranConfigurator
44
import ch.qos.logback.access.common.spi.AccessContext
5-
import ch.qos.logback.core.status.*
5+
import ch.qos.logback.core.status.ErrorStatus
6+
import ch.qos.logback.core.status.InfoStatus
7+
import ch.qos.logback.core.status.OnConsoleStatusListener
8+
import ch.qos.logback.core.status.Status
9+
import ch.qos.logback.core.status.WarnStatus
610
import ch.qos.logback.core.util.StatusListenerConfigHelper
711
import ch.qos.logback.core.util.StatusPrinter2
812
import reactor.netty.http.server.logging.AccessLogArgProvider
@@ -42,7 +46,6 @@ import java.net.URL
4246
* @see AccessLogFactory
4347
*/
4448
class ReactorNettyAccessLogFactory : AccessLogFactory {
45-
4649
/**
4750
* An instance of [AccessContext], which serves as the main context object for managing the state and components
4851
* involved in the access logging process. It provides facilities for managing appenders, filters, and status listeners
@@ -116,7 +119,11 @@ class ReactorNettyAccessLogFactory : AccessLogFactory {
116119
initialize(getDefaultConfig(), JoranConfigurator(), false)
117120
}
118121

119-
private fun initialize(config: URL?, joranConfigurator: JoranConfigurator, debug: Boolean) {
122+
private fun initialize(
123+
config: URL?,
124+
joranConfigurator: JoranConfigurator,
125+
debug: Boolean,
126+
) {
120127
try {
121128
if (config != null) {
122129
addStatus(InfoStatus("Start configuring with configuration file [${config.file}]", this::class.java.simpleName))
@@ -141,15 +148,25 @@ class ReactorNettyAccessLogFactory : AccessLogFactory {
141148

142149
private fun getDefaultConfig(): URL? {
143150
return try {
144-
val fileNameFromSystemProperty = System.getProperty(CONFIG_FILE_NAME_PROPERTY)?.also {
145-
addStatus(InfoStatus("Found system property [$CONFIG_FILE_NAME_PROPERTY] value: [$it]",
146-
this::class.java.simpleName))
147-
}
148-
val fileName = fileNameFromSystemProperty ?: run {
149-
addStatus(InfoStatus("No system property [$CONFIG_FILE_NAME_PROPERTY] provided, checking [$DEFAULT_CONFIG_FILE_NAME]",
150-
this::class.java.simpleName))
151-
DEFAULT_CONFIG_FILE_NAME
152-
}
151+
val fileNameFromSystemProperty =
152+
System.getProperty(CONFIG_FILE_NAME_PROPERTY)?.also {
153+
addStatus(
154+
InfoStatus(
155+
"Found system property [$CONFIG_FILE_NAME_PROPERTY] value: [$it]",
156+
this::class.java.simpleName,
157+
),
158+
)
159+
}
160+
val fileName =
161+
fileNameFromSystemProperty ?: run {
162+
addStatus(
163+
InfoStatus(
164+
"No system property [$CONFIG_FILE_NAME_PROPERTY] provided, checking [$DEFAULT_CONFIG_FILE_NAME]",
165+
this::class.java.simpleName,
166+
),
167+
)
168+
DEFAULT_CONFIG_FILE_NAME
169+
}
153170
getConfigFromFileName(fileName)
154171
} catch (e: FileNotFoundException) {
155172
addStatus(WarnStatus(e.message, this::class.java.simpleName))
@@ -195,4 +212,4 @@ class ReactorNettyAccessLogFactory : AccessLogFactory {
195212
@JvmStatic
196213
val DEFAULT_CONFIG_FILE_NAME = "logback-access.xml"
197214
}
198-
}
215+
}

logback-access-reactor-netty/src/main/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/ReactorNettyServerAdapter.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ class ReactorNettyServerAdapter(
1212

1313
override fun getStatusCode() = argProvider.status()?.toString()?.toIntOrNull() ?: -1
1414

15-
override fun buildResponseHeaderMap() = argProvider.responseHeaderIterator()?.asSequence()
16-
?.associate { it.key.toString() to it.value.toString() }
17-
?: emptyMap()
18-
}
15+
override fun buildResponseHeaderMap() =
16+
argProvider
17+
.responseHeaderIterator()
18+
?.asSequence()
19+
?.associate { it.key.toString() to it.value.toString() }
20+
?: emptyMap()
21+
}

logback-access-reactor-netty/src/test/kotlin/io/github/dmitrysulman/logback/access/reactor/netty/AccessLogTests.kt

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ package io.github.dmitrysulman.logback.access.reactor.netty
33
import ch.qos.logback.access.common.joran.JoranConfigurator
44
import ch.qos.logback.access.common.spi.IAccessEvent
55
import io.github.dmitrysulman.logback.access.reactor.netty.util.EventCaptureAppender
6+
import io.kotest.matchers.comparables.shouldBeGreaterThan
7+
import io.kotest.matchers.nulls.shouldNotBeNull
8+
import io.kotest.matchers.shouldBe
69
import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH
710
import io.netty.handler.codec.http.cookie.DefaultCookie
811
import org.junit.jupiter.api.AfterEach
9-
import org.junit.jupiter.api.Assertions.assertEquals
10-
import org.junit.jupiter.api.Assertions.assertTrue
1112
import org.junit.jupiter.api.BeforeEach
1213
import org.junit.jupiter.api.Test
13-
import org.junit.jupiter.api.assertNotNull
1414
import reactor.core.publisher.Mono
1515
import reactor.netty.DisposableServer
1616
import reactor.netty.http.client.HttpClient
@@ -39,7 +39,6 @@ private const val TEST_COOKIE_VALUE = "cookieValue"
3939
// TODO different ways of providing configuration
4040
// TODO filters
4141
class AccessLogTests {
42-
4342
private lateinit var server: DisposableServer
4443
private lateinit var eventCaptureAppender: EventCaptureAppender
4544
private var now: Long = 0
@@ -57,17 +56,18 @@ class AccessLogTests {
5756

5857
@Test
5958
fun `GET request without query params`() {
60-
val accessLogFactory = ReactorNettyAccessLogFactory("logback-access-stdout.xml", JoranConfigurator(), true)
61-
.apply {
62-
accessContext.addAppender(eventCaptureAppender)
63-
}
59+
val accessLogFactory =
60+
ReactorNettyAccessLogFactory("logback-access-stdout.xml", JoranConfigurator(), true)
61+
.apply {
62+
accessContext.addAppender(eventCaptureAppender)
63+
}
6464

6565
server = createServer(accessLogFactory, "mock response")
6666
val response = performGetRequest("/test")
67-
assertNotNull(response)
67+
response.shouldNotBeNull()
6868

6969
Thread.sleep(100)
70-
assertEquals(1, eventCaptureAppender.list.size)
70+
eventCaptureAppender.list.size shouldBe 1
7171
val accessEvent = eventCaptureAppender.list[0]
7272
assertAccessEvent(accessEvent, response)
7373
}
@@ -76,30 +76,31 @@ class AccessLogTests {
7676
accessEvent: IAccessEvent,
7777
response: HttpClientResponse,
7878
) {
79-
assertEquals("${response.method().name()} ${response.uri()} ${response.version().text()}", accessEvent.requestURL)
80-
assertEquals(response.uri(), accessEvent.requestURI)
81-
assertEquals(server.port(), accessEvent.localPort)
82-
assertEquals(response.responseHeaders().get(REMOTE_HOST_HEADER), accessEvent.remoteHost)
83-
assertEquals(response.responseHeaders().get(REMOTE_ADDRESS_HEADER), accessEvent.remoteAddr)
84-
assertEquals("", accessEvent.queryString)
85-
assertEquals(response.version().text(), accessEvent.protocol)
86-
assertEquals(response.method().name(), accessEvent.method)
87-
assertEquals(response.status().code(), accessEvent.statusCode)
88-
assertTrue(accessEvent.elapsedTime > 0)
89-
assertTrue(accessEvent.timeStamp > now)
90-
assertEquals(0, accessEvent.sequenceNumber)
91-
assertEquals( "-", accessEvent.remoteUser)
92-
assertEquals(TEST_REQUEST_HEADER_VALUE, accessEvent.getRequestHeader(TEST_REQUEST_HEADER_NAME))
93-
assertEquals(TEST_RESPONSE_HEADER_VALUE, accessEvent.getResponseHeader(TEST_RESPONSE_HEADER_NAME))
94-
assertEquals(TEST_COOKIE_VALUE, accessEvent.getCookie(TEST_COOKIE_NAME))
95-
assertEquals(response.responseHeaders().get(CONTENT_LENGTH)?.toLongOrNull() ?: 0, accessEvent.contentLength)
79+
accessEvent.requestURL shouldBe "${response.method().name()} ${response.uri()} ${response.version().text()}"
80+
accessEvent.requestURI shouldBe response.uri()
81+
accessEvent.localPort shouldBe server.port()
82+
accessEvent.remoteHost shouldBe response.responseHeaders().get(REMOTE_HOST_HEADER)
83+
accessEvent.remoteAddr shouldBe response.responseHeaders().get(REMOTE_ADDRESS_HEADER)
84+
accessEvent.queryString shouldBe ""
85+
accessEvent.protocol shouldBe response.version().text()
86+
accessEvent.method shouldBe response.method().name()
87+
accessEvent.statusCode shouldBe response.status().code()
88+
accessEvent.elapsedTime shouldBeGreaterThan 0
89+
accessEvent.timeStamp shouldBeGreaterThan now
90+
accessEvent.sequenceNumber shouldBe 0
91+
accessEvent.remoteUser shouldBe "-"
92+
accessEvent.getRequestHeader(TEST_REQUEST_HEADER_NAME) shouldBe TEST_REQUEST_HEADER_VALUE
93+
accessEvent.getResponseHeader(TEST_RESPONSE_HEADER_NAME) shouldBe TEST_RESPONSE_HEADER_VALUE
94+
accessEvent.getCookie(TEST_COOKIE_NAME) shouldBe TEST_COOKIE_VALUE
95+
accessEvent.contentLength shouldBe (response.responseHeaders().get(CONTENT_LENGTH)?.toLongOrNull() ?: 0)
9696
}
9797

9898
private fun createServer(
9999
accessLogFactory: ReactorNettyAccessLogFactory,
100-
responseContent: String
101-
): DisposableServer {
102-
return HttpServer.create()
100+
responseContent: String,
101+
): DisposableServer =
102+
HttpServer
103+
.create()
103104
.handle { request, response ->
104105
val remoteHost = (request.remoteAddress() as InetSocketAddress).hostString
105106
val remoteAddress = (request.remoteAddress() as InetSocketAddress).address.hostAddress
@@ -108,13 +109,11 @@ class AccessLogTests {
108109
.addHeader(REMOTE_ADDRESS_HEADER, remoteAddress)
109110
.addHeader(TEST_RESPONSE_HEADER_NAME, TEST_RESPONSE_HEADER_VALUE)
110111
.sendByteArray(Mono.just(responseContent.toByteArray()))
111-
}
112-
.accessLog(true, accessLogFactory)
112+
}.accessLog(true, accessLogFactory)
113113
.bindNow()
114-
}
115114

116-
private fun performGetRequest(uri: String): HttpClientResponse? {
117-
return HttpClient
115+
private fun performGetRequest(uri: String): HttpClientResponse? =
116+
HttpClient
118117
.create()
119118
.port(server.port())
120119
.headers { it.add(TEST_REQUEST_HEADER_NAME, TEST_REQUEST_HEADER_VALUE) }
@@ -123,5 +122,4 @@ class AccessLogTests {
123122
.uri(uri)
124123
.response()
125124
.block(30.seconds.toJavaDuration())
126-
}
127-
}
125+
}

0 commit comments

Comments
 (0)