From 24f2ad63a7306ca2726d5be920e0875740278c89 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 18 Nov 2024 15:53:23 -0500 Subject: [PATCH] feat: Moved cli client tests to XCTest to work in docker --- .gitignore | 2 +- .swiftlint.yml | 7 + .../xcschemes/MQTTConnectionService.xcscheme | 67 ------ .../xcschemes/MQTTManager.xcscheme | 67 ------ .../xcshareddata/xcschemes/Models.xcscheme | 67 ------ .../xcschemes/SensorsService.xcscheme | 67 ------ .../dewPoint-controller-Package.xcscheme | 61 +++--- .../xcschemes/dewPoint-controller.xcscheme | 78 ------- Package.swift | 3 +- Sources/CliClient/CliClient.swift | 5 +- Sources/CliClient/EnvironmentDependency.swift | 69 ++++-- Tests/CliClientTests/CliClientTests.swift | 207 +++++++++++------- Tests/CliClientTests/test-env.json | 10 + docker/Dockerfile.test | 3 +- 14 files changed, 227 insertions(+), 486 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/MQTTConnectionService.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/MQTTManager.xcscheme delete mode 100755 .swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/SensorsService.xcscheme delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/dewPoint-controller.xcscheme create mode 100755 Tests/CliClientTests/test-env.json diff --git a/.gitignore b/.gitignore index bb9d983..2836264 100755 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.dewPoint-env +.dewPoint-env* .topics mqtt_password.txt .env diff --git a/.swiftlint.yml b/.swiftlint.yml index e2ccc42..8df1f05 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,2 +1,9 @@ disabled_rules: - closing_brace + - fuction_body_length + +included: + - Sources + - Tests + +ignore_multiline_statement_conditions: true diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MQTTConnectionService.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MQTTConnectionService.xcscheme deleted file mode 100644 index 45f5e1a..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MQTTConnectionService.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/MQTTManager.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/MQTTManager.xcscheme deleted file mode 100644 index a9416cb..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/MQTTManager.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme deleted file mode 100755 index 4435fa7..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Models.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/SensorsService.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/SensorsService.xcscheme deleted file mode 100644 index f4e1101..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/SensorsService.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/dewPoint-controller-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/dewPoint-controller-Package.xcscheme index 4ff0e6e..c67fbf9 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/dewPoint-controller-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/dewPoint-controller-Package.xcscheme @@ -7,6 +7,20 @@ buildImplicitDependencies = "YES" buildArchitectures = "Automatic"> + + + + - - - - - - - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Package.swift b/Package.swift index 373e6ec..ae08c9a 100755 --- a/Package.swift +++ b/Package.swift @@ -48,7 +48,8 @@ let package = Package( "CliClient" ], resources: [ - .copy("test.env") + .copy("test.env"), + .copy("test-env.json") ] ), .executableTarget( diff --git a/Sources/CliClient/CliClient.swift b/Sources/CliClient/CliClient.swift index 5b165fa..50f1b03 100644 --- a/Sources/CliClient/CliClient.swift +++ b/Sources/CliClient/CliClient.swift @@ -67,6 +67,7 @@ public struct CliClient { self.logger = logger self.mqttClientVersion = mqttClientVersion } + } } @@ -121,8 +122,8 @@ extension EnvVars { @Dependency(\.environment) var environment let defaultEnvVars = EnvVars() - let encoder = JSONEncoder() - let decoder = JSONDecoder() + let encoder = environment.jsonEncoder() + let decoder = environment.jsonDecoder() let defaultEnvDict = (try? encoder.encode(defaultEnvVars)) .flatMap { try? decoder.decode([String: String].self, from: $0) } diff --git a/Sources/CliClient/EnvironmentDependency.swift b/Sources/CliClient/EnvironmentDependency.swift index 607d4b8..e985668 100644 --- a/Sources/CliClient/EnvironmentDependency.swift +++ b/Sources/CliClient/EnvironmentDependency.swift @@ -6,6 +6,11 @@ import Models @_spi(Internal) public extension DependencyValues { + + /// A dependecy responsible for loding environment variables. + /// + /// This is just used internally of this module, but is useful to + /// override for testing purposes, so import using `@_spi(Internal)`. var environment: EnvironmentDependency { get { self[EnvironmentDependency.self] } set { self[EnvironmentDependency.self] = newValue } @@ -19,16 +24,27 @@ public extension DependencyValues { @DependencyClient public struct EnvironmentDependency: Sendable { + /// A json decoder to use to decode files and environment variables. + public var jsonDecoder: @Sendable () -> JSONDecoder = { JSONDecoder() } + + /// A json encoder to use to encode files and environment variables. + public var jsonEncoder: @Sendable () -> JSONEncoder = { JSONEncoder() } + /// Load the variables based on the request. public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] } + /// Load the environment variables based on the current process environment. + /// + /// You can override this to use an empty environment, which is useful for testing purposes. public var processInfo: @Sendable () -> [String: String] = { [:] } + /// Represents the types of files that can be loaded and decoded into + /// the environment. public enum FileType: Equatable { case dotEnv(path: String) case json(path: String) - init?(path: String) { + public init?(path: String) { let strings = path.split(separator: ".") guard let ext = strings.last else { return nil @@ -50,22 +66,39 @@ extension EnvironmentDependency: DependencyKey { public static let testValue: EnvironmentDependency = Self() - public static let liveValue: EnvironmentDependency = Self( - load: { file in - switch file { - case let .dotEnv(path: path): - let file = try DotEnv.read(path: path) - return file.lines.reduce(into: [String: String]()) { partialResult, line in - partialResult[line.key] = line.value + public static func live( + decoder: JSONDecoder = .init(), + encoder: JSONEncoder = .init() + ) -> Self { + Self( + jsonDecoder: { decoder }, + jsonEncoder: { encoder }, + load: { file in + switch file { + case let .dotEnv(path: path): + let file = try DotEnv.read(path: path) + return file.lines.reduce(into: [String: String]()) { partialResult, line in + partialResult[line.key] = line.value + } + case let .json(path: path): + let url = url(for: path) + return try decoder.decode( + [String: String].self, + from: Data(contentsOf: url) + ) } - case let .json(path: path): - let url = URL(filePath: path) - return try JSONDecoder().decode( - [String: String].self, - from: Data(contentsOf: url) - ) - } - }, - processInfo: { ProcessInfo.processInfo.environment } - ) + }, + processInfo: { ProcessInfo.processInfo.environment } + ) + } + + public static let liveValue: EnvironmentDependency = .live() +} + +private func url(for path: String) -> URL { + #if os(Linux) + return URL(fileURLWithPath: path) + #else + return URL(filePath: path) + #endif } diff --git a/Tests/CliClientTests/CliClientTests.swift b/Tests/CliClientTests/CliClientTests.swift index c76ebe8..93cf3cf 100644 --- a/Tests/CliClientTests/CliClientTests.swift +++ b/Tests/CliClientTests/CliClientTests.swift @@ -4,96 +4,139 @@ import Foundation import Logging import Models import MQTTNIO -import Testing +import XCTest -@Test -func checkTesting() { - #expect(Bool(true)) -} +final class CliClientTests: XCTestCase { -@Test( - arguments: [ - (MQTTClient.Version.v3_1_1, ["3", "3.1", "3.1.1", "00367894"]), - (MQTTClient.Version.v5_0, ["5", "5.1", "5.1.1", "00000500012"]), - (nil, ["0", "0.1", "0.1.1", "0000000001267894", "blob"]) - ] -) -func checkParseMQTTVersion( - version: MQTTClient.Version?, - strings: [String] -) { - withDependencies { - $0.cliClient = .liveValue - } operation: { - @Dependency(\.cliClient) var cliClient - for string in strings { - #expect(cliClient.parseMqttClientVersion(string) == version) - #expect(cliClient.parseMqttClientVersion("v\(string)") == version) + override func invokeTest() { + withDependencies { + $0.cliClient = .liveValue + $0.environment = .liveValue + $0.environment.processInfo = { [:] } + } operation: { + super.invokeTest() } } -} -@Test( - arguments: [ - (Logger.Level.debug, EnvVars(appEnv: .staging, logLevel: nil)), - (Logger.Level.debug, EnvVars(appEnv: .development, logLevel: nil)), - (Logger.Level.info, EnvVars(appEnv: .production, logLevel: nil)), - (Logger.Level.trace, EnvVars(appEnv: .testing, logLevel: nil)), - (Logger.Level.info, EnvVars(appEnv: .staging, logLevel: .info)), - (Logger.Level.trace, EnvVars(appEnv: .development, logLevel: .trace)), - (Logger.Level.warning, EnvVars(appEnv: .production, logLevel: .warning)), - (Logger.Level.debug, EnvVars(appEnv: .testing, logLevel: .debug)) - ] -) -func logLevelFromEnvVars(expectedLevel: Logger.Level, environment: EnvVars) { - withDependencies { - $0.cliClient = .liveValue - } operation: { + func testParsingMQTTVersion() { @Dependency(\.cliClient) var cliClient - #expect(cliClient.logLevel(environment) == expectedLevel) - } -} -@Test( - arguments: [ - ( - CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: nil), - EnvVars() - ), - ( - CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: "3"), - EnvVars(version: "3") - ), - ( - CliClient.EnvVarsRequest( - envFilePath: "Tests/CliClientTests/test.env", - logger: nil, - version: nil + let arguments = [ + (MQTTClient.Version.v3_1_1, ["3", "3.1", "3.1.1", "00367894"]), + (MQTTClient.Version.v5_0, ["5", "5.1", "5.1.1", "00000500012"]), + (nil, ["0", "0.1", "0.1.1", "0000000001267894", "blob"]) + ] + + for (version, strings) in arguments { + for string in strings { + XCTAssertEqual(cliClient.parseMqttClientVersion(string), version) + } + } + } + + func testLogLevelFromEnvironment() { + @Dependency(\.cliClient) var cliClient + + let arguments = [ + (Logger.Level.debug, EnvVars(appEnv: .staging, logLevel: nil)), + (Logger.Level.debug, EnvVars(appEnv: .development, logLevel: nil)), + (Logger.Level.info, EnvVars(appEnv: .production, logLevel: nil)), + (Logger.Level.trace, EnvVars(appEnv: .testing, logLevel: nil)), + (Logger.Level.info, EnvVars(appEnv: .staging, logLevel: .info)), + (Logger.Level.trace, EnvVars(appEnv: .development, logLevel: .trace)), + (Logger.Level.warning, EnvVars(appEnv: .production, logLevel: .warning)), + (Logger.Level.debug, EnvVars(appEnv: .testing, logLevel: .debug)) + ] + + for (expected, envVars) in arguments { + XCTAssertEqual(expected, cliClient.logLevel(envVars)) + } + } + + func testMakeEnvVars() async throws { + @Dependency(\.cliClient) var cliClient + @Dependency(\.environment) var environment + + let arguments = [ + ( + CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: nil), + EnvVars() ), - EnvVars( - appEnv: .testing, - host: "test.mqtt", - port: "1234", - identifier: "testing-mqtt", - userName: "test-user", - password: "super-secret", - logLevel: .debug, - version: "5.0" + ( + CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: "3"), + EnvVars(version: "3") + ), + ( + CliClient.EnvVarsRequest( + envFilePath: "test.env", // Needs to be a bundled resource. + logger: nil, + version: nil + ), + EnvVars.test + ), + ( + CliClient.EnvVarsRequest( + envFilePath: "test-env.json", // Needs to be a bundled resource. + logger: nil, + version: nil + ), + EnvVars.test ) - ) - ] -) -func checkMakeEnvVars( - request: CliClient.EnvVarsRequest, - expectedEnvVars: EnvVars -) async throws { - try await withDependencies { - $0.cliClient = .liveValue - $0.environment = .liveValue - $0.environment.processInfo = { [:] } - } operation: { - @Dependency(\.cliClient) var cliClient - let result = try await cliClient.makeEnvVars(request) - #expect(result == expectedEnvVars) + ] + + for (request, expectedEnvVars) in arguments { + var request = request + if let file = request.envFilePath { + request = .init( + envFilePath: cleanFilePath(file), + logger: request.logger, + version: request.mqttClientVersion + ) + } + let result = try await cliClient.makeEnvVars(request) + XCTAssertEqual(result, expectedEnvVars) + } + } + + func testFileType() { + let arguments = [ + (EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"), + (EnvironmentDependency.FileType.json(path: "test.json"), "test.json"), + (nil, "test"), + (nil, "") + ] + + for (expected, file) in arguments { + XCTAssertEqual(EnvironmentDependency.FileType(path: file), expected) + } + } + + func testEnvironmentLiveValueProcessInfo() { + let environment = EnvironmentDependency.liveValue + XCTAssertEqual(environment.processInfo(), ProcessInfo.processInfo.environment) } } + +// - MARK: Helper + +private func cleanFilePath(_ path: String) -> String { + let split = path.split(separator: ".") + let fileName = split.first! + let ext = split.last! + let url = Bundle.module.url(forResource: String(fileName), withExtension: String(ext))!.absoluteString + let cleaned = url.split(separator: "file://").last! + return String(cleaned) +} + +extension EnvVars { + static let test = EnvVars( + appEnv: .testing, + host: "test.mqtt", + port: "1234", + identifier: "testing-mqtt", + userName: "test-user", + password: "super-secret", + logLevel: .debug, + version: "5.0" + ) +} diff --git a/Tests/CliClientTests/test-env.json b/Tests/CliClientTests/test-env.json new file mode 100755 index 0000000..7c625ec --- /dev/null +++ b/Tests/CliClientTests/test-env.json @@ -0,0 +1,10 @@ +{ + "APP_ENV": "testing", + "MQTT_HOST": "test.mqtt", + "MQTT_PORT": "1234", + "MQTT_IDENTIFIER": "testing-mqtt", + "MQTT_USERNAME": "test-user", + "MQTT_PASSWORD": "super-secret", + "LOG_LEVEL": "debug", + "MQTT_VERSION": "5.0" +} diff --git a/docker/Dockerfile.test b/docker/Dockerfile.test index cd9123b..ad06064 100644 --- a/docker/Dockerfile.test +++ b/docker/Dockerfile.test @@ -1,5 +1,6 @@ # Used to build a test image. -FROM swift:5.10 +ARG SWIFT_IMAGE_VERSION="5.10" +FROM swift:${SWIFT_IMAGE_VERSION} WORKDIR /app COPY ./Package.* ./ RUN swift package resolve