diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-02-25 22:38:19 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-02-25 22:38:44 +0900 |
| commit | f4bf20791f4470d3e9b865a53b610b5f277ebb82 (patch) | |
| tree | 7d0b3f9d3eff87730413290d4436b43ff6146b7a /engine | |
| parent | 56ed04185036635dcfd2763391cd64b8be371b1a (diff) | |
| download | LunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.tar.gz LunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.tar.bz2 LunaticChat-f4bf20791f4470d3e9b865a53b610b5f277ebb82.zip | |
feat: add Codecov/Jacoco integration and expand test coverage
- Add Jacoco plugin to all subprojects with XML report generation
- Replace post-test-results.sh with Codecov upload in CI workflow
- Add codecov.yml configuration
- Add 140 new tests across engine, platform-paper, and platform-velocity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'engine')
7 files changed, 618 insertions, 0 deletions
diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts index c178870..0f4e419 100644 --- a/engine/build.gradle.kts +++ b/engine/build.gradle.kts @@ -12,4 +12,6 @@ dependencies { // Adventure API (provided by platform implementations) compileOnly("net.kyori:adventure-api:4.26.1") + + testImplementation("net.kyori:adventure-api:4.26.1") } diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt new file mode 100644 index 0000000..5656a14 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/ChatModeTest.kt @@ -0,0 +1,33 @@ +package dev.m1sk9.lunaticChat.engine.chat + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ChatModeTest { + @Test + fun `toggle should switch GLOBAL to CHANNEL`() { + assertEquals(ChatMode.CHANNEL, ChatMode.GLOBAL.toggle()) + } + + @Test + fun `toggle should switch CHANNEL to GLOBAL`() { + assertEquals(ChatMode.GLOBAL, ChatMode.CHANNEL.toggle()) + } + + @Test + fun `DEFAULT should be GLOBAL`() { + assertEquals(ChatMode.GLOBAL, ChatMode.DEFAULT) + } + + @Test + fun `double toggle should return original mode`() { + assertEquals(ChatMode.GLOBAL, ChatMode.GLOBAL.toggle().toggle()) + assertEquals(ChatMode.CHANNEL, ChatMode.CHANNEL.toggle().toggle()) + } + + @Test + fun `enum should have exactly two values`() { + assertEquals(2, ChatMode.entries.size) + assertEquals(setOf(ChatMode.GLOBAL, ChatMode.CHANNEL), ChatMode.entries.toSet()) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt new file mode 100644 index 0000000..b24e169 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/chat/channel/ChannelTest.kt @@ -0,0 +1,139 @@ +package dev.m1sk9.lunaticChat.engine.chat.channel + +import kotlinx.serialization.json.Json +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class ChannelTest { + private val testOwnerId = UUID.fromString("00000001-0000-0000-0000-000000000000") + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun `valid channel ID should be accepted`() { + val channel = Channel(id = "test-channel", name = "Test", ownerId = testOwnerId) + assertEquals("test-channel", channel.id) + } + + @Test + fun `channel ID with underscores and hyphens should be accepted`() { + val channel = Channel(id = "my_test-channel", name = "Test", ownerId = testOwnerId) + assertEquals("my_test-channel", channel.id) + } + + @Test + fun `channel ID with exactly 3 characters should be accepted`() { + val channel = Channel(id = "abc", name = "Test", ownerId = testOwnerId) + assertEquals("abc", channel.id) + } + + @Test + fun `channel ID with exactly 30 characters should be accepted`() { + val id = "a".repeat(30) + val channel = Channel(id = id, name = "Test", ownerId = testOwnerId) + assertEquals(id, channel.id) + } + + @Test + fun `channel ID with less than 3 characters should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "ab", name = "Test", ownerId = testOwnerId) + } + } + + @Test + fun `channel ID with more than 30 characters should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "a".repeat(31), name = "Test", ownerId = testOwnerId) + } + } + + @Test + fun `channel ID with spaces should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "test channel", name = "Test", ownerId = testOwnerId) + } + } + + @Test + fun `channel ID with special characters should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "test@channel", name = "Test", ownerId = testOwnerId) + } + } + + @Test + fun `empty channel ID should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "", name = "Test", ownerId = testOwnerId) + } + } + + @Test + fun `blank channel name should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "valid-id", name = " ", ownerId = testOwnerId) + } + } + + @Test + fun `empty channel name should be rejected`() { + assertFailsWith<IllegalArgumentException> { + Channel(id = "valid-id", name = "", ownerId = testOwnerId) + } + } + + @Test + fun `default values should be correct`() { + val channel = Channel(id = "test-ch", name = "Test Channel", ownerId = testOwnerId) + assertNull(channel.description) + assertFalse(channel.isPrivate) + assertTrue(channel.bannedPlayers.isEmpty()) + } + + @Test + fun `serialization round-trip should preserve all fields`() { + val bannedPlayer = UUID.fromString("00000002-0000-0000-0000-000000000000") + val original = + Channel( + id = "test-channel", + name = "Test Channel", + description = "A test channel", + isPrivate = true, + ownerId = testOwnerId, + createdAt = 1000L, + bannedPlayers = setOf(bannedPlayer), + ) + + val jsonString = json.encodeToString(Channel.serializer(), original) + val decoded = json.decodeFromString(Channel.serializer(), jsonString) + + assertEquals(original.id, decoded.id) + assertEquals(original.name, decoded.name) + assertEquals(original.description, decoded.description) + assertEquals(original.isPrivate, decoded.isPrivate) + assertEquals(original.ownerId, decoded.ownerId) + assertEquals(original.createdAt, decoded.createdAt) + assertEquals(original.bannedPlayers, decoded.bannedPlayers) + } + + @Test + fun `CHANNEL_ID_PATTERN should match valid patterns`() { + assertTrue("abc".matches(Channel.CHANNEL_ID_PATTERN)) + assertTrue("test-123".matches(Channel.CHANNEL_ID_PATTERN)) + assertTrue("my_channel".matches(Channel.CHANNEL_ID_PATTERN)) + assertTrue("ABC123".matches(Channel.CHANNEL_ID_PATTERN)) + } + + @Test + fun `CHANNEL_ID_PATTERN should reject invalid patterns`() { + assertFalse("ab".matches(Channel.CHANNEL_ID_PATTERN)) + assertFalse("".matches(Channel.CHANNEL_ID_PATTERN)) + assertFalse("test channel".matches(Channel.CHANNEL_ID_PATTERN)) + assertFalse("test@ch".matches(Channel.CHANNEL_ID_PATTERN)) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt new file mode 100644 index 0000000..b80f116 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/command/CommandResultTest.kt @@ -0,0 +1,54 @@ +package dev.m1sk9.lunaticChat.engine.command + +import net.kyori.adventure.text.Component +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class CommandResultTest { + @Test + fun `Success toBrigadierResult should return 1`() { + assertEquals(1, CommandResult.Success.toBrigadierResult()) + } + + @Test + fun `SuccessWithMessage toBrigadierResult should return 1`() { + val result = CommandResult.SuccessWithMessage(Component.text("ok")) + assertEquals(1, result.toBrigadierResult()) + } + + @Test + fun `Failure toBrigadierResult should return 0`() { + val result = CommandResult.Failure(Component.text("error")) + assertEquals(0, result.toBrigadierResult()) + } + + @Test + fun `InvalidUsage toBrigadierResult should return 0`() { + val result = CommandResult.InvalidUsage("/cmd <arg>") + assertEquals(0, result.toBrigadierResult()) + } + + @Test + fun `SuccessWithMessage should preserve message`() { + val message = Component.text("Test message") + val result = CommandResult.SuccessWithMessage(message) + assertIs<CommandResult.SuccessWithMessage>(result) + assertEquals(message, result.message) + } + + @Test + fun `Failure should preserve message`() { + val message = Component.text("Error message") + val result = CommandResult.Failure(message) + assertIs<CommandResult.Failure>(result) + assertEquals(message, result.message) + } + + @Test + fun `InvalidUsage should preserve usage hint`() { + val result = CommandResult.InvalidUsage("/lc setting <key> <on|off>") + assertIs<CommandResult.InvalidUsage>(result) + assertEquals("/lc setting <key> <on|off>", result.usageHint) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt new file mode 100644 index 0000000..c792aad --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/PluginMessageCodecTest.kt @@ -0,0 +1,233 @@ +package dev.m1sk9.lunaticChat.engine.protocol + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs + +class PluginMessageCodecTest { + @Test + fun `encode and decode Handshake round-trip`() { + val original = + PluginMessage.Handshake( + pluginVersion = "0.10.0", + protocolMajor = 1, + protocolMinor = 0, + protocolPatch = 0, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.Handshake>(decoded) + assertEquals(original.pluginVersion, decoded.pluginVersion) + assertEquals(original.protocolMajor, decoded.protocolMajor) + assertEquals(original.protocolMinor, decoded.protocolMinor) + assertEquals(original.protocolPatch, decoded.protocolPatch) + } + + @Test + fun `encode and decode HandshakeResponse compatible round-trip`() { + val original = + PluginMessage.HandshakeResponse( + compatible = true, + velocityVersion = "0.10.0", + error = null, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.HandshakeResponse>(decoded) + assertEquals(original.compatible, decoded.compatible) + assertEquals(original.velocityVersion, decoded.velocityVersion) + assertEquals(original.error, decoded.error) + } + + @Test + fun `encode and decode HandshakeResponse incompatible round-trip`() { + val original = + PluginMessage.HandshakeResponse( + compatible = false, + velocityVersion = "0.10.0", + error = "Version mismatch", + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.HandshakeResponse>(decoded) + assertEquals(false, decoded.compatible) + assertEquals("Version mismatch", decoded.error) + } + + @Test + fun `encode and decode StatusRequest round-trip`() { + val original = PluginMessage.StatusRequest + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.StatusRequest>(decoded) + } + + @Test + fun `encode and decode StatusResponse round-trip`() { + val original = + PluginMessage.StatusResponse( + velocityVersion = "0.10.0", + protocolVersion = "1.0.0", + online = true, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.StatusResponse>(decoded) + assertEquals(original.velocityVersion, decoded.velocityVersion) + assertEquals(original.protocolVersion, decoded.protocolVersion) + assertEquals(original.online, decoded.online) + } + + @Test + fun `encode and decode GlobalChatMessage round-trip`() { + val original = + PluginMessage.GlobalChatMessage( + messageId = "test-id-123", + serverName = "lobby", + playerId = "00000001-0000-0000-0000-000000000000", + playerName = "TestPlayer", + message = "Hello, world!", + timestamp = 1000L, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.GlobalChatMessage>(decoded) + assertEquals(original.messageId, decoded.messageId) + assertEquals(original.serverName, decoded.serverName) + assertEquals(original.playerId, decoded.playerId) + assertEquals(original.playerName, decoded.playerName) + assertEquals(original.message, decoded.message) + assertEquals(original.timestamp, decoded.timestamp) + } + + @Test + fun `decode should throw on unknown sub-channel`() { + val out = java.io.ByteArrayOutputStream() + val dataOut = java.io.DataOutputStream(out) + dataOut.writeUTF("unknown_channel") + dataOut.writeUTF("{}") + + assertFailsWith<IllegalArgumentException> { + PluginMessageCodec.decode(out.toByteArray()) + } + } + + @Test + fun `decode should throw on empty data`() { + assertFailsWith<Exception> { + PluginMessageCodec.decode(byteArrayOf()) + } + } + + @Test + fun `encode and decode GlobalChatMessage with special characters`() { + val original = + PluginMessage.GlobalChatMessage( + messageId = "msg-special", + serverName = "survival", + playerId = "00000002-0000-0000-0000-000000000000", + playerName = "Player_With-Dash", + message = "Hello! こんにちは 🎉 \"quotes\" & <tags>", + timestamp = 2000L, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.GlobalChatMessage>(decoded) + assertEquals(original.message, decoded.message) + } + + @Test + fun `SubChannel constants should have correct values`() { + assertEquals("handshake", PluginMessageCodec.SubChannel.HANDSHAKE) + assertEquals("handshake_response", PluginMessageCodec.SubChannel.HANDSHAKE_RESPONSE) + assertEquals("status_request", PluginMessageCodec.SubChannel.STATUS_REQUEST) + assertEquals("status_response", PluginMessageCodec.SubChannel.STATUS_RESPONSE) + assertEquals("global_chat", PluginMessageCodec.SubChannel.GLOBAL_CHAT) + } + + @Test + fun `encode should produce non-empty byte array for all message types`() { + val messages = + listOf( + PluginMessage.Handshake("1.0.0", 1, 0, 0), + PluginMessage.HandshakeResponse(true, "1.0.0"), + PluginMessage.StatusRequest, + PluginMessage.StatusResponse("1.0.0", "1.0.0", true), + PluginMessage.GlobalChatMessage("id", "srv", "pid", "name", "msg", 0L), + ) + + messages.forEach { message -> + val encoded = PluginMessageCodec.encode(message) + assert(encoded.isNotEmpty()) { "Encoded ${message::class.simpleName} should not be empty" } + } + } + + @Test + fun `StatusResponse with online false round-trip`() { + val original = + PluginMessage.StatusResponse( + velocityVersion = "0.10.0", + protocolVersion = "1.0.0", + online = false, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.StatusResponse>(decoded) + assertEquals(false, decoded.online) + } + + @Test + fun `Handshake with various protocol versions round-trip`() { + val original = + PluginMessage.Handshake( + pluginVersion = "2.5.3", + protocolMajor = 99, + protocolMinor = 42, + protocolPatch = 7, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.Handshake>(decoded) + assertEquals(99, decoded.protocolMajor) + assertEquals(42, decoded.protocolMinor) + assertEquals(7, decoded.protocolPatch) + } + + @Test + fun `GlobalChatMessage with empty message round-trip`() { + val original = + PluginMessage.GlobalChatMessage( + messageId = "msg-empty", + serverName = "lobby", + playerId = "00000003-0000-0000-0000-000000000000", + playerName = "Player", + message = "", + timestamp = 3000L, + ) + + val encoded = PluginMessageCodec.encode(original) + val decoded = PluginMessageCodec.decode(encoded) + + assertIs<PluginMessage.GlobalChatMessage>(decoded) + assertEquals("", decoded.message) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt new file mode 100644 index 0000000..7f05039 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/protocol/ProtocolVersionTest.kt @@ -0,0 +1,57 @@ +package dev.m1sk9.lunaticChat.engine.protocol + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ProtocolVersionTest { + @Test + fun `version string should match MAJOR MINOR PATCH format`() { + assertEquals("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.${ProtocolVersion.PATCH}", ProtocolVersion.version) + } + + @Test + fun `isCompatible with matching major and minor should return true`() { + assertTrue(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR)) + } + + @Test + fun `isCompatible with different major should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR + 1, ProtocolVersion.MINOR)) + } + + @Test + fun `isCompatible with different minor should return false`() { + assertFalse(ProtocolVersion.isCompatible(ProtocolVersion.MAJOR, ProtocolVersion.MINOR + 1)) + } + + @Test + fun `isCompatible string with matching version should return true`() { + assertTrue(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.0")) + } + + @Test + fun `isCompatible string with different patch should return true`() { + assertTrue(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR}.${ProtocolVersion.MINOR}.99")) + } + + @Test + fun `isCompatible string with different major should return false`() { + assertFalse(ProtocolVersion.isCompatible("${ProtocolVersion.MAJOR + 1}.${ProtocolVersion.MINOR}.0")) + } + + @Test + fun `isCompatible string with malformed version should return false`() { + assertFalse(ProtocolVersion.isCompatible("invalid")) + assertFalse(ProtocolVersion.isCompatible("1.0")) + assertFalse(ProtocolVersion.isCompatible("")) + assertFalse(ProtocolVersion.isCompatible("a.b.c")) + assertFalse(ProtocolVersion.isCompatible("1.0.0.0")) + } + + @Test + fun `isCompatible string with two parts should return false`() { + assertFalse(ProtocolVersion.isCompatible("1.0")) + } +} diff --git a/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt new file mode 100644 index 0000000..599a372 --- /dev/null +++ b/engine/src/test/kotlin/dev/m1sk9/lunaticChat/engine/settings/UUIDSerializerTest.kt @@ -0,0 +1,100 @@ +package dev.m1sk9.lunaticChat.engine.settings + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import java.util.UUID +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class UUIDSerializerTest { + private val json = Json + + @Serializable + private data class UUIDHolder( + @Serializable(with = UUIDSerializer::class) + val uuid: UUID, + ) + + @Serializable + private data class UUIDAsStringHolder( + @Serializable(with = UUIDASStringSerializer::class) + val uuid: UUID, + ) + + @Test + fun `UUIDSerializer should serialize UUID to string`() { + val uuid = UUID.fromString("12345678-1234-1234-1234-123456789abc") + val holder = UUIDHolder(uuid) + + val jsonString = json.encodeToString(UUIDHolder.serializer(), holder) + + assert(jsonString.contains("12345678-1234-1234-1234-123456789abc")) + } + + @Test + fun `UUIDSerializer should deserialize string to UUID`() { + val jsonString = """{"uuid":"12345678-1234-1234-1234-123456789abc"}""" + val holder = json.decodeFromString(UUIDHolder.serializer(), jsonString) + + assertEquals(UUID.fromString("12345678-1234-1234-1234-123456789abc"), holder.uuid) + } + + @Test + fun `UUIDSerializer round-trip should preserve UUID`() { + val originalUuid = UUID.randomUUID() + val holder = UUIDHolder(originalUuid) + + val jsonString = json.encodeToString(UUIDHolder.serializer(), holder) + val decoded = json.decodeFromString(UUIDHolder.serializer(), jsonString) + + assertEquals(originalUuid, decoded.uuid) + } + + @Test + fun `UUIDASStringSerializer should serialize UUID to string`() { + val uuid = UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789") + val holder = UUIDAsStringHolder(uuid) + + val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder) + + assert(jsonString.contains("abcdef01-2345-6789-abcd-ef0123456789")) + } + + @Test + fun `UUIDASStringSerializer should deserialize string to UUID`() { + val jsonString = """{"uuid":"abcdef01-2345-6789-abcd-ef0123456789"}""" + val holder = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) + + assertEquals(UUID.fromString("abcdef01-2345-6789-abcd-ef0123456789"), holder.uuid) + } + + @Test + fun `UUIDASStringSerializer round-trip should preserve UUID`() { + val originalUuid = UUID.randomUUID() + val holder = UUIDAsStringHolder(originalUuid) + + val jsonString = json.encodeToString(UUIDAsStringHolder.serializer(), holder) + val decoded = json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) + + assertEquals(originalUuid, decoded.uuid) + } + + @Test + fun `UUIDSerializer should fail on invalid UUID string`() { + val jsonString = """{"uuid":"not-a-valid-uuid"}""" + + assertFailsWith<Exception> { + json.decodeFromString(UUIDHolder.serializer(), jsonString) + } + } + + @Test + fun `UUIDASStringSerializer should fail on invalid UUID string`() { + val jsonString = """{"uuid":"not-a-valid-uuid"}""" + + assertFailsWith<Exception> { + json.decodeFromString(UUIDAsStringHolder.serializer(), jsonString) + } + } +} |
