Skip to content

Commit 223585c

Browse files
committed
Refactor port helpers and increase coverage
1 parent ad48dd0 commit 223585c

File tree

7 files changed

+261
-18
lines changed

7 files changed

+261
-18
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ dependencies {
4545
testImplementation("org.junit.jupiter:junit-jupiter")
4646
testImplementation("org.springframework.boot:spring-boot-starter-data-redis")
4747
testImplementation("org.springframework.boot:spring-boot-starter-test")
48+
testImplementation("io.mockk:mockk:1.13.9")
4849
}
4950

5051
tasks.withType<KotlinCompile> {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.ports
2+
3+
import java.net.InetAddress
4+
import javax.net.ServerSocketFactory
5+
6+
/**
7+
* This object is used to check if a port is available.
8+
*/
9+
internal object PortChecker {
10+
11+
/**
12+
* Checks if the given port is available on `localhost`.
13+
* @param port The port to check.
14+
* @return `true` if the port is available, `false` otherwise.
15+
*/
16+
internal fun available(port: Int): Boolean {
17+
try {
18+
val serverSocket = ServerSocketFactory.getDefault()
19+
.createServerSocket(port, 1, InetAddress.getByName("localhost"))
20+
serverSocket.close()
21+
return true
22+
} catch (ex: Exception) {
23+
return false
24+
}
25+
}
26+
}

src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/PortProvider.kt renamed to src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/ports/PortProvider.kt

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
package io.github.tobi.laa.spring.boot.embedded.redis
1+
package io.github.tobi.laa.spring.boot.embedded.redis.ports
22

33
import redis.embedded.Redis.DEFAULT_REDIS_PORT
44
import redis.embedded.core.PortProvider.REDIS_CLUSTER_MAX_PORT_EXCLUSIVE
5-
import java.net.InetAddress
65
import java.util.concurrent.ConcurrentSkipListSet
7-
import javax.net.ServerSocketFactory
86

97
/**
108
* The default port used by Redis Sentinel.
@@ -47,17 +45,6 @@ internal class PortProvider {
4745
private fun portCanBeHandedOut(port: Int): Boolean {
4846
val busPort = port + BUS_PORT_OFFSET
4947
return !handedOutPorts.contains(port) && !handedOutPorts.contains(busPort) &&
50-
available(port) && available(busPort)
51-
}
52-
53-
private fun available(port: Int): Boolean {
54-
try {
55-
val serverSocket = ServerSocketFactory.getDefault()
56-
.createServerSocket(port, 1, InetAddress.getByName("localhost"))
57-
serverSocket.close()
58-
return true
59-
} catch (ex: Exception) {
60-
return false
61-
}
48+
PortChecker.available(port) && PortChecker.available(busPort)
6249
}
6350
}

src/main/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/server/RedisServerContextCustomizer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package io.github.tobi.laa.spring.boot.embedded.redis.server
22

3-
import io.github.tobi.laa.spring.boot.embedded.redis.PortProvider
43
import io.github.tobi.laa.spring.boot.embedded.redis.RedisStore
54
import io.github.tobi.laa.spring.boot.embedded.redis.conf.RedisConf
65
import io.github.tobi.laa.spring.boot.embedded.redis.conf.RedisConfLocator
76
import io.github.tobi.laa.spring.boot.embedded.redis.conf.RedisConfParser
7+
import io.github.tobi.laa.spring.boot.embedded.redis.ports.PortProvider
88
import org.springframework.boot.test.util.TestPropertyValues
99
import org.springframework.context.ConfigurableApplicationContext
1010
import org.springframework.context.event.ContextClosedEvent

src/test/kotlin/io/github/tobi/laa/spring/boot/embedded/redis/conf/RedisConfParserTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ internal class RedisConfParserTest {
5959
}
6060

6161
private fun thenValidRedisConfShouldBeReturned() {
62-
assertThatCode { parse!!.call() }.doesNotThrowAnyException()
62+
assertThatCode(parse).doesNotThrowAnyException()
6363
assertThat(result).isNotNull
6464
assertThat(result!!.directives).isNotEmpty
6565
}
6666

6767
private fun thenParsedConfShouldBeAs(expected: RedisConf) {
68-
assertThatCode { parse!!.call() }.doesNotThrowAnyException()
68+
assertThatCode(parse!!).doesNotThrowAnyException()
6969
assertThat(result).usingRecursiveComparison().isEqualTo(expected)
7070
}
7171

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.ports
2+
3+
import io.mockk.every
4+
import io.mockk.impl.annotations.MockK
5+
import io.mockk.junit5.MockKExtension
6+
import io.mockk.mockkStatic
7+
import io.mockk.unmockkStatic
8+
import io.mockk.verify
9+
import org.assertj.core.api.Assertions.assertThat
10+
import org.junit.jupiter.api.*
11+
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
12+
import org.junit.jupiter.api.extension.ExtendWith
13+
import java.io.IOException
14+
import java.net.ServerSocket
15+
import javax.net.ServerSocketFactory
16+
import kotlin.random.Random
17+
18+
@ExtendWith(MockKExtension::class)
19+
@DisplayName("PortChecker tests")
20+
@TestInstance(PER_CLASS)
21+
internal class PortCheckerTest {
22+
23+
@MockK
24+
private lateinit var factory: ServerSocketFactory
25+
26+
@MockK
27+
private lateinit var givenSocket: ServerSocket
28+
private var givenPort: Int = 0
29+
30+
private var actualAvailable: Boolean = false
31+
32+
@BeforeAll
33+
fun mockStatic() {
34+
mockkStatic(ServerSocketFactory::class)
35+
every { ServerSocketFactory.getDefault() } returns factory
36+
}
37+
38+
@AfterAll
39+
fun closeStaticMock() {
40+
unmockkStatic(ServerSocketFactory::getDefault)
41+
}
42+
43+
@RepeatedTest(5)
44+
@DisplayName("Port is unavailable if socket creation does not succeed")
45+
fun socketCreationError_whenCheckingPort_isUnvailable() {
46+
givenArbitraryPort()
47+
givenSocketCreationFails()
48+
whenCheckingPort()
49+
thenPortIsUnavailable()
50+
thenSocketCreationWasAttempted()
51+
}
52+
53+
@RepeatedTest(5)
54+
@DisplayName("Port is available if socket creation succeeds and socket is closed afterwards")
55+
fun socketCreationSuccess_whenCheckingPort_isAvailable_closesSocket() {
56+
givenArbitraryPort()
57+
givenSocketCreationSucceeds()
58+
whenCheckingPort()
59+
thenPortIsAvailable()
60+
thenSocketCreationWasAttempted()
61+
thenSocketIsClosed()
62+
}
63+
64+
private fun givenArbitraryPort() {
65+
givenPort = Random.Default.nextInt(1, 65535)
66+
}
67+
68+
private fun givenSocketCreationSucceeds() {
69+
every { factory.createServerSocket(any(), any(), any()) } returns givenSocket
70+
every { givenSocket.close() } returns Unit
71+
}
72+
73+
private fun givenSocketCreationFails() {
74+
every { factory.createServerSocket(any(), any(), any()) } throws IOException()
75+
}
76+
77+
private fun whenCheckingPort() {
78+
actualAvailable = PortChecker.available(givenPort)
79+
}
80+
81+
private fun thenPortIsUnavailable() {
82+
assertThat(actualAvailable).isFalse
83+
}
84+
85+
private fun thenPortIsAvailable() {
86+
assertThat(actualAvailable).isTrue
87+
}
88+
89+
private fun thenSocketCreationWasAttempted() {
90+
verify { factory.createServerSocket(eq(givenPort), any(), any()) }
91+
}
92+
93+
private fun thenSocketIsClosed() {
94+
verify { givenSocket.close() }
95+
}
96+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package io.github.tobi.laa.spring.boot.embedded.redis.ports
2+
3+
import io.mockk.every
4+
import io.mockk.junit5.MockKExtension
5+
import io.mockk.mockkObject
6+
import io.mockk.unmockkObject
7+
import org.assertj.core.api.AbstractIntegerAssert
8+
import org.assertj.core.api.Assertions.*
9+
import org.assertj.core.api.ThrowableAssert
10+
import org.junit.jupiter.api.*
11+
import org.junit.jupiter.api.extension.ExtendWith
12+
import org.junit.jupiter.params.ParameterizedTest
13+
import org.junit.jupiter.params.provider.ValueSource
14+
import java.util.stream.IntStream.range
15+
16+
@ExtendWith(MockKExtension::class)
17+
@DisplayName("PortProvider tests")
18+
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
19+
internal class PortProviderTest {
20+
21+
private lateinit var portProvider: PortProvider
22+
23+
private var givenSentinel = false
24+
private var requestPorts: ThrowableAssert.ThrowingCallable? = null
25+
private var actualPorts = emptyList<Int>()
26+
27+
@BeforeAll
28+
fun mockPortChecker() {
29+
mockkObject(PortChecker)
30+
}
31+
32+
@AfterAll
33+
fun unmockPortChecker() {
34+
unmockkObject(PortChecker)
35+
}
36+
37+
@BeforeEach
38+
fun setup() {
39+
portProvider = PortProvider()
40+
givenSentinel = false
41+
requestPorts = null
42+
actualPorts = emptyList<Int>().toMutableList()
43+
}
44+
45+
@Test
46+
@DisplayName("Error should occur if no free port is found")
47+
fun noFreePorts_requestingPort_throwsError() {
48+
givenNoFreePorts()
49+
whenNextPortIsRequested()
50+
thenErrorIsThrown()
51+
}
52+
53+
@Test
54+
@DisplayName("Valid port should be returned if all ports are free")
55+
fun freePorts_requestingPort_returnsValidPort() {
56+
givenFreePorts()
57+
whenNextPortIsRequested()
58+
thenValidPortIsReturned()
59+
}
60+
61+
@Test
62+
@DisplayName("Valid sentinel port should be returned if all ports are free")
63+
fun freePorts_requestingPortForSentinel_returnsValidPort() {
64+
givenSentinel()
65+
givenFreePorts()
66+
whenNextPortIsRequested()
67+
thenValidPortIsReturned()
68+
}
69+
70+
@Test
71+
@DisplayName("Valid port greater than 10000 should be returned if ports smaller than 10000 are taken")
72+
fun freePortsAfter10000_requestingPort_returnsValidPortGreater10000() {
73+
givenFreePortsStartingAt(10001)
74+
whenNextPortIsRequested()
75+
thenValidPortIsReturned().isGreaterThan(10000)
76+
}
77+
78+
@DisplayName("Multiple valid ports should be returned if all ports are free")
79+
@ParameterizedTest(name = "{0} valid ports should be returned")
80+
@ValueSource(ints = [5, 10, 100, 10000])
81+
fun freePorts_requestingPorts_returnsValidPorts(nOfPorts: Int) {
82+
givenFreePorts()
83+
whenNextPortsAreRequested(nOfPorts)
84+
thenValidPortsAreReturned(nOfPorts)
85+
}
86+
87+
private fun givenSentinel() {
88+
givenSentinel = true
89+
}
90+
91+
private fun givenFreePorts() = givenFreePortsStartingAt(1)
92+
93+
private fun givenFreePortsStartingAt(smallestFreePort: Int) {
94+
every { PortChecker.available(less(smallestFreePort)) } returns false
95+
every { PortChecker.available(more(smallestFreePort - 1)) } returns true
96+
}
97+
98+
private fun givenNoFreePorts() {
99+
every { PortChecker.available(any()) } returns false
100+
}
101+
102+
private fun whenNextPortIsRequested() = whenNextPortsAreRequested(1)
103+
104+
private fun whenNextPortsAreRequested(nOfPorts: Int) {
105+
requestPorts = ThrowableAssert.ThrowingCallable {
106+
range(0, nOfPorts).forEach { _ -> actualPorts += portProvider.next(givenSentinel) }
107+
}
108+
}
109+
110+
private fun thenErrorIsThrown() {
111+
assertThatThrownBy(requestPorts!!)
112+
.isExactlyInstanceOf(IllegalStateException::class.java)
113+
.hasMessage("Could not find an available TCP port")
114+
}
115+
116+
private fun thenValidPortIsReturned(): AbstractIntegerAssert<*> {
117+
thenValidPortsAreReturned(1)
118+
return assertThat(actualPorts.first())
119+
}
120+
121+
private fun thenValidPortsAreReturned(nOfPorts: Int) {
122+
assertThatCode(requestPorts!!).doesNotThrowAnyException()
123+
assertThat(actualPorts).hasSize(nOfPorts)
124+
assertThat(actualPorts).isNotEmpty.doesNotHaveDuplicates()
125+
if (givenSentinel) {
126+
assertThat(actualPorts).allSatisfy { assertThat(it).isBetween(26379, 65535) }
127+
} else {
128+
assertThat(actualPorts).allSatisfy { assertThat(it).isBetween(6379, 65535) }
129+
}
130+
// bus port should have been left free
131+
assertThat(actualPorts).allSatisfy { assertThat(actualPorts).doesNotContain(it + 10000) }
132+
}
133+
}

0 commit comments

Comments
 (0)