Skip to content

Commit b0a8905

Browse files
committed
Added utilities to debug protobuf conformance tests (#453)
1 parent 9d5b704 commit b0a8905

File tree

11 files changed

+448
-22
lines changed

11 files changed

+448
-22
lines changed

.setup_protoscope.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
3+
#
4+
# Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
5+
#
6+
7+
echo "Installing protoscope"
8+
9+
brew install go
10+
go install github.com/protocolbuffers/protoscope/cmd/protoscope...@latest
11+
12+
PROTOSCOPE_PATH=~/go/bin/protoscope
13+
if [ -f "$PROTOSCOPE_PATH" ]; then
14+
if grep -q "protoscope_path=" local.properties; then
15+
sed -i '' "s|protoscope_path=.*|protoscope_path=$PROTOSCOPE_PATH|" local.properties
16+
else
17+
echo "protoscope_path=$PROTOSCOPE_PATH" >> local.properties
18+
fi
19+
else
20+
echo "Error: protoscope not found at $PROTOSCOPE_PATH"
21+
exit 1
22+
fi

docs/environment.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,39 @@ TODO: write a guide about kRPC API check tests.
363363

364364
Use `./publishLocal.sh` script. All artifacts will be in the local directory of `<REPO_ROOT>/build/repo/` .
365365

366+
## How to debug tests/protobuf-conformance
367+
368+
Prerequisite (Only macOS for now):
369+
```bash
370+
./setup_protoscope.sh
371+
```
372+
It will install the `protoscope` utility: https://github.com/protocolbuffers/protoscope
373+
374+
Now you can run tests:
375+
```bash
376+
gradle :tests:protobuf-conformance:runConformanceTest -Pconformance.test='<test_name>'
377+
```
378+
379+
In the [manual](../tests/protobuf-conformance/build/protobuf-conformance/manual) directory
380+
you will find files:
381+
- dump_conformance_input.bin.txt - decoded ConformanceRequest protobuf message
382+
- dump_conformance_output.bin.txt - decoded ConformanceResponse protobuf message
383+
- dump_payload_input.bin.txt - decoded ConformanceRequest.payload (if protobuf)
384+
- dump_payload_output.bin.txt - decoded ConformanceResponse.result (if protobuf)
385+
386+
IMPORTANT: `protoscope` only works with proto3 and not 'editions' messages.
387+
For proto2 and editions this won't work.
388+
389+
To debug tests,
390+
use [ConformanceClient.kt](../tests/protobuf-conformance/src/main/kotlin/kotlinx/rpc/protoc/gen/test/ConformanceClient.kt)
391+
and this command:
392+
```bash
393+
gradle :tests:protobuf-conformance:runConformanceTest -Pconformance.test.debug='true' -Pconformance.test='<test_name>'
394+
```
395+
396+
Then use IntelliJ 'Attach to Process' feature to attach to the process on port 5005.
397+
(kill the process if port is already in use: `kill $(lsof -t -i:5005)`)
398+
366399
## Troubleshooting
367400

368401
Nothing works? Well, you are onto a journey!
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package util.other
6+
7+
import org.gradle.api.Project
8+
import org.gradle.internal.extensions.core.extra
9+
import org.gradle.kotlin.dsl.provideDelegate
10+
import java.util.Properties
11+
import java.util.concurrent.atomic.AtomicReference
12+
import kotlin.io.path.Path
13+
import kotlin.io.path.inputStream
14+
15+
private val ref = AtomicReference<Properties>()
16+
17+
fun Project.localProperties(): Properties {
18+
if (ref.get() == null) {
19+
ref.compareAndSet(null, Properties().apply {
20+
val globalRootDir: String by extra
21+
22+
load(Path(globalRootDir, "local.properties").inputStream())
23+
})
24+
}
25+
26+
return ref.get()
27+
}

gradle-conventions/src/main/kotlin/util/tasks/protobufConformanceUpdate.kt

Lines changed: 130 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ package util.tasks
66

77
import org.gradle.api.DefaultTask
88
import org.gradle.api.Project
9+
import org.gradle.api.file.FileTree
10+
import org.gradle.api.provider.ListProperty
911
import org.gradle.api.provider.Property
1012
import org.gradle.api.tasks.Copy
13+
import org.gradle.api.tasks.Exec
1114
import org.gradle.api.tasks.Input
1215
import org.gradle.api.tasks.InputFile
16+
import org.gradle.api.tasks.InputFiles
1317
import org.gradle.api.tasks.OutputFile
1418
import org.gradle.api.tasks.TaskAction
1519
import org.gradle.kotlin.dsl.accessors.runtime.addExternalModuleDependencyTo
@@ -21,8 +25,40 @@ import util.other.libs
2125
import java.io.File
2226

2327
const val CONFORMANCE_TEST_RUNNER_CONFIGURATION = "conformanceTestRunner"
28+
const val PROTOC_TESTING_CONFIGURATION = "protoc_internalTesting"
2429
const val UNZIP_PROTOBUF_CONFORMANCE_TASK = "unzipProtobufConformance"
2530
const val WRITE_CONFORMANCE_EXECUTABLE_PATH_TASK = "writeConformanceExecutablePath"
31+
const val PAYLOAD_PB = "payload.pb"
32+
const val CONFORMANCE_PB = "conformance.pb"
33+
34+
private fun Project.getBinFrom(configuration: String): File {
35+
return configurations.getByName(configuration).map {
36+
zipTree(it).matching { include("bin/**") }.files.first()
37+
}.single()
38+
}
39+
40+
private fun Project.getIncludeFrom(configuration: String): List<FileTree> {
41+
return configurations.getByName(configuration).map {
42+
zipTree(it).matching { include("include/**") }
43+
}
44+
}
45+
46+
private fun List<String>.commonPrefix(): String {
47+
return fold(first()) { acc, s -> acc.commonPrefixWith(s) }
48+
}
49+
50+
private fun Project.pbFile(name: String): File {
51+
return layout.buildDirectory.get()
52+
.dir("protobuf-conformance")
53+
.file(name)
54+
.asFile
55+
.apply {
56+
if (!exists()) {
57+
parentFile.mkdirs()
58+
createNewFile()
59+
}
60+
}
61+
}
2662

2763
abstract class ConformanceExecutablePathWriter : DefaultTask() {
2864
@get:Input
@@ -64,6 +100,48 @@ abstract class ConformanceExecutablePathWriter : DefaultTask() {
64100
}
65101
}
66102

103+
abstract class GenerateConformanceFileDescriptorSet : Exec() {
104+
@get:InputFiles
105+
abstract val wktFilesCollection: ListProperty<File>
106+
107+
@get:InputFiles
108+
abstract val conformanceFilesCollection: ListProperty<File>
109+
110+
@get:InputFile
111+
abstract val bin: Property<File>
112+
113+
@get:OutputFile
114+
abstract val outputFile: Property<File>
115+
116+
@TaskAction
117+
fun generate() {
118+
val wktFiles = wktFilesCollection.get().map { it.absolutePath }
119+
val conformanceFiles = conformanceFilesCollection.get().map { it.absolutePath }
120+
121+
val wktProtoPath = if (wktFiles.isEmpty()) {
122+
emptyList()
123+
} else {
124+
listOf("--proto_path=${wktFiles.commonPrefix().substringBefore("/google/protobuf/")}")
125+
}
126+
127+
val conformanceIncludeDir = conformanceFiles
128+
.commonPrefix()
129+
.substringBefore("/google/protobuf/")
130+
.substringBefore("/conformance/")
131+
132+
val conformanceProtoPath = "--proto_path=$conformanceIncludeDir"
133+
134+
commandLine(
135+
bin.get().absolutePath,
136+
*wktProtoPath.toTypedArray(),
137+
conformanceProtoPath,
138+
"-o", outputFile.get().absolutePath,
139+
*wktFiles.toTypedArray(),
140+
*conformanceFiles.toTypedArray(),
141+
)
142+
}
143+
}
144+
67145
fun Project.setupProtobufConformanceResources() {
68146
val os = System.getProperty("os.name").lowercase()
69147
val osPart = when {
@@ -79,7 +157,7 @@ fun Project.setupProtobufConformanceResources() {
79157

80158
// https://stackoverflow.com/questions/23023069/gradle-download-and-unzip-file-from-url
81159
repositories.ivy {
82-
name = "protobuf-conformance-github"
160+
name = "github"
83161
url = uri("https://github.com")
84162

85163
patternLayout {
@@ -93,6 +171,7 @@ fun Project.setupProtobufConformanceResources() {
93171
}
94172

95173
configurations.create(CONFORMANCE_TEST_RUNNER_CONFIGURATION)
174+
configurations.create(PROTOC_TESTING_CONFIGURATION)
96175

97176
// https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/dsl/DependencyHandler.html
98177
dependencies {
@@ -115,10 +194,28 @@ fun Project.setupProtobufConformanceResources() {
115194
}
116195
}
117196

197+
dependencies {
198+
addExternalModuleDependencyTo(
199+
this,
200+
PROTOC_TESTING_CONFIGURATION,
201+
group = "protocolbuffers",
202+
name = "protobuf",
203+
version = libs.versions.protobuf.asProvider().get().substringAfter("."),
204+
classifier = null,
205+
ext = null,
206+
configuration = null,
207+
) {
208+
artifact {
209+
name = "protoc"
210+
type = "zip"
211+
extension = "zip"
212+
classifier = "$osPart-$archPart"
213+
}
214+
}
215+
}
216+
118217
val unzipProtobufConformance = tasks.register<Copy>(UNZIP_PROTOBUF_CONFORMANCE_TASK) {
119-
from(configurations.getByName(CONFORMANCE_TEST_RUNNER_CONFIGURATION).map {
120-
zipTree(it).matching { include("include/**") }
121-
})
218+
from(getIncludeFrom(CONFORMANCE_TEST_RUNNER_CONFIGURATION))
122219

123220
val destDir = project.layout.projectDirectory
124221
.dir("src")
@@ -140,6 +237,34 @@ fun Project.setupProtobufConformanceResources() {
140237
}
141238
}
142239

240+
tasks.register<GenerateConformanceFileDescriptorSet>("generateConformanceFileDescriptorSet_conformance") {
241+
wktFilesCollection.set(emptyList())
242+
243+
val conformanceFiles = project.getIncludeFrom(CONFORMANCE_TEST_RUNNER_CONFIGURATION).flatMap { it.files }
244+
.filter { it.name == "conformance.proto" }
245+
246+
conformanceFilesCollection.set(conformanceFiles)
247+
248+
bin.set(getBinFrom(PROTOC_TESTING_CONFIGURATION))
249+
250+
outputFile.set(project.pbFile(CONFORMANCE_PB))
251+
}
252+
253+
tasks.register<GenerateConformanceFileDescriptorSet>("generateConformanceFileDescriptorSet_payload") {
254+
val wktFiles = project.getIncludeFrom(PROTOC_TESTING_CONFIGURATION).flatMap { it.files }
255+
wktFilesCollection.set(wktFiles)
256+
257+
// editions are not supported in protoscope and proto2 fails
258+
val conformanceFiles = project.getIncludeFrom(CONFORMANCE_TEST_RUNNER_CONFIGURATION).flatMap { it.files }
259+
.filter { it.name == "test_messages_proto3.proto" }
260+
261+
conformanceFilesCollection.set(conformanceFiles)
262+
263+
bin.set(getBinFrom(PROTOC_TESTING_CONFIGURATION))
264+
265+
outputFile.set(project.pbFile(PAYLOAD_PB))
266+
}
267+
143268
val writeConformanceExecutablePath =
144269
tasks.register<ConformanceExecutablePathWriter>(WRITE_CONFORMANCE_EXECUTABLE_PATH_TASK) {
145270
outputDir.set(
@@ -156,11 +281,7 @@ fun Project.setupProtobufConformanceResources() {
156281
.asFile
157282
)
158283

159-
executable.set(
160-
configurations.getByName(CONFORMANCE_TEST_RUNNER_CONFIGURATION).map {
161-
zipTree(it).matching { include("bin/**") }.files.first()
162-
}.single()
163-
)
284+
executable.set(getBinFrom(CONFORMANCE_TEST_RUNNER_CONFIGURATION))
164285

165286
destination.set(
166287
project.layout.buildDirectory.get()

tests/protobuf-conformance/build.gradle.kts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import kotlinx.rpc.buf.tasks.BufGenerateTask
88
import kotlinx.rpc.internal.InternalRpcApi
99
import kotlinx.rpc.internal.configureLocalProtocGenDevelopmentDependency
1010
import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode
11+
import util.other.localProperties
12+
import util.tasks.CONFORMANCE_PB
13+
import util.tasks.GenerateConformanceFileDescriptorSet
14+
import util.tasks.PAYLOAD_PB
1115
import util.tasks.setupProtobufConformanceResources
1216

1317
plugins {
@@ -86,6 +90,58 @@ val generateConformanceTests = tasks.register<JavaExec>("generateConformanceTest
8690
mainClass.set("kotlinx.rpc.protoc.gen.test.GenerateConformanceTestsKt")
8791
}
8892

93+
val conformanceTest = properties.getOrDefault("conformance.test", "").toString()
94+
val conformanceTestDebug = properties.getOrDefault("conformance.test.debug", "false").toString().toBooleanStrictOrNull() ?: false
95+
96+
val generateConformanceFileDescriptorSet = tasks
97+
.withType<GenerateConformanceFileDescriptorSet>()
98+
99+
tasks.register<JavaExec>("runConformanceTest") {
100+
classpath = sourceSets.main.get().runtimeClasspath
101+
102+
dependsOn(mockClientJar)
103+
dependsOn(tasks.named("bufGenerateMain"))
104+
dependsOn(generateConformanceFileDescriptorSet)
105+
106+
args = listOfNotNull(
107+
mockClientJar.get().archiveFile.get().asFile.absolutePath,
108+
conformanceTest,
109+
if (conformanceTestDebug) "--debug" else null
110+
)
111+
112+
mainClass.set("kotlinx.rpc.protoc.gen.test.RunConformanceTestKt")
113+
114+
val protoscope = localProperties().getProperty("protoscope_path")
115+
?: throw GradleException("protoscope_path property is not set. Run ./setup_protoscope.sh")
116+
117+
environment("PROTOSCOPE_PATH", protoscope)
118+
119+
val pbFiles = generateConformanceFileDescriptorSet.map {
120+
it.outputFile.get()
121+
}
122+
123+
environment(
124+
"CONFORMANCE_PB_PATH",
125+
pbFiles.single { it.name == CONFORMANCE_PB }.absolutePath
126+
)
127+
environment(
128+
"TEST_ALL_TYPES_PROTO3_PB_PATH",
129+
pbFiles.single { it.name == PAYLOAD_PB }.absolutePath
130+
)
131+
132+
doFirst {
133+
if (!File(protoscope).exists()) {
134+
throw GradleException(
135+
"""
136+
Protoscope is not found. Use the following command to install it:
137+
138+
$ brew install go
139+
$ go install github.com/protocolbuffers/protoscope/cmd/protoscope...@latest
140+
""".trimIndent()
141+
)
142+
}
143+
}
144+
}
89145

90146
tasks.test {
91147
environment("MOCK_CLIENT_JAR", mockClientJar.get().archiveFile.get().asFile.absolutePath)

0 commit comments

Comments
 (0)