From 96728011dc80e728ab94abf24e02f0db870554b1 Mon Sep 17 00:00:00 2001 From: Ludwig Valda Vasquez Date: Tue, 27 Jan 2026 16:14:45 +0100 Subject: [PATCH] refactor [lsp]: add tests for the read line func GitOrigin-RevId: 153cdd1c4f05c31292946809514890384a89ec53 --- .idea/modules.xml | 1 + build/bazel-generated-file-list.txt | 1 + fleet/lsp.protocol/test/BUILD.bazel | 55 ++++++++ .../test/fleet.lsp.protocol.test.iml | 35 +++++ .../test/gradlebuild/build.gradle.kts | 72 ++++++++++ .../com/jetbrains/lsp/test/ByteReaderTest.kt | 129 ++++++++++++++++++ 6 files changed, 293 insertions(+) create mode 100644 fleet/lsp.protocol/test/BUILD.bazel create mode 100644 fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml create mode 100644 fleet/lsp.protocol/test/gradlebuild/build.gradle.kts create mode 100644 fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 086a6a9cb4ad..8d131a60d490 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -24,6 +24,7 @@ + diff --git a/build/bazel-generated-file-list.txt b/build/bazel-generated-file-list.txt index 57842044c818..41b2ce271e64 100644 --- a/build/bazel-generated-file-list.txt +++ b/build/bazel-generated-file-list.txt @@ -240,6 +240,7 @@ fleet/ktor/client/core fleet/ktor/network/tls fleet/ktor/server/cio fleet/lsp.protocol +fleet/lsp.protocol/test fleet/modules/api fleet/modules/jvm fleet/multiplatform.shims diff --git a/fleet/lsp.protocol/test/BUILD.bazel b/fleet/lsp.protocol/test/BUILD.bazel new file mode 100644 index 000000000000..4f08dcf384e3 --- /dev/null +++ b/fleet/lsp.protocol/test/BUILD.bazel @@ -0,0 +1,55 @@ +### auto-generated section `build fleet.lsp.protocol.test` start +load("//build:compiler-options.bzl", "create_kotlinc_options") +load("@rules_jvm//:jvm.bzl", "jvm_library") + +create_kotlinc_options( + name = "custom_test", + opt_in = [ + "kotlinx.coroutines.ExperimentalCoroutinesApi", + "kotlinx.serialization.ExperimentalSerializationApi", + "kotlin.concurrent.atomics.ExperimentalAtomicApi", + ], + x_consistent_data_class_copy_visibility = True, + x_context_parameters = True, + x_lambdas = "class" +) + +jvm_library( + name = "test", + visibility = ["//visibility:public"], + srcs = glob([], allow_empty = True, exclude = ["**/module-info.java"]), + runtime_deps = [ + "@lib//:kotlin-stdlib", + "@lib//:kotlinx-coroutines-core", + "@lib//:kotlinx-io-core", + ] +) + +jvm_library( + name = "test_test_lib", + module_name = "fleet.lsp.protocol.test", + visibility = ["//visibility:public"], + srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True, exclude = ["**/module-info.java"]), + kotlinc_opts = ":custom_test", + deps = [ + "@lib//:kotlin-stdlib", + "@lib//:kotlinx-coroutines-core", + "@lib//:kotlinx-io-core", + "//fleet/ktor/client/cio", + "//fleet/ktor/client/core", + "//fleet/ktor/network/tls", + "//fleet/lsp.protocol", + "//fleet/test-runtime", + ], + runtime_deps = [":test"] +) +### auto-generated section `build fleet.lsp.protocol.test` end + +### auto-generated section `test fleet.lsp.protocol.test` start +load("@community//build:tests-options.bzl", "jps_test") + +jps_test( + name = "test_test", + runtime_deps = [":test_test_lib"] +) +### auto-generated section `test fleet.lsp.protocol.test` end \ No newline at end of file diff --git a/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml b/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml new file mode 100644 index 000000000000..c54e2f4e2645 --- /dev/null +++ b/fleet/lsp.protocol/test/fleet.lsp.protocol.test.iml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts b/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts new file mode 100644 index 000000000000..aca461499cf1 --- /dev/null +++ b/fleet/lsp.protocol/test/gradlebuild/build.gradle.kts @@ -0,0 +1,72 @@ +// IMPORT__MARKER_START +import fleet.buildtool.conventions.configureAtMostOneJvmTargetOrThrow +import fleet.buildtool.conventions.withJavaSourceSet +// IMPORT__MARKER_END +plugins { + alias(libs.plugins.kotlin.multiplatform) + id("fleet.project-module-conventions") + id("fleet.toolchain-conventions") + id("fleet.module-publishing-conventions") + id("fleet.sdk-repositories-publishing-conventions") + id("fleet.open-source-module-conventions") + alias(libs.plugins.dokka) + // GRADLE_PLUGINS__MARKER_START + id("fleet-module") + // GRADLE_PLUGINS__MARKER_END +} + +fleetModule { + module { + name = "fleet.lsp.protocol.test" + importedFromJps {} + test {} + } +} + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) +kotlin { + // KOTLIN__MARKER_START + compilerOptions.freeCompilerArgs = listOf( + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xlambdas=class", + "-Xconsistent-data-class-copy-visibility", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-Xcontext-parameters", + "-Xjvm-default=all", + "-XXLanguage:+AllowEagerSupertypeAccessibilityChecks", + "-opt-in=kotlin.concurrent.atomics.ExperimentalAtomicApi", + "-progressive", + ) + jvm {} + sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../src")) } + configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../src")) } } } + sourceSets.commonMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonMain")) } + sourceSets.commonMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonMain")) } + sourceSets.commonTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcCommonTest")) } + sourceSets.commonTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesCommonTest")) } + sourceSets.jvmMain.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmMain")) } + configureAtMostOneJvmTargetOrThrow { compilations.named("main") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmMain")) } } } + sourceSets.jvmMain.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmMain")) } + sourceSets.jvmTest.configure { kotlin.srcDir(layout.projectDirectory.dir("../srcJvmTest")) } + configureAtMostOneJvmTargetOrThrow { compilations.named("test") { withJavaSourceSet { javaSourceSet -> javaSourceSet.java.srcDir(layout.projectDirectory.dir("../srcJvmTest")) } } } + sourceSets.jvmTest.configure { resources.srcDir(layout.projectDirectory.dir("../resourcesJvmTest")) } + sourceSets.commonMain.dependencies { + implementation(jps.org.jetbrains.kotlin.kotlin.stdlib1993400674.get().let { "${it.group}:${it.name}:${it.version}" }) { + exclude(group = "org.jetbrains", module = "annotations") + } + implementation(jps.org.jetbrains.intellij.deps.kotlinx.kotlinx.coroutines.core.jvm930800474.get().let { "${it.group}:kotlinx-coroutines-core:${it.version}" }) { + isTransitive = false + } + implementation(jps.org.jetbrains.kotlinx.kotlinx.io.core.jvm479158162.get().let { "${it.group}:kotlinx-io-core:${it.version}" }) { + exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") + } + } + sourceSets.commonTest.dependencies { + implementation(project(":fleet.ktor.client.cio")) + implementation(project(":fleet.ktor.client.core")) + implementation(project(":fleet.ktor.network.tls")) + implementation(project(":fleet.lsp.protocol")) + implementation(project(":fleet.test.runtime")) + } + // KOTLIN__MARKER_END +} \ No newline at end of file diff --git a/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt b/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt new file mode 100644 index 000000000000..187387c461c7 --- /dev/null +++ b/fleet/lsp.protocol/test/src/com/jetbrains/lsp/test/ByteReaderTest.kt @@ -0,0 +1,129 @@ +package com.jetbrains.lsp.test + +import com.jetbrains.lsp.implementation.ByteReader +import com.jetbrains.lsp.implementation.readUTF8Line +import io.ktor.utils.io.* +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.withIndex +import kotlinx.coroutines.runBlocking +import kotlinx.io.Source +import kotlin.test.Test +import kotlin.test.assertEquals + +private const val LF = 0x0A.toByte() +private const val CR = 0x0D.toByte() + +class ByteReaderTest { + + @Test + fun `readUTF8Line reads an LF-terminated line`() { + runBlocking { + val message = "Hello, world!" + val channel = ByteChannel() + val reader = ByteChannelReader(channel) + + val line = async { reader.readUTF8Line() } + channel.writeLine(message, LF) + channel.flushAndClose() + + assertEquals(message, line.await()) + } + } + + @Test + fun `readUTF8Line reads an LF-terminated line sent byte by byte`() { + runBlocking { + val message = "Hello, world!" + val channel = ByteChannel() + val reader = ByteChannelReader(channel) + + val line = async { reader.readUTF8Line() } + message.toByteArray(Charsets.UTF_8).forEach { byte -> + channel.writeByte(byte) + channel.flush() + } + channel.writeByte(LF) + channel.flushAndClose() + + assertEquals(message, line.await()) + } + } + + @Test + fun `readUTF8Line reads a CRLF-terminated line sent byte by byte`() { + runBlocking { + val message = "Hello, world!" + val channel = ByteChannel() + val reader = ByteChannelReader(channel) + + val line = async { reader.readUTF8Line() } + message.toByteArray(Charsets.UTF_8).forEach { byte -> + channel.writeByte(byte) + channel.flush() + } + channel.writeBytes(CR, LF) + channel.flushAndClose() + + assertEquals(message, line.await()) + } + } + + @Test + fun `readUTF8Line reads several LF-terminated lines`() { + runBlocking { + val messages = listOf( + "Hello", "world", "how", "are", "you", "doing" + ) + val channel = ByteChannel() + val reader = ByteChannelReader(channel) + + val lines = flow { + while (!reader.isClosedForRead) { + emit(reader.readUTF8Line()) + } + } + + for (message in messages) { + channel.writeLine(message, LF) + channel.flush() + } + channel.close() + + lines.withIndex().collect { (i, line) -> + assertEquals(messages[i], line) + } + } + } + + private suspend fun ByteWriteChannel.writeBytes(vararg bytes: Byte) { + for (byte in bytes) { + writeByte(byte) + } + } + + private suspend fun ByteWriteChannel.writeLine(s: String, vararg terminators: Byte = byteArrayOf(LF)) { + writeStringUtf8(s) + for (terminator in terminators) { + writeByte(terminator) + } + } +} + +@OptIn(InternalAPI::class) +internal class ByteChannelReader(val underlying: ByteChannel) : ByteReader { + override val closedCause: Throwable? + get() = underlying.closedCause + override val isClosedForRead: Boolean + get() = underlying.isClosedForRead + override val readBuffer: Source + get() = underlying.readBuffer + + override suspend fun awaitContent(min: Int): Boolean { + return underlying.awaitContent() + } + + override fun cancel(cause: Throwable?) { + underlying.cancel(cause) + } +} \ No newline at end of file