Skip to content

Commit dcd781a

Browse files
authored
Add decodeBuffered unit tests (#228)
1 parent ccbee9a commit dcd781a

File tree

7 files changed

+255
-46
lines changed

7 files changed

+255
-46
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/*
2+
* Copyright (c) 2025 Matthew Nelson
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
package io.matthewnelson.encoding.core
17+
18+
import io.matthewnelson.encoding.core.Decoder.Companion.decodeBuffered
19+
import io.matthewnelson.encoding.core.EncoderDecoder.Companion.DEFAULT_BUFFER_SIZE
20+
import io.matthewnelson.encoding.core.helpers.TestConfig
21+
import io.matthewnelson.encoding.core.helpers.TestEncoderDecoder
22+
import kotlin.test.Test
23+
import kotlin.test.assertEquals
24+
import kotlin.test.assertFailsWith
25+
import kotlin.test.fail
26+
27+
class DecodeBufferedUnitTest {
28+
29+
@Test
30+
fun givenBufferSize_whenLessThanOrEqualToConfigMaxDecodeEmit_thenThrowsIllegalArgumentException() {
31+
val decoder = TestEncoderDecoder(TestConfig(
32+
maxDecodeEmit = 255,
33+
decodeInputReturn = { error("Should not make it here") }
34+
))
35+
36+
intArrayOf(254, 255).forEach { testSize ->
37+
assertFailsWith<IllegalArgumentException> {
38+
"a".decodeBuffered(
39+
decoder,
40+
throwOnOverflow = true,
41+
maxBufSize = testSize,
42+
action = { _, _, _ -> error("Should not make it here") },
43+
)
44+
}
45+
46+
assertFailsWith<IllegalArgumentException> {
47+
"a".decodeBuffered(
48+
decoder,
49+
throwOnOverflow = true,
50+
buf = ByteArray(testSize),
51+
action = { _, _, _ -> error("Should not make it here") },
52+
)
53+
}
54+
}
55+
}
56+
57+
@Test
58+
fun givenDecodeInputNonSizeException_whenConfigThrows_thenIsNeverIgnored() {
59+
val expected = "Config implementation has some sort of checksum at end of encoding and it did not pass"
60+
val decoder = TestEncoderDecoder(TestConfig(
61+
decodeInputReturn = { throw EncodingException(expected) }
62+
))
63+
try {
64+
"a".decodeBuffered(decoder, throwOnOverflow = true) { _, _, _ -> error("Should not make it here") }
65+
fail()
66+
} catch (e: EncodingException) {
67+
assertEquals(expected, e.message)
68+
}
69+
try {
70+
"a".decodeBuffered(decoder, throwOnOverflow = false) { _, _, _ -> error("Should not make it here") }
71+
fail()
72+
} catch (e: EncodingException) {
73+
assertEquals(expected, e.message)
74+
}
75+
}
76+
77+
@Test
78+
fun givenDecodeInputSizeException_whenThrowOnOverflowTrue_thenEncodingSizeExceptionIsRethrown() {
79+
val decoder = TestEncoderDecoder(TestConfig())
80+
assertFailsWith<EncodingSizeException> {
81+
"a".decodeBuffered(
82+
decoder,
83+
throwOnOverflow = true,
84+
action = { _, _, _ -> error("Should not make it here") }
85+
)
86+
}
87+
}
88+
89+
@Test
90+
fun givenDecodeInputSizeException_whenThrowOnOverflowFalse_thenEncodingSizeExceptionIsIgnored() {
91+
var invocationInput = 0
92+
val decoder = TestEncoderDecoder(TestConfig(
93+
decodeInputReturn = { invocationInput++; -1 },
94+
))
95+
96+
var invocationAction = 0
97+
object : CharSequence {
98+
override val length: Int = DEFAULT_BUFFER_SIZE + 50
99+
override fun get(index: Int): Char = 'a'
100+
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { error("unused") }
101+
}.decodeBuffered(
102+
decoder,
103+
throwOnOverflow = false,
104+
action = { buf, _, _ ->
105+
invocationAction++
106+
assertEquals(DEFAULT_BUFFER_SIZE, buf.size)
107+
}
108+
)
109+
assertEquals(1, invocationInput)
110+
// Confirms that it went into stream decoding b/c was flushed 2 times
111+
assertEquals(2, invocationAction)
112+
}
113+
114+
@Test
115+
fun givenDecodeInputSize_whenLessThanBufferSize_thenOneShotDecodesWithSmallerSize() {
116+
val expectedSize = 2
117+
val expectedInputSize = DEFAULT_BUFFER_SIZE + 50
118+
var invocationConsume = 0
119+
val decoder = TestEncoderDecoder(
120+
config = TestConfig(
121+
decodeInputReturn = { expectedSize },
122+
),
123+
decoderConsume = { invocationConsume++ },
124+
decoderDoFinal = { (it as TestEncoderDecoder.DecoderFeed).getOut().output(1) },
125+
)
126+
127+
var invocationAction = 0
128+
val result = object : CharSequence {
129+
override val length: Int = expectedInputSize
130+
override fun get(index: Int): Char = 'a'
131+
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = error("unused")
132+
}.decodeBuffered(
133+
decoder,
134+
throwOnOverflow = true,
135+
maxBufSize = expectedSize * 10,
136+
action = { buf, _, len ->
137+
invocationAction++
138+
assertEquals(expectedSize, buf.size)
139+
assertEquals(1, len)
140+
}
141+
)
142+
assertEquals(1, invocationAction)
143+
assertEquals(invocationConsume, expectedInputSize)
144+
assertEquals(1L, result)
145+
}
146+
147+
@Test
148+
fun givenConfigMaxDecodeEmit_whenDecodingBuffered_thenFlushesIfLastIndexWouldBeExceeded() {
149+
val decoder = TestEncoderDecoder(TestConfig(maxDecodeEmit = 50))
150+
151+
var invocationAction = 0
152+
var invocationActionAssertion = 0
153+
var actualLen = 0
154+
val result = object : CharSequence {
155+
override val length: Int = DEFAULT_BUFFER_SIZE
156+
override fun get(index: Int): Char = 'a'
157+
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence = error("unused")
158+
}.decodeBuffered(
159+
decoder,
160+
throwOnOverflow = false,
161+
maxBufSize = DEFAULT_BUFFER_SIZE,
162+
action = { buf, _, len ->
163+
invocationAction++
164+
actualLen += len
165+
assertEquals(DEFAULT_BUFFER_SIZE, buf.size)
166+
if (invocationAction == 1) {
167+
invocationActionAssertion++
168+
assertEquals(DEFAULT_BUFFER_SIZE - decoder.config.maxDecodeEmit + 1, len, "invocationAction[$invocationAction]")
169+
}
170+
if (invocationAction == 2) {
171+
invocationActionAssertion++
172+
assertEquals(decoder.config.maxDecodeEmit, len)
173+
}
174+
}
175+
)
176+
assertEquals(2, invocationAction)
177+
assertEquals(2, invocationActionAssertion)
178+
// TestEncoderDecoder.doFinal default value will output one 1 byte
179+
assertEquals((DEFAULT_BUFFER_SIZE + 1).toLong(), result)
180+
assertEquals(result, actualLen.toLong())
181+
}
182+
}

library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/EncoderDecoderConfigUnitTest.kt

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,18 @@ class EncoderDecoderConfigUnitTest {
2828
fun givenConfig_whenNegativeValuesSent_thenThrowsEncodingSizeExceptionBeforePassingToProtected() {
2929
val config = TestConfig(
3030
encodeReturn = {
31-
fail("Should not make it here")
31+
error("Should not make it here")
3232
},
3333
decodeInputReturn = {
34-
fail("Should not make it here")
34+
error("Should not make it here")
3535
},
3636
decodeReturn = {
37-
fail("Should not make it here")
37+
error("Should not make it here")
3838
}
3939
)
4040

41-
try {
42-
config.decodeOutMaxSize(-1L)
43-
fail()
44-
} catch (_: EncodingSizeException) {
45-
// pass
46-
}
47-
48-
try {
49-
config.encodeOutMaxSize(-1L)
50-
fail()
51-
} catch (_: EncodingSizeException) {
52-
// pass
53-
}
41+
assertFailsWith<EncodingSizeException> { config.decodeOutMaxSize(-1L) }
42+
assertFailsWith<EncodingSizeException> { config.encodeOutMaxSize(-1L) }
5443
}
5544

5645
@Test
@@ -160,13 +149,7 @@ class EncoderDecoderConfigUnitTest {
160149
@Test
161150
fun givenLineBreakInterval_whenSizeIncreaseWouldExceedMaxValue_thenThrowsEncodingSizeException() {
162151
val config = TestConfig(isLenient = true, lineBreakInterval = 10, encodeReturn = { it })
163-
164-
try {
165-
config.encodeOutMaxSize(Long.MAX_VALUE - 10L)
166-
fail()
167-
} catch (_: EncodingSizeException) {
168-
// pass
169-
}
152+
assertFailsWith<EncodingSizeException> { config.encodeOutMaxSize(Long.MAX_VALUE - 10L) }
170153
}
171154

172155
@Test

library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/EncoderDecoderFeedUnitTest.kt

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,44 +34,58 @@ class EncoderDecoderFeedUnitTest {
3434
}
3535

3636
@Test
37-
fun givenDecoderFeed_whenClosed_thenConsumeAndDoFinalThrowException() {
37+
fun givenDecoderFeed_whenClosed_thenThrowsClosedException() {
3838
val feed = TestEncoderDecoder(TestConfig()).newDecoderFeed {}
3939

4040
feed.close()
4141

4242
try {
4343
feed.consume('d')
4444
fail()
45-
} catch (_: EncodingException) {
46-
// pass
45+
} catch (e: EncodingException) {
46+
assertTrue(e.message.contains("is closed"))
47+
}
48+
49+
try {
50+
feed.flush()
51+
fail()
52+
} catch (e: EncodingException) {
53+
assertTrue(e.message.contains("is closed"))
4754
}
4855

4956
try {
5057
feed.doFinal()
5158
fail()
52-
} catch (_: EncodingException) {
53-
// pass
59+
} catch (e: EncodingException) {
60+
assertTrue(e.message.contains("is closed"))
5461
}
5562
}
5663

5764
@Test
58-
fun givenEncoderFeed_whenClosed_thenConsumeAndDoFinalThrowException() {
65+
fun givenEncoderFeed_whenClosed_thenThrowsClosedException() {
5966
val feed = TestEncoderDecoder(TestConfig()).newEncoderFeed {}
6067

6168
feed.close()
6269

6370
try {
6471
feed.consume(5)
6572
fail()
66-
} catch (_: EncodingException) {
67-
// pass
73+
} catch (e: EncodingException) {
74+
assertTrue(e.message.contains("is closed"))
75+
}
76+
77+
try {
78+
feed.flush()
79+
fail()
80+
} catch (e: EncodingException) {
81+
assertTrue(e.message.contains("is closed"))
6882
}
6983

7084
try {
7185
feed.doFinal()
7286
fail()
73-
} catch (_: EncodingException) {
74-
// pass
87+
} catch (e: EncodingException) {
88+
assertTrue(e.message.contains("is closed"))
7589
}
7690
}
7791

@@ -89,6 +103,20 @@ class EncoderDecoderFeedUnitTest {
89103
}
90104
}
91105

106+
@Test
107+
fun givenDecoderFeed_whenFlushThrowsException_thenFeedClosesAutomaticallyBeforeThrowing() {
108+
val feed = TestEncoderDecoder(TestConfig()).newDecoderFeed {
109+
throw IllegalStateException("")
110+
}
111+
112+
try {
113+
feed.flush()
114+
fail()
115+
} catch (_: IllegalStateException) {
116+
assertTrue(feed.isClosed())
117+
}
118+
}
119+
92120
@Test
93121
fun givenEncoderFeed_whenConsumeThrowsException_thenFeedClosesAutomaticallyBeforeThrowing() {
94122
val feed = TestEncoderDecoder(TestConfig()).newEncoderFeed {
@@ -103,6 +131,20 @@ class EncoderDecoderFeedUnitTest {
103131
}
104132
}
105133

134+
@Test
135+
fun givenEncoderFeed_whenFlushThrowsException_thenFeedClosesAutomaticallyBeforeThrowing() {
136+
val feed = TestEncoderDecoder(TestConfig()).newEncoderFeed {
137+
throw IllegalStateException("")
138+
}
139+
140+
try {
141+
feed.flush()
142+
fail()
143+
} catch (_: IllegalStateException) {
144+
assertTrue(feed.isClosed())
145+
}
146+
}
147+
106148
@Test
107149
fun givenDecoderFeed_whenIsLenientTrue_thenSpacesAndNewLinesAreNotSubmitted() {
108150
var out: Byte = 0
@@ -137,7 +179,7 @@ class EncoderDecoderFeedUnitTest {
137179
try {
138180
feed.consume(char)
139181
fail()
140-
} catch (_: EncodingException) {
182+
} catch (_: MalformedEncodingException) {
141183
assertTrue(feed.isClosed())
142184
}
143185
}
@@ -190,7 +232,7 @@ class EncoderDecoderFeedUnitTest {
190232
try {
191233
feed.consume('g')
192234
fail()
193-
} catch (_: EncodingException) {
235+
} catch (_: MalformedEncodingException) {
194236
assertTrue(feed.isClosed())
195237
}
196238
}

library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/helpers/TestEncoderDecoder.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import io.matthewnelson.encoding.core.Decoder
2222

2323
class TestEncoderDecoder(
2424
config: TestConfig,
25+
private val encoderConsume: Encoder.OutFeed.(input: Byte) -> Unit = { output(Char.MAX_VALUE) },
26+
private val decoderConsume: Decoder.OutFeed.(input: Char) -> Unit = { output(Byte.MAX_VALUE) },
2527
private val encoderDoFinal: ((Encoder<TestConfig>.Feed) -> Unit)? = null,
2628
private val decoderDoFinal: ((Decoder<TestConfig>.Feed) -> Unit)? = null,
2729
): EncoderDecoder<TestConfig>(config) {
@@ -33,15 +35,19 @@ class TestEncoderDecoder(
3335

3436
inner class DecoderFeed(out: Decoder.OutFeed): Decoder<TestConfig>.Feed(_out = out) {
3537
fun getOut(): Decoder.OutFeed = _out
36-
override fun consumeProtected(input: Char) { _out.output(Byte.MAX_VALUE) }
38+
override fun consumeProtected(input: Char) {
39+
decoderConsume(_out, input)
40+
}
3741
override fun doFinalProtected() {
3842
decoderDoFinal?.invoke(this) ?: _out.output(Byte.MIN_VALUE)
3943
}
4044
}
4145

4246
inner class EncoderFeed(out: Encoder.OutFeed): Encoder<TestConfig>.Feed(_out = out) {
4347
fun getOut(): Encoder.OutFeed = _out
44-
override fun consumeProtected(input: Byte) { _out.output(Char.MAX_VALUE) }
48+
override fun consumeProtected(input: Byte) {
49+
encoderConsume(_out, input)
50+
}
4551
override fun doFinalProtected() {
4652
encoderDoFinal?.invoke(this) ?: _out.output(Char.MIN_VALUE)
4753
}

library/core/src/commonTest/kotlin/io/matthewnelson/encoding/core/util/DecoderInputUnitTest.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,4 @@ class DecoderInputUnitTest {
136136
@Suppress("DEPRECATION_ERROR")
137137
config.decodeOutMaxSizeOrFail(DecoderInput(validInput))
138138
}
139-
140-
141139
}

0 commit comments

Comments
 (0)