feat: Fixes some tests and docker builds

This commit is contained in:
2024-11-14 14:58:09 -05:00
parent e7a849b003
commit 163f603b69
9 changed files with 176 additions and 282 deletions

View File

@@ -5,10 +5,10 @@ WORKDIR /build
COPY ./Package.* ./ COPY ./Package.* ./
RUN swift package resolve RUN swift package resolve
COPY . . COPY . .
RUN swift build --enable-test-discovery -c release -Xswiftc -g RUN swift build -c release -Xswiftc -g
# Run image # Run image
FROM swift:5.10-slim FROM swift:5.10-slim
WORKDIR /run WORKDIR /run
COPY --from=build /build/.build/release/dewPoint-controller /run COPY --from=build /build/.build/release/dewpoint-controller /run
CMD ["/bin/bash", "-xc", "./dewpoint-controller"] CMD ["/bin/bash", "-xc", "./dewpoint-controller"]

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "d3104d51323f6bc98cf3ab2930e5a26f72c9a4fdb7640360ba27628672397841", "originHash" : "c2538e3229d6c80f3d6a979c2f3605d63b4973e1e49786819017473eb2916f4e",
"pins" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",
@@ -69,8 +69,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-dependencies", "location" : "https://github.com/pointfreeco/swift-dependencies",
"state" : { "state" : {
"revision" : "0fc0255e780bf742abeef29dec80924f5f0ae7b9", "revision" : "96eecd47660e8307877acb8c41cc5295ba7350a7",
"version" : "1.4.1" "version" : "1.5.2"
} }
}, },
{ {

View File

@@ -24,7 +24,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"), .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"), .package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"), .package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.4.1"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
.package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"), .package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"),
.package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"), .package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"),
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0") .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0")
@@ -82,7 +82,7 @@ let package = Package(
name: "SensorsService", name: "SensorsService",
dependencies: [ dependencies: [
"Models", "Models",
"MQTTConnectionService", "MQTTConnectionManager",
"TopicDependencies", "TopicDependencies",
.product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "DependenciesMacros", package: "swift-dependencies"), .product(name: "DependenciesMacros", package: "swift-dependencies"),

View File

@@ -33,6 +33,17 @@ public struct MQTTConnectionManager: Sendable {
/// Create a stream of connection events. /// Create a stream of connection events.
public var stream: @Sendable () throws -> AsyncStream<Event> public var stream: @Sendable () throws -> AsyncStream<Event>
/// Perform an operation with the underlying MQTTClient, this can be useful in
/// tests, so this module needs imported with `@_spi(Testing) import` to use this method.
private var _withClient: @Sendable ((MQTTClient) async throws -> Void) async throws -> Void
@_spi(Internal)
public func withClient(
_ callback: @Sendable (MQTTClient) async throws -> Void
) async throws {
try await _withClient(callback)
}
/// Represents connection events that clients can listen for and /// Represents connection events that clients can listen for and
/// react accordingly. /// react accordingly.
public enum Event: Sendable { public enum Event: Sendable {
@@ -61,6 +72,8 @@ public struct MQTTConnectionManager: Sendable {
.start() .start()
.removeDuplicates() .removeDuplicates()
.eraseToStream() .eraseToStream()
} _withClient: { callback in
try await callback(client)
} }
} }
} }
@@ -73,18 +86,19 @@ extension MQTTConnectionManager: TestDependencyKey {
// MARK: - Helpers // MARK: - Helpers
final class MQTTConnectionStream: AsyncSequence, Sendable { @_spi(Internal)
public final actor MQTTConnectionStream: Sendable {
typealias AsyncIterator = AsyncStream<Element>.AsyncIterator public typealias Element = MQTTConnectionManager.Event
typealias Element = MQTTConnectionManager.Event
private let client: MQTTClient private let client: MQTTClient
private let continuation: AsyncStream<Element>.Continuation private let continuation: AsyncStream<Element>.Continuation
private let logger: Logger? private let logger: Logger?
private let name: String private let name: String
private let stream: AsyncStream<Element> private let stream: AsyncStream<Element>
private var isShuttingDown = false
init(client: MQTTClient, logger: Logger?) { public init(client: MQTTClient, logger: Logger?) {
let (stream, continuation) = AsyncStream<Element>.makeStream() let (stream, continuation) = AsyncStream<Element>.makeStream()
self.client = client self.client = client
self.continuation = continuation self.continuation = continuation
@@ -95,12 +109,19 @@ final class MQTTConnectionStream: AsyncSequence, Sendable {
deinit { stop() } deinit { stop() }
func start( public nonisolated func start() -> AsyncStream<Element> {
isolation: isolated (any Actor)? = #isolation // Check if the client is active and yield the initial result.
) -> AsyncStream<Element> {
// Check if the client is active and yield the result.
continuation.yield(client.isActive() ? .connected : .disconnected) continuation.yield(client.isActive() ? .connected : .disconnected)
// Continually check if the client is active.
let task = Task {
let isShuttingDown = await self.isShuttingDown
while !Task.isCancelled, !isShuttingDown {
try await Task.sleep(for: .milliseconds(100))
continuation.yield(client.isActive() ? .connected : .disconnected)
}
}
// Register listener on the client for when the connection // Register listener on the client for when the connection
// closes. // closes.
client.addCloseListener(named: name) { _ in client.addCloseListener(named: name) { _ in
@@ -111,24 +132,26 @@ final class MQTTConnectionStream: AsyncSequence, Sendable {
// Register listener on the client for when the client // Register listener on the client for when the client
// is shutdown. // is shutdown.
client.addShutdownListener(named: name) { _ in client.addShutdownListener(named: name) { _ in
self.logger?.trace("Client is shutting down.") self.logger?.trace("Client is shutting down, ending connection stream: \(self.name)")
self.continuation.yield(.shuttingDown) self.continuation.yield(.shuttingDown)
Task { await self.setIsShuttingDown() }
task.cancel()
self.stop() self.stop()
} }
return stream return stream
} }
func stop() { private func setIsShuttingDown() {
isShuttingDown = true
}
public nonisolated func stop() {
client.removeCloseListener(named: name) client.removeCloseListener(named: name)
client.removeShutdownListener(named: name) client.removeShutdownListener(named: name)
continuation.finish() continuation.finish()
} }
public __consuming func makeAsyncIterator() -> AsyncIterator {
start().makeAsyncIterator()
}
} }
actor ConnectionManager { actor ConnectionManager {
@@ -160,13 +183,12 @@ actor ConnectionManager {
} }
func connect( func connect(
isolation: isolated (any Actor)? = #isolation,
cleanSession: Bool cleanSession: Bool
) async throws { ) async throws {
guard !(await hasConnected) else { return } guard !hasConnected else { return }
do { do {
try await client.connect(cleanSession: cleanSession) try await client.connect(cleanSession: cleanSession)
await setHasConnected() setHasConnected()
client.addCloseListener(named: name) { [weak self] _ in client.addCloseListener(named: name) { [weak self] _ in
guard let `self` else { return } guard let `self` else { return }

View File

@@ -3,6 +3,7 @@ import DependenciesMacros
import Foundation import Foundation
import Logging import Logging
import Models import Models
import MQTTConnectionManager
import MQTTNIO import MQTTNIO
import NIO import NIO
import PsychrometricClient import PsychrometricClient
@@ -16,6 +17,7 @@ import TopicDependencies
/// ///
public actor SensorsService: Service { public actor SensorsService: Service {
@Dependency(\.mqttConnectionManager.stream) var connectionStream
@Dependency(\.topicListener) var topicListener @Dependency(\.topicListener) var topicListener
@Dependency(\.topicPublisher) var topicPublisher @Dependency(\.topicPublisher) var topicPublisher
@@ -55,25 +57,41 @@ public actor SensorsService: Service {
public func run() async throws { public func run() async throws {
precondition(sensors.count > 0, "Sensors should not be empty.") precondition(sensors.count > 0, "Sensors should not be empty.")
try await withGracefulShutdownHandler {
// Listen for connection events, so that we can automatically
// reconnect any sensor topics we're listening to upon a disconnect / reconnect
// event. We can also shutdown any topic listeners upon a shutdown event.
for await event in try connectionStream().cancelOnGracefulShutdown() {
switch event {
case .shuttingDown:
logger?.debug("Received shutdown event.")
try await self.shutdown()
case .disconnected:
logger?.debug("Received disconnected event.")
try await Task.sleep(for: .milliseconds(100))
case .connected:
logger?.debug("Received connected event.")
let stream = try await makeStream() let stream = try await makeStream()
await withGracefulShutdownHandler {
await withDiscardingTaskGroup { group in
for await result in stream.cancelOnGracefulShutdown() { for await result in stream.cancelOnGracefulShutdown() {
logger?.trace("Received result for topic: \(result.topic)") logger?.debug("Received result for topic: \(result.topic)")
group.addTask { await self.handleResult(result) } await self.handleResult(result)
}
} }
// group.cancelAll()
} }
} onGracefulShutdown: { } onGracefulShutdown: {
Task { Task {
self.logger?.trace("Received graceful shutdown.") self.logger?.debug("Received graceful shutdown.")
try? await self.publishUpdates() try await self.shutdown()
await self.topicListener.shutdown()
} }
} }
} }
@_spi(Internal)
public func shutdown() async throws {
try await publishUpdates()
topicListener.shutdown()
}
private func makeStream() async throws -> AsyncStream<(buffer: ByteBuffer, topic: String)> { private func makeStream() async throws -> AsyncStream<(buffer: ByteBuffer, topic: String)> {
try await topicListener.listen(to: topics) try await topicListener.listen(to: topics)
// ignore errors, so that we continue to listen, but log them // ignore errors, so that we continue to listen, but log them
@@ -81,7 +99,7 @@ public actor SensorsService: Service {
.compactMap { result in .compactMap { result in
switch result { switch result {
case let .failure(error): case let .failure(error):
self.logger?.trace("Received error listening for sensors: \(error)") self.logger?.debug("Received error listening for sensors: \(error)")
return nil return nil
case let .success(info): case let .success(info):
return (info.payload, info.topicName) return (info.payload, info.topicName)
@@ -100,7 +118,7 @@ public actor SensorsService: Service {
do { do {
let topic = result.topic let topic = result.topic
assert(topics.contains(topic)) assert(topics.contains(topic))
logger?.trace("Begin handling result for topic: \(topic)") logger?.debug("Begin handling result for topic: \(topic)")
func decode<V: BufferInitalizable>(_: V.Type) -> V? { func decode<V: BufferInitalizable>(_: V.Type) -> V? {
var buffer = result.buffer var buffer = result.buffer
@@ -108,28 +126,28 @@ public actor SensorsService: Service {
} }
if topic.contains("temperature") { if topic.contains("temperature") {
logger?.trace("Begin handling temperature result.") logger?.debug("Begin handling temperature result.")
guard let temperature = decode(DryBulb.self) else { guard let temperature = decode(DryBulb.self) else {
logger?.trace("Failed to decode temperature: \(result.buffer)") logger?.debug("Failed to decode temperature: \(result.buffer)")
throw DecodingError() throw DecodingError()
} }
logger?.trace("Decoded temperature: \(temperature)") logger?.debug("Decoded temperature: \(temperature)")
try sensors.update(topic: topic, keyPath: \.temperature, with: temperature) try sensors.update(topic: topic, keyPath: \.temperature, with: temperature)
} else if topic.contains("humidity") { } else if topic.contains("humidity") {
logger?.trace("Begin handling humidity result.") logger?.debug("Begin handling humidity result.")
guard let humidity = decode(RelativeHumidity.self) else { guard let humidity = decode(RelativeHumidity.self) else {
logger?.trace("Failed to decode humidity: \(result.buffer)") logger?.debug("Failed to decode humidity: \(result.buffer)")
throw DecodingError() throw DecodingError()
} }
logger?.trace("Decoded humidity: \(humidity)") logger?.debug("Decoded humidity: \(humidity)")
try sensors.update(topic: topic, keyPath: \.humidity, with: humidity) try sensors.update(topic: topic, keyPath: \.humidity, with: humidity)
} }
try await publishUpdates() try await publishUpdates()
logger?.trace("Done handling result for topic: \(topic)") logger?.debug("Done handling result for topic: \(topic)")
} catch { } catch {
logger?.error("Received error: \(error)") logger?.error("Received error while handling result: \(error)")
} }
} }
@@ -141,7 +159,7 @@ public actor SensorsService: Service {
qos: .exactlyOnce, qos: .exactlyOnce,
retain: true retain: true
) )
logger?.trace("Published update to topic: \(topic)") logger?.debug("Published update to topic: \(topic)")
} }
private func publishUpdates() async throws { private func publishUpdates() async throws {

View File

@@ -10,7 +10,7 @@ import MQTTNIO
@DependencyClient @DependencyClient
public struct TopicListener: Sendable { public struct TopicListener: Sendable {
public typealias Stream = AsyncStream<Result<MQTTPublishInfo, MQTTListenResultError>> public typealias Stream = AsyncStream<Result<MQTTPublishInfo, any Error>>
/// Create an async stream that listens for changes to the given topics. /// Create an async stream that listens for changes to the given topics.
private var _listen: @Sendable ([String], MQTTQoS) async throws -> Stream private var _listen: @Sendable ([String], MQTTQoS) async throws -> Stream
@@ -82,6 +82,7 @@ public extension DependencyValues {
// MARK: - Helpers // MARK: - Helpers
private actor MQTTTopicListener { private actor MQTTTopicListener {
private let client: MQTTClient private let client: MQTTClient
private let continuation: TopicListener.Stream.Continuation private let continuation: TopicListener.Stream.Continuation
private let name: String private let name: String
@@ -116,12 +117,12 @@ private actor MQTTTopicListener {
func listen( func listen(
_ topics: [String], _ topics: [String],
_ qos: MQTTQoS = .atLeastOnce _ qos: MQTTQoS = .atLeastOnce
) async throws(TopicListenerError) -> TopicListener.Stream { ) async throws -> TopicListener.Stream {
var sleepTimes = 0 var sleepTimes = 0
while !client.isActive() { while !client.isActive() {
guard sleepTimes < 10 else { guard sleepTimes < 10 else {
throw .connectionTimeout throw TopicListenerError.connectionTimeout
} }
try? await Task.sleep(for: .milliseconds(100)) try? await Task.sleep(for: .milliseconds(100))
sleepTimes += 1 sleepTimes += 1
@@ -135,7 +136,7 @@ private actor MQTTTopicListener {
guard subscription != nil else { guard subscription != nil else {
client.logger.error("Error subscribing to topics: \(topics)") client.logger.error("Error subscribing to topics: \(topics)")
throw .failedToSubscribe throw TopicListenerError.failedToSubscribe
} }
client.logger.trace("Done subscribing, begin listening to topics.") client.logger.trace("Done subscribing, begin listening to topics.")
@@ -144,7 +145,7 @@ private actor MQTTTopicListener {
switch result { switch result {
case let .failure(error): case let .failure(error):
self.client.logger.error("Received error while listening: \(error)") self.client.logger.error("Received error while listening: \(error)")
self.continuation.yield(.failure(.init(error))) self.continuation.yield(.failure(MQTTListenResultError(error)))
case let .success(publishInfo): case let .success(publishInfo):
if topics.contains(publishInfo.topicName) { if topics.contains(publishInfo.topicName) {
self.client.logger.trace("Recieved new value for topic: \(publishInfo.topicName)") self.client.logger.trace("Recieved new value for topic: \(publishInfo.topicName)")
@@ -169,6 +170,8 @@ private actor MQTTTopicListener {
} }
} }
// MARK: - Errors
public enum TopicListenerError: Error { public enum TopicListenerError: Error {
case connectionTimeout case connectionTimeout
case failedToSubscribe case failedToSubscribe

View File

@@ -1,6 +1,7 @@
import AsyncAlgorithms
import Logging import Logging
import Models import Models
@testable import MQTTConnectionManager @_spi(Internal) import MQTTConnectionManager
import MQTTConnectionService import MQTTConnectionService
import MQTTNIO import MQTTNIO
import NIO import NIO
@@ -18,23 +19,6 @@ final class MQTTConnectionServiceTests: XCTestCase {
return logger return logger
}() }()
// func testGracefulShutdownWorks() async throws {
// let client = createClient(identifier: "testGracefulShutdown")
// let service = MQTTConnectionService(client: client)
// await service.connect()
// try await Task.sleep(for: .seconds(1))
// XCTAssert(client.isActive())
// service.shutdown()
// XCTAssertFalse(client.isActive())
// }
func testWhatHappensIfConnectIsCalledMultipleTimes() async throws {
let client = createClient(identifier: "testWhatHappensIfConnectIsCalledMultipleTimes")
let manager = MQTTConnectionManager.live(client: client)
try await manager.connect()
try await manager.connect()
}
// TODO: Move to integration tests. // TODO: Move to integration tests.
func testMQTTConnectionStream() async throws { func testMQTTConnectionStream() async throws {
let client = createClient(identifier: "testNonManagedStream") let client = createClient(identifier: "testNonManagedStream")
@@ -43,8 +27,13 @@ final class MQTTConnectionServiceTests: XCTestCase {
logger: Self.logger, logger: Self.logger,
alwaysReconnect: false alwaysReconnect: false
) )
let stream = MQTTConnectionStream(client: client, logger: Self.logger) let connectionStream1 = MQTTConnectionStream(client: client, logger: Self.logger)
var events = [MQTTConnectionManager.Event]() let connectionStream2 = MQTTConnectionStream(client: client, logger: Self.logger)
var events1 = [MQTTConnectionManager.Event]()
var events2 = [MQTTConnectionManager.Event]()
let stream1 = connectionStream1.start()
let stream2 = connectionStream2.start()
_ = try await manager.connect() _ = try await manager.connect()
@@ -55,17 +44,22 @@ final class MQTTConnectionServiceTests: XCTestCase {
try await Task.sleep(for: .milliseconds(200)) try await Task.sleep(for: .milliseconds(200))
manager.shutdown() manager.shutdown()
try await client.disconnect() try await client.disconnect()
try await Task.sleep(for: .milliseconds(200)) try await Task.sleep(for: .seconds(1))
try await client.shutdown() try await client.shutdown()
try await Task.sleep(for: .milliseconds(200)) try await Task.sleep(for: .seconds(1))
stream.stop() connectionStream1.stop()
connectionStream2.stop()
} }
for await event in stream.removeDuplicates() { for await event in stream1.removeDuplicates() {
events.append(event) events1.append(event)
}
for await event in stream2.removeDuplicates() {
events2.append(event)
} }
XCTAssertEqual(events, [.disconnected, .connected, .disconnected, .shuttingDown]) XCTAssertEqual(events1, [.disconnected, .connected, .disconnected, .shuttingDown])
XCTAssertEqual(events2, [.disconnected, .connected, .disconnected, .shuttingDown])
} }
func createClient(identifier: String) -> MQTTClient { func createClient(identifier: String) -> MQTTClient {

View File

@@ -1,10 +1,11 @@
import Dependencies import Dependencies
import Logging import Logging
import Models import Models
@_spi(Internal) import MQTTConnectionManager
import MQTTNIO import MQTTNIO
import NIO import NIO
import PsychrometricClientLive import PsychrometricClientLive
@testable import SensorsService @_spi(Internal) import SensorsService
import TopicDependencies import TopicDependencies
import XCTest import XCTest
@@ -14,213 +15,73 @@ final class SensorsClientTests: XCTestCase {
static let logger: Logger = { static let logger: Logger = {
var logger = Logger(label: "SensorsClientTests") var logger = Logger(label: "SensorsClientTests")
logger.logLevel = .debug logger.logLevel = .trace
return logger return logger
}() }()
override func invokeTest() { override func invokeTest() {
let client = createClient(identifier: "\(Self.self)")
withDependencies { withDependencies {
$0.mqttConnectionManager = .live(client: client, logger: Self.logger)
$0.psychrometricClient = PsychrometricClient.liveValue $0.psychrometricClient = PsychrometricClient.liveValue
$0.topicListener = .live(client: client)
$0.topicPublisher = .live(client: client)
} operation: { } operation: {
super.invokeTest() super.invokeTest()
} }
} }
func testWhatHappensIfClientDisconnectsWhileListening() async throws { func testListeningResumesAfterDisconnectThenReconnect() async throws {
let client = createClient(identifier: "testWhatHappensIfClientDisconnectsWhileListening") @Dependency(\.mqttConnectionManager) var manager
let listener = TopicListener.live(client: client) struct TimeoutError: Error {}
let sensor = TemperatureAndHumiditySensor(location: .return)
var results = [TopicPublisher.PublishRequest]()
try await withDependencies {
$0.topicPublisher = .capturing { results.append($0) }
} operation: {
let sensorsService = SensorsService(sensors: [sensor], logger: Self.logger)
let task = Task { try await sensorsService.run() }
defer { task.cancel() }
try await manager.connect()
defer { manager.shutdown() }
try await manager.withClient { client in
try await client.disconnect()
try await client.connect() try await client.connect()
try await Task.sleep(for: .milliseconds(100))
let stream = try await listener.listen("/some/topic")
// try await Task.sleep(for: .seconds(1))
// try await client.disconnect()
//
// try await client.connect()
// try await Task.sleep(for: .seconds(1))
try await client.publish( try await client.publish(
to: "/some/topic", to: sensor.topics.temperature,
payload: ByteBufferAllocator().buffer(string: "Foo"), payload: ByteBufferAllocator().buffer(string: "25"),
qos: .atLeastOnce, qos: .atLeastOnce,
retain: true retain: false
)
try await client.publish(
to: sensor.topics.humidity,
payload: ByteBufferAllocator().buffer(string: "50"),
qos: .atLeastOnce,
retain: false
) )
try await Task.sleep(for: .seconds(1))
listener.shutdown()
try await client.shutdown()
} }
// func testConnectAndShutdown() async throws { var timeoutCount = 0
// let client = createClient(identifier: "testConnectAndShutdown") while !(results.count == 2) {
// await client.connect() guard timeoutCount < 20 else {
// await client.shutdown() throw TimeoutError()
// } }
try await Task.sleep(for: .milliseconds(100))
timeoutCount += 1
}
// func testSensorService() async throws { XCTAssertEqual(results.count, 2)
// let mqtt = createClient(identifier: "testSensorService") XCTAssert(results.contains(where: { $0.topicName == sensor.topics.dewPoint }))
// // let mqtt = await client.client XCTAssert(results.contains(where: { $0.topicName == sensor.topics.enthalpy }))
// let sensor = TemperatureAndHumiditySensor(location: .mixedAir) try await sensorsService.shutdown()
// let publishInfo = PublishInfoContainer(topicFilters: [ }
// sensor.topics.dewPoint, }
// sensor.topics.enthalpy
// ])
// let service = SensorsService(client: mqtt, sensors: [sensor])
//
// // fix to connect the mqtt client.
// try await mqtt.connect()
// let task = Task { try await service.run() }
//
// _ = try await mqtt.subscribe(to: [
// MQTTSubscribeInfo(topicFilter: sensor.topics.dewPoint, qos: .exactlyOnce),
// MQTTSubscribeInfo(topicFilter: sensor.topics.enthalpy, qos: .exactlyOnce)
// ])
//
// let listener = mqtt.createPublishListener()
// Task {
// for await result in listener {
// switch result {
// case let .failure(error):
// XCTFail("\(error)")
// case let .success(value):
// await publishInfo.addPublishInfo(value)
// }
// }
// }
//
// try await mqtt.publish(
// to: sensor.topics.temperature,
// payload: ByteBufferAllocator().buffer(string: "75.123"),
// qos: MQTTQoS.exactlyOnce,
// retain: true
// )
//
// try await Task.sleep(for: .seconds(1))
//
// // XCTAssert(client.sensors.first!.needsProcessed)
// // let firstSensor = await client.sensors.first!
// // XCTAssertEqual(firstSensor.temperature, .init(75.123, units: .celsius))
//
// try await mqtt.publish(
// to: sensor.topics.humidity,
// payload: ByteBufferAllocator().buffer(string: "50"),
// qos: MQTTQoS.exactlyOnce,
// retain: true
// )
//
// try await Task.sleep(for: .seconds(1))
//
// // not working for some reason
// // XCTAssertEqual(publishInfo.info.count, 2)
//
// XCTAssert(publishInfo.info.count > 1)
//
// // fix to shutdown the mqtt client.
// task.cancel()
// try await mqtt.shutdown()
// }
// func testCapturingSensorClient() async throws {
// class CapturedValues {
// var values = [(value: Double, topic: String)]()
// var didShutdown = false
//
// init() {}
// }
//
// let capturedValues = CapturedValues()
//
// try await withDependencies {
// $0.sensorsClient = .testing(
// yielding: [
// (value: 76, to: "not-listening"),
// (value: 75, to: "test")
// ]
// ) { value, topic in
// capturedValues.values.append((value, topic))
// } captureShutdownEvent: {
// capturedValues.didShutdown = $0
// }
// } operation: {
// @Dependency(\.sensorsClient) var client
// let stream = try await client.listen(to: ["test"])
//
// for await result in stream {
// var buffer = result.buffer
// guard let double = Double(buffer: &buffer) else {
// XCTFail("Failed to decode double")
// return
// }
//
// XCTAssertEqual(double, 75)
// XCTAssertEqual(result.topic, "test")
// try await client.publish(26, to: "publish")
// try await Task.sleep(for: .milliseconds(100))
// client.shutdown()
// }
//
// XCTAssertEqual(capturedValues.values.count, 1)
// XCTAssertEqual(capturedValues.values.first?.value, 26)
// XCTAssertEqual(capturedValues.values.first?.topic, "publish")
// XCTAssertTrue(capturedValues.didShutdown)
// }
// }
//
// func testSensorCapturesPublishedState() async throws {
// let client = createClient(identifier: "testSensorCapturesPublishedState")
// let mqtt = client.client
// let sensor = TemperatureAndHumiditySensor(location: .mixedAir)
// let publishInfo = PublishInfoContainer(topicFilters: [
// sensor.topics.dewPoint,
// sensor.topics.enthalpy
// ])
//
// try await client.addSensor(sensor)
// await client.connect()
// try await client.start()
//
// _ = try await mqtt.subscribe(to: [
// MQTTSubscribeInfo(topicFilter: sensor.topics.dewPoint, qos: MQTTQoS.exactlyOnce),
// MQTTSubscribeInfo(topicFilter: sensor.topics.enthalpy, qos: MQTTQoS.exactlyOnce)
// ])
//
// let listener = mqtt.createPublishListener()
// Task {
// for await result in listener {
// switch result {
// case let .failure(error):
// XCTFail("\(error)")
// case let .success(value):
// await publishInfo.addPublishInfo(value)
// }
// }
// }
//
// try await mqtt.publish(
// to: sensor.topics.temperature,
// payload: ByteBufferAllocator().buffer(string: "75.123"),
// qos: MQTTQoS.exactlyOnce,
// retain: true
// )
//
// try await Task.sleep(for: .seconds(1))
//
// // XCTAssert(client.sensors.first!.needsProcessed)
// let firstSensor = client.sensors.first!
// XCTAssertEqual(firstSensor.temperature, DryBulb.celsius(75.123))
//
// try await mqtt.publish(
// to: sensor.topics.humidity,
// payload: ByteBufferAllocator().buffer(string: "50"),
// qos: MQTTQoS.exactlyOnce,
// retain: true
// )
//
// try await Task.sleep(for: .seconds(1))
//
// XCTAssertEqual(publishInfo.info.count, 2)
//
// await client.shutdown()
// }
func createClient(identifier: String) -> MQTTClient { func createClient(identifier: String) -> MQTTClient {
let envVars = EnvVars( let envVars = EnvVars(
@@ -252,16 +113,6 @@ final class SensorsClientTests: XCTestCase {
// MARK: Helpers for tests. // MARK: Helpers for tests.
extension AsyncStream {
func first() async -> Element {
var first: Element
for await value in self {
first = value
}
return first
}
}
class PublishInfoContainer { class PublishInfoContainer {
private(set) var info: [MQTTPublishInfo] private(set) var info: [MQTTPublishInfo]
private var topicFilters: [String]? private var topicFilters: [String]?
@@ -282,6 +133,14 @@ class PublishInfoContainer {
} }
} }
extension TopicPublisher {
static func capturing(
_ callback: @escaping (PublishRequest) -> Void
) -> Self {
.init { callback($0) }
}
}
// extension SensorsClient { // extension SensorsClient {
// //
// static func testing( // static func testing(

View File

@@ -10,7 +10,6 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
platform: linux/amd64
depends_on: depends_on:
- mosquitto - mosquitto
environment: environment:
@@ -20,7 +19,6 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile.test dockerfile: Dockerfile.test
platform: linux/amd64
working_dir: /app working_dir: /app
networks: networks:
- test - test