Skip to content

Commit d34eedf

Browse files
authored
Fix native tests not running issues (#258)
1 parent cf25b78 commit d34eedf

6 files changed

Lines changed: 84 additions & 79 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77

88
env:
99
DOCKER_API_VERSION: 1.48
10+
DOCKER_CE_VERSION: 28.3.3
1011

1112
concurrency:
1213
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,24 +18,14 @@ permissions:
1718

1819
jobs:
1920
jvm:
20-
strategy:
21-
fail-fast: false
22-
matrix:
23-
docker_version:
24-
- "29.2.1"
25-
runs-on: ubuntu-latest
21+
runs-on: ubuntu-24.04
2622
name: JVM
2723
steps:
2824
- name: Checkout
2925
uses: actions/checkout@v6
3026
with:
3127
fetch-depth: 0
3228

33-
- name: Setup Docker CE v${{ matrix.docker_version }}
34-
uses: docker/setup-docker-action@v4.6.0
35-
with:
36-
version: v${{ matrix.docker_version }}
37-
3829
- name: Set up JDK 17
3930
uses: actions/setup-java@v5
4031
with:
@@ -49,53 +40,44 @@ jobs:
4940

5041
- name: Run Tests
5142
run: ./gradlew jvmTest
43+
44+
- name: Upload test reports
45+
if: failure()
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: test-report-jvm
49+
path: build/reports/tests/
5250
native:
5351
if: ${{ vars.NATIVE_TESTS_ENABLED == 'true' }}
5452
strategy:
5553
fail-fast: false
5654
matrix:
57-
docker_version:
58-
- "29.2.1"
5955
os:
6056
- name: Linux
61-
runner: ubuntu-22.04
57+
runner: ubuntu-24.04
6258
sourceset: linuxX64
6359

64-
- name: macOS (Intel)
60+
- name: macOS
6561
runner: macos-15-intel
6662
sourceset: macosX64
6763

68-
- name: macOS (Apple Silicon)
69-
runner: macos-15
70-
sourceset: macosArm64
71-
7264
- name: Windows
7365
runner: windows-2025
7466
sourceset: mingwX64
7567

7668
runs-on: ${{ matrix.os.runner }}
77-
name: Native - ${{ matrix.os.name }}
69+
name: ${{ matrix.os.name }}
7870
steps:
7971
- name: Checkout
8072
uses: actions/checkout@v6
8173
with:
8274
fetch-depth: 0
8375

84-
- name: Setup Docker CE v${{ matrix.docker_version }}
76+
- name: Setup Docker CE
8577
uses: docker/setup-docker-action@v4.6.0
86-
if: ${{ matrix.os.sourceset != 'macosArm64' }}
78+
if: ${{ matrix.os.sourceset == 'macosX64' }}
8779
with:
88-
version: v${{ matrix.docker_version }}
89-
90-
- name: Setup Homebrew
91-
uses: Homebrew/actions/setup-homebrew@master
92-
if: ${{ matrix.os.sourceset == 'macosArm64' }}
93-
94-
- name: Setup Docker
95-
if: ${{ matrix.os.sourceset == 'macosArm64' }}
96-
run: |
97-
brew install colima docker
98-
colima start
80+
version: v${{ env.DOCKER_CE_VERSION }}
9981

10082
- name: Set up JDK 17
10183
uses: actions/setup-java@v5
@@ -111,3 +93,10 @@ jobs:
11193

11294
- name: Run Tests
11395
run: ./gradlew ${{ matrix.os.sourceset }}Test
96+
97+
- name: Upload test reports
98+
if: failure()
99+
uses: actions/upload-artifact@v4
100+
with:
101+
name: test-report-${{ matrix.os.sourceset }}
102+
path: build/reports/tests/

src/commonMain/kotlin/me/devnatan/dockerkt/resource/container/ContainerResource.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import io.ktor.client.statement.bodyAsChannel
1414
import io.ktor.client.statement.readRawBytes
1515
import io.ktor.http.ContentType
1616
import io.ktor.http.HttpStatusCode
17+
import io.ktor.http.content.ByteArrayContent
1718
import io.ktor.http.contentType
1819
import io.ktor.util.decodeBase64Bytes
1920
import io.ktor.utils.io.ByteReadChannel
@@ -513,8 +514,10 @@ public class ContainerResource internal constructor(
513514
parameter("noOverwriteDirNonDir", options.noOverwriteDirNonDir.toString())
514515
parameter("copyUIDGID", options.copyUIDGID.toString())
515516

516-
setBody(tarArchive)
517-
contentType(ContentType.Application.OctetStream)
517+
// ByteArrayContent guarantees Content-Length is set; plain setBody(ByteArray) can fall
518+
// back to chunked transfer on Ktor CIO native, which Docker's archive endpoint rejects
519+
// with "request body length should be specified".
520+
setBody(ByteArrayContent(tarArchive, ContentType.Application.OctetStream))
518521
}
519522
}
520523

src/commonTest/kotlin/me/devnatan/dockerkt/resource/ResourceIT.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ package me.devnatan.dockerkt.resource
22

33
import me.devnatan.dockerkt.DockerClient
44
import me.devnatan.dockerkt.createTestDockerClient
5+
import kotlin.jvm.JvmOverloads
6+
import kotlin.test.AfterTest
57

68
open class ResourceIT(
7-
private val debugHttpCalls: Boolean = true,
9+
private val debugHttpCalls: Boolean,
810
) {
11+
constructor() : this(debugHttpCalls = true)
12+
913
val testClient: DockerClient by lazy {
1014
createTestDockerClient {
1115
debugHttpCalls(this@ResourceIT.debugHttpCalls)
1216
}
1317
}
18+
19+
@AfterTest
20+
fun closeTestClient() {
21+
runCatching { testClient.close() }
22+
}
1423
}

src/commonTest/kotlin/me/devnatan/dockerkt/resource/container/CopyContainerArchivesIT.kt

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package me.devnatan.dockerkt.resource.container
22

3-
import kotlinx.coroutines.delay
43
import kotlinx.coroutines.test.runTest
54
import kotlinx.io.files.Path
65
import me.devnatan.dockerkt.io.FileSystemUtils
7-
import me.devnatan.dockerkt.models.exec.ExecStartOptions
8-
import me.devnatan.dockerkt.models.exec.ExecStartResult
96
import me.devnatan.dockerkt.resource.ResourceIT
10-
import me.devnatan.dockerkt.resource.exec.create
117
import me.devnatan.dockerkt.sleepForever
128
import me.devnatan.dockerkt.withContainer
139
import kotlin.test.Test
@@ -24,13 +20,11 @@ class CopyContainerArchivesIT : ResourceIT() {
2420
testClient.withContainer(
2521
testImage,
2622
{
27-
command = listOf("sh", "-c", "echo 'test content' > /tmp/test.txt && sleep infinity")
23+
command = listOf("sh", "-c", "echo 'test content' > /tmp/test.txt")
2824
},
2925
) { id ->
3026
testClient.containers.start(id)
31-
32-
// Wait for file to be created
33-
delay(500)
27+
testClient.containers.wait(id)
3428

3529
val tempDir = FileSystemUtils.createTempDirectory()
3630
try {
@@ -50,7 +44,6 @@ class CopyContainerArchivesIT : ResourceIT() {
5044
)
5145
} finally {
5246
FileSystemUtils.deleteRecursively(tempDir)
53-
testClient.containers.stop(id)
5447
}
5548
}
5649
}
@@ -76,15 +69,22 @@ class CopyContainerArchivesIT : ResourceIT() {
7669
"/tmp/",
7770
)
7871

79-
val execId =
80-
testClient.exec.create(id) {
81-
command = listOf("cat", "/tmp/${tempFile.name}")
82-
attachStdout = true
83-
}
84-
85-
val result = testClient.exec.start(execId, ExecStartOptions())
86-
assertTrue(result is ExecStartResult.Complete)
87-
assertTrue(result.output.contains("hello from host"))
72+
val verifyDir = FileSystemUtils.createTempDirectory()
73+
try {
74+
testClient.containers.copyFileFrom(
75+
id,
76+
"/tmp/${tempFile.name}",
77+
verifyDir.toString(),
78+
)
79+
val copiedBack = Path(verifyDir, tempFile.name)
80+
assertTrue(FileSystemUtils.exists(copiedBack))
81+
assertEquals(
82+
expected = "hello from host",
83+
actual = FileSystemUtils.readFile(copiedBack).decodeToString(),
84+
)
85+
} finally {
86+
FileSystemUtils.deleteRecursively(verifyDir)
87+
}
8888
} finally {
8989
FileSystemUtils.delete(tempFile)
9090
testClient.containers.stop(id)
@@ -102,13 +102,12 @@ class CopyContainerArchivesIT : ResourceIT() {
102102
listOf(
103103
"sh",
104104
"-c",
105-
"mkdir -p /tmp/testdir && echo 'file1' > /tmp/testdir/file1.txt && echo 'file2' > /tmp/testdir/file2.txt && sleep infinity",
105+
"mkdir -p /tmp/testdir && echo 'file1' > /tmp/testdir/file1.txt && echo 'file2' > /tmp/testdir/file2.txt",
106106
)
107107
},
108108
) { id ->
109109
testClient.containers.start(id)
110-
111-
delay(500)
110+
testClient.containers.wait(id)
112111

113112
val tempDir = FileSystemUtils.createTempDirectory()
114113
try {
@@ -134,7 +133,6 @@ class CopyContainerArchivesIT : ResourceIT() {
134133
)
135134
} finally {
136135
FileSystemUtils.deleteRecursively(tempDir)
137-
testClient.containers.stop(id)
138136
}
139137
}
140138
}
@@ -165,24 +163,19 @@ class CopyContainerArchivesIT : ResourceIT() {
165163
"/tmp/",
166164
)
167165

168-
val execId =
169-
testClient.exec.create(id) {
170-
command = listOf("sh", "-c", "cat /tmp/file1.txt && cat /tmp/file2.txt")
171-
attachStdout = true
172-
}
173-
174-
val result = testClient.exec.start(execId, ExecStartOptions())
175-
assertTrue(result is ExecStartResult.Complete)
176-
177-
val output = result.output
178-
assertTrue(
179-
actual = output.contains("content1"),
180-
message = "Expected 'content1' in output, but got: $output",
181-
)
182-
assertTrue(
183-
actual = output.contains("content2"),
184-
message = "Expected 'content2' in output, but got: $output",
185-
)
166+
val verifyDir = FileSystemUtils.createTempDirectory()
167+
try {
168+
testClient.containers.copyFileFrom(id, "/tmp/file1.txt", verifyDir.toString())
169+
testClient.containers.copyFileFrom(id, "/tmp/file2.txt", verifyDir.toString())
170+
val copiedFile1 = Path(verifyDir, "file1.txt")
171+
val copiedFile2 = Path(verifyDir, "file2.txt")
172+
assertTrue(FileSystemUtils.exists(copiedFile1))
173+
assertTrue(FileSystemUtils.exists(copiedFile2))
174+
assertEquals("content1", FileSystemUtils.readFile(copiedFile1).decodeToString())
175+
assertEquals("content2", FileSystemUtils.readFile(copiedFile2).decodeToString())
176+
} finally {
177+
FileSystemUtils.deleteRecursively(verifyDir)
178+
}
186179
} finally {
187180
FileSystemUtils.deleteRecursively(tempDir)
188181
testClient.containers.stop(id)

src/commonTest/kotlin/me/devnatan/dockerkt/resource/container/LogContainerIT.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,7 @@ class LogContainerIT : ResourceIT() {
257257
},
258258
) { container ->
259259
testClient.containers.start(container)
260-
261-
delay(3000)
260+
testClient.containers.wait(container)
262261

263262
val result =
264263
testClient.containers.logs(container) {

src/nativeMain/kotlin/me/devnatan/dockerkt/io/Http.native.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,27 @@ import io.ktor.client.engine.HttpClientEngineConfig
55
import io.ktor.client.engine.HttpClientEngineFactory
66
import io.ktor.client.engine.cio.CIO
77
import io.ktor.client.plugins.defaultRequest
8+
import io.ktor.client.request.header
9+
import io.ktor.http.HttpHeaders
810
import me.devnatan.dockerkt.DockerClient
911

1012
internal actual val defaultHttpClientEngine: HttpClientEngineFactory<*>? get() = CIO
1113

1214
internal actual fun <T : HttpClientEngineConfig> HttpClientConfig<out T>.configureHttpClient(client: DockerClient) {
15+
engine {
16+
require(this is io.ktor.client.engine.cio.CIOEngineConfig) { "Only CIO engine is supported for now" }
17+
// disable request timeout so long-running calls (image pulls, log streams) aren't killed
18+
requestTimeout = 0
19+
}
1320
defaultRequest {
1421
val socketPath = client.config.socketPath
1522
if (isUnixSocket(socketPath)) {
16-
unixSocket(socketPath)
23+
unixSocket(socketPath.removePrefix(UnixSocketPrefix))
1724
}
25+
// Force Connection: close. Docker often returns bodies framed by connection-close (no
26+
// Content-Length, no Transfer-Encoding), and Ktor CIO over unix sockets cannot otherwise
27+
// determine where the response ends — throwing "request body length should be specified,
28+
// chunked transfer encoding should be used or keep-alive should be disabled (connection: close)".
29+
header(HttpHeaders.Connection, "close")
1830
}
1931
}

0 commit comments

Comments
 (0)