Skip to content

Commit 3d66c26

Browse files
Bump minimum Swift to 5.9, adopt StrictConcurrency=complete, add GitHub Actions CI (#15)
### Motivation Although this package is still WIP and has no releases, it would still be good for it to line up with the rest of the ecosystem with its supported Swift versions, adoption of strict concurrency, and its CI support. ### Modifications - Bump minimum Swift version to 5.9. - Add `StrictConcurrency=complete` flag and fix test to remove warning. - Add the standard set of GitHub Actions workflows. - Format the code to satisfy the license and format checks. ### Result: Ready for strict concurrency and has CI.
1 parent 0aa2aea commit 3d66c26

File tree

13 files changed

+211
-301
lines changed

13 files changed

+211
-301
lines changed

.github/release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
changelog:
2+
categories:
3+
- title: SemVer Major
4+
labels:
5+
- ⚠️ semver/major
6+
- title: SemVer Minor
7+
labels:
8+
- semver/minor
9+
- title: SemVer Patch
10+
labels:
11+
- semver/patch
12+
- title: Other Changes
13+
labels:
14+
- semver/none

.github/workflows/main.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Main
2+
3+
on:
4+
push:
5+
branches: [main]
6+
schedule:
7+
- cron: "0 8,20 * * *"
8+
9+
jobs:
10+
unit-tests:
11+
name: Unit tests
12+
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
13+
with:
14+
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
15+
linux_5_10_arguments_override: --explicit-target-dependency-import-check error"
16+
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
17+
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
18+
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"

.github/workflows/pull_request.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: PR
2+
3+
on:
4+
pull_request:
5+
types: [opened, reopened, synchronize]
6+
7+
jobs:
8+
soundness:
9+
name: Soundness
10+
uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main
11+
with:
12+
license_header_check_project_name: "swift-etcd-client-gsoc"
13+
14+
unit-tests:
15+
name: Unit tests
16+
uses: apple/swift-nio/.github/workflows/unit_tests.yml@main
17+
with:
18+
linux_5_9_arguments_override: "--explicit-target-dependency-import-check error"
19+
linux_5_10_arguments_override: "--explicit-target-dependency-import-check error"
20+
linux_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
21+
linux_nightly_6_0_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
22+
linux_nightly_main_arguments_override: "--explicit-target-dependency-import-check error -Xswiftc -require-explicit-sendable"
23+
24+
# Integration tests use a running etcd so this job uses a service container.
25+
integration-tests:
26+
name: Integration tests
27+
runs-on: ubuntu-latest
28+
services:
29+
etcd:
30+
image: quay.io/coreos/etcd:v3.5.6
31+
env:
32+
ETCD_ADVERTISE_CLIENT_URLS: http://0.0.0.0:2379
33+
ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379
34+
ETCD_INITIAL_CLUSTER_STATE: new
35+
ports:
36+
- 2379:2379
37+
container:
38+
image: swift:6.0-noble
39+
steps:
40+
- name: Checkout repository
41+
uses: actions/checkout@v4
42+
with:
43+
persist-credentials: false
44+
- name: Build package
45+
run: swift build --build-tests
46+
- name: Run integration tests
47+
shell: bash # explicitly choose bash, which ensures -o pipefail
48+
run: swift test --filter "IntegrationTests" | tee test.out
49+
env:
50+
SWIFT_ETCD_CLIENT_INTEGRATION_TEST_ENABLED: true
51+
ETCD_HOST: etcd
52+
ETCD_PORT: 2379
53+
- name: Check integration tests actually did run
54+
run: test -r test.out && ! grep -i -e "executed 0 tests" -e "skipped" test.out
55+
56+
cxx-interop:
57+
name: Cxx interop
58+
uses: apple/swift-nio/.github/workflows/cxx_interop.yml@main
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: PR label
2+
3+
on:
4+
pull_request:
5+
types: [labeled, unlabeled, opened, reopened, synchronize]
6+
7+
jobs:
8+
semver-label-check:
9+
name: Semantic version label check
10+
runs-on: ubuntu-latest
11+
timeout-minutes: 1
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
with:
16+
persist-credentials: false
17+
- name: Check for Semantic Version label
18+
uses: apple/swift-nio/.github/actions/pull_request_semver_label_checker@main

.licenseignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.gitignore
2+
**/.gitignore
3+
.licenseignore
4+
.gitattributes
5+
.git-blame-ignore-revs
6+
.mailfilter
7+
.mailmap
8+
.spi.yml
9+
.swiftformat
10+
.swift-format
11+
.swiftformatignore
12+
.editorconfig
13+
.github/*
14+
*.md
15+
*.txt
16+
*.yml
17+
*.yaml
18+
*.json
19+
*.proto
20+
*.pb.swift
21+
*.grpc.swift
22+
Package.swift
23+
**/Package.swift
24+
Package@-*.swift
25+
**/Package@-*.swift
26+
Package.resolved
27+
**/Package.resolved
28+
Makefile
29+
*.modulemap
30+
**/*.modulemap
31+
**/*.docc/*
32+
*.xcprivacy
33+
**/*.xcprivacy
34+
*.symlink
35+
**/*.symlink
36+
Dockerfile
37+
**/Dockerfile
38+
Snippets/*
39+
dev/git.commit.template
40+
.unacceptablelanguageignore

Package.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version: 5.7
1+
// swift-tools-version: 5.9
22
//===----------------------------------------------------------------------===//
33
//
44
// This source file is part of the swift-etcd-client-gsoc open source project
@@ -27,7 +27,7 @@ let package = Package(
2727
.library(
2828
name: "ETCD",
2929
targets: ["ETCD"]
30-
),
30+
)
3131
],
3232
dependencies: [
3333
.package(url: "https://github.com/apple/swift-nio.git", from: "2.56.0"),
@@ -59,3 +59,9 @@ let package = Package(
5959
),
6060
]
6161
)
62+
63+
for target in package.targets {
64+
var settings = target.swiftSettings ?? []
65+
settings.append(.enableExperimentalFeature("StrictConcurrency=complete"))
66+
target.swiftSettings = settings
67+
}

Sources/ETCD/DeleteRangeRequest.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ public struct DeleteRangeRequest {
1919
public var key: Data
2020
public var rangeEnd: Data?
2121
public var prevKV: Bool = false
22-
22+
2323
init(protoDeleteRangeRequest: Etcdserverpb_DeleteRangeRequest) {
2424
self.key = protoDeleteRangeRequest.key
2525
self.rangeEnd = protoDeleteRangeRequest.rangeEnd.isEmpty ? nil : protoDeleteRangeRequest.rangeEnd
2626
self.prevKV = protoDeleteRangeRequest.prevKv
2727
}
28-
28+
2929
/// Struct representing a deleteRangeRequest in etcd.
3030
///
3131
/// - Parameters:
@@ -37,7 +37,7 @@ public struct DeleteRangeRequest {
3737
self.rangeEnd = rangeEnd
3838
self.prevKV = prevKV
3939
}
40-
40+
4141
func toProto() -> Etcdserverpb_DeleteRangeRequest {
4242
var protoDeleteRangeRequest = Etcdserverpb_DeleteRangeRequest()
4343
protoDeleteRangeRequest.key = self.key

Sources/ETCD/ETCDClient.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ public final class EtcdClient: @unchecked Sendable {
110110
/// - Parameters:
111111
/// - key: The key for which the value is watched. Parameter is of type Sequence<UInt8>.
112112
/// - operation: The operation to be run on the WatchAsyncSequence for the key.
113-
public func watch<Result>(_ key: some Sequence<UInt8>, _ operation: (WatchAsyncSequence) async throws -> Result) async throws -> Result {
113+
public func watch<Result>(
114+
_ key: some Sequence<UInt8>,
115+
_ operation: (WatchAsyncSequence) async throws -> Result
116+
) async throws -> Result {
114117
let request = [Etcdserverpb_WatchRequest.with { $0.createRequest.key = Data(key) }]
115118
let watchAsyncSequence = WatchAsyncSequence(grpcAsyncSequence: watchClient.watch(request))
116119
return try await operation(watchAsyncSequence)
@@ -121,7 +124,10 @@ public final class EtcdClient: @unchecked Sendable {
121124
/// - Parameters:
122125
/// - key: The key for which the value is watched. Parameter is of type String.
123126
/// - operation: The operation to be run on the WatchAsyncSequence for the key.
124-
public func watch<Result>(_ key: String, _ operation: (WatchAsyncSequence) async throws -> Result) async throws -> Result {
127+
public func watch<Result>(
128+
_ key: String,
129+
_ operation: (WatchAsyncSequence) async throws -> Result
130+
) async throws -> Result {
125131
try await watch(key.utf8, operation)
126132
}
127133
}

Sources/ETCDExample/ETCDExample.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Dispatch
2-
31
//===----------------------------------------------------------------------===//
42
//
53
// This source file is part of the swift-etcd-client-gsoc open source project
@@ -13,6 +11,8 @@ import Dispatch
1311
// SPDX-License-Identifier: Apache-2.0
1412
//
1513
//===----------------------------------------------------------------------===//
14+
15+
import Dispatch
1616
import ETCD
1717
import NIO
1818

Tests/ETCDTests/ETCDTests.swift renamed to Tests/IntegrationTests/IntegrationTests.swift

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import ETCD
2-
import NIO
3-
41
//===----------------------------------------------------------------------===//
52
//
63
// This source file is part of the swift-etcd-client-gsoc open source project
@@ -14,18 +11,22 @@ import NIO
1411
// SPDX-License-Identifier: Apache-2.0
1512
//
1613
//===----------------------------------------------------------------------===//
14+
15+
import ETCD
16+
import NIO
1717
import XCTest
1818

19-
final class EtcdClientTests: XCTestCase {
20-
var eventLoopGroup: EventLoopGroup!
21-
var etcdClient: EtcdClient!
19+
final class IntegrationTests: XCTestCase {
20+
21+
private static let integrationTestEnabled = getBoolEnv("SWIFT_ETCD_CLIENT_INTEGRATION_TEST_ENABLED") ?? false
2222

2323
override func setUp() async throws {
24-
eventLoopGroup = MultiThreadedEventLoopGroup.singleton
25-
etcdClient = EtcdClient(host: "localhost", port: 2379, eventLoopGroup: eventLoopGroup)
24+
try XCTSkipUnless(Self.integrationTestEnabled)
2625
}
2726

2827
func testSetAndGetStringValue() async throws {
28+
let etcdClient = EtcdClient.testClient
29+
2930
try await etcdClient.set("testKey", value: "testValue")
3031
let key = "testKey".data(using: .utf8)!
3132
let rangeRequest = RangeRequest(key: key)
@@ -36,13 +37,17 @@ final class EtcdClientTests: XCTestCase {
3637
}
3738

3839
func testGetNonExistentKey() async throws {
40+
let etcdClient = EtcdClient.testClient
41+
3942
let key = "nonExistentKey".data(using: .utf8)!
4043
let rangeRequest = RangeRequest(key: key)
4144
let result = try await etcdClient.getRange(rangeRequest)
4245
XCTAssertNil(result)
4346
}
4447

4548
func testDeleteKeyExists() async throws {
49+
let etcdClient = EtcdClient.testClient
50+
4651
let key = "testKey"
4752
let value = "testValue"
4853
try await etcdClient.set(key, value: value)
@@ -54,12 +59,14 @@ final class EtcdClientTests: XCTestCase {
5459

5560
let deleteRangeRequest = DeleteRangeRequest(key: rangeRequestKey)
5661
try await etcdClient.deleteRange(deleteRangeRequest)
57-
62+
5863
fetchedValue = try await etcdClient.getRange(rangeRequest)
5964
XCTAssertNil(fetchedValue)
6065
}
6166

6267
func testDeleteNonExistentKey() async throws {
68+
let etcdClient = EtcdClient.testClient
69+
6370
let key = "testKey".data(using: .utf8)!
6471
let rangeRequest = RangeRequest(key: key)
6572

@@ -68,12 +75,14 @@ final class EtcdClientTests: XCTestCase {
6875

6976
let deleteRangeRequest = DeleteRangeRequest(key: key)
7077
try await etcdClient.deleteRange(deleteRangeRequest)
71-
78+
7279
fetchedValue = try await etcdClient.getRange(rangeRequest)
7380
XCTAssertNil(fetchedValue)
7481
}
7582

7683
func testUpdateExistingKey() async throws {
84+
let etcdClient = EtcdClient.testClient
85+
7786
let key = "testKey"
7887
let value = "testValue"
7988
try await etcdClient.set(key, value: value)
@@ -95,14 +104,16 @@ final class EtcdClientTests: XCTestCase {
95104
}
96105

97106
func testWatch() async throws {
107+
let etcdClient = EtcdClient.testClient
108+
98109
let key = "testKey"
99110
let value = "testValue".data(using: .utf8)!
100111

101112
try await etcdClient.put(key, value: "foo")
102113

103114
try await withThrowingTaskGroup(of: Void.self) { group in
104115
group.addTask {
105-
try await self.etcdClient.watch(key) { watchAsyncSequence in
116+
try await etcdClient.watch(key) { watchAsyncSequence in
106117
var iterator = watchAsyncSequence.makeAsyncIterator()
107118
let events = try await iterator.next()
108119
guard let events = events else {
@@ -122,8 +133,27 @@ final class EtcdClientTests: XCTestCase {
122133
}
123134

124135
try await Task.sleep(nanoseconds: 1_000_000_000)
125-
try await self.etcdClient.put(key, value: String(data: value, encoding: .utf8)!)
136+
try await etcdClient.put(key, value: String(data: value, encoding: .utf8)!)
126137
group.cancelAll()
127138
}
128139
}
129140
}
141+
142+
extension EtcdClient {
143+
fileprivate static let testClient = EtcdClient(
144+
host: ProcessInfo.processInfo.environment["ETCD_HOST"] ?? "localhost",
145+
port: getIntEnv("ETCD_PORT") ?? 2379,
146+
eventLoopGroup: .singletonMultiThreadedEventLoopGroup
147+
)
148+
}
149+
150+
/// Returns true if `key` is a truthy string, otherwise returns false.
151+
private func getBoolEnv(_ key: String) -> Bool? {
152+
switch ProcessInfo.processInfo.environment[key]?.lowercased() {
153+
case .none: return nil
154+
case "true", "y", "yes", "on", "1": return true
155+
default: return false
156+
}
157+
}
158+
159+
private func getIntEnv(_ key: String) -> Int? { ProcessInfo.processInfo.environment[key].flatMap(Int.init(_:)) }

0 commit comments

Comments
 (0)