diff --git a/Docs/commands-help/plan.md b/Docs/commands-help/plan.md index ac648eab..2834c972 100644 --- a/Docs/commands-help/plan.md +++ b/Docs/commands-help/plan.md @@ -35,6 +35,7 @@ The command allows you to write a plan (combination of commands) in YAML format: cache_plan_name: - command: warmup argument: s3.eu-west-2.amazonaws.com + headers: 'secret-key: ${RUGBY_S3_SECRET_KEY}' except: SomePod arch: x86_64 - command: build @@ -174,3 +175,12 @@ usual: strip: true ``` For example, the [cache](shortcuts/cache.md) command has a flag `strip`. + +Plans can access environment variables in two different ways: +```yml +usual: +- command: warmup + argument: s3.eu-west-2.amazonaws.com + headers: 'secret-key: ${RUGBY_S3_SECRET_KEY}' + except: $BAD_POD_TARGET_NAME0 +``` diff --git a/Sources/Rugby/Commands/Mixed/Plan.swift b/Sources/Rugby/Commands/Mixed/Plan.swift index 543d9267..8abb91f6 100644 --- a/Sources/Rugby/Commands/Mixed/Plan.swift +++ b/Sources/Rugby/Commands/Mixed/Plan.swift @@ -25,7 +25,7 @@ struct Plan: AsyncParsableCommand { // It's a hidden subcommand if let name, name == .plansList { // Prints raw plans list for autocompletion - let plans = (try? dependencies.plansParser.plans(atPath: path)) ?? [] + let plans = (try? await dependencies.plansParser.plans(atPath: path)) ?? [] plans.forEach { print($0.name) } return } @@ -40,15 +40,15 @@ struct Plan: AsyncParsableCommand { extension Plan: RunnableCommand { func body() async throws { - let plan = try selectPlan() + let plan = try await selectPlan() try await run(plan: plan) } - private func selectPlan() throws -> RugbyFoundation.Plan { + private func selectPlan() async throws -> RugbyFoundation.Plan { if let name { - return try dependencies.plansParser.planNamed(name, path: path) + return try await dependencies.plansParser.planNamed(name, path: path) } else { - return try dependencies.plansParser.topPlan(atPath: path) + return try await dependencies.plansParser.topPlan(atPath: path) } } diff --git a/Sources/RugbyFoundation/Core/Common/Hashers/BuildPhasesHasher.swift b/Sources/RugbyFoundation/Core/Common/Hashers/BuildPhasesHasher.swift index 1ecd6c07..762d88de 100644 --- a/Sources/RugbyFoundation/Core/Common/Hashers/BuildPhasesHasher.swift +++ b/Sources/RugbyFoundation/Core/Common/Hashers/BuildPhasesHasher.swift @@ -14,18 +14,18 @@ final class BuildPhaseHasher: Loggable { let logger: ILogger private let workingDirectoryPath: String private let fileContentHasher: IFileContentHasher - private let xcodeEnvResolver: IXcodeEnvResolver + private let envVariablesResolver: IEnvVariablesResolver private let dollarSymbol = "$" init(logger: ILogger, workingDirectoryPath: String, fileContentHasher: IFileContentHasher, - xcodeEnvResolver: IXcodeEnvResolver) { + envVariablesResolver: IEnvVariablesResolver) { self.workingDirectoryPath = workingDirectoryPath self.logger = logger self.fileContentHasher = fileContentHasher - self.xcodeEnvResolver = xcodeEnvResolver + self.envVariablesResolver = envVariablesResolver } // MARK: - Private @@ -64,7 +64,7 @@ final class BuildPhaseHasher: Loggable { additionalEnv: [String: String] ) async throws -> (resolved: [String], unresolved: [String]) { let pathsToFileLists = try await paths.concurrentMap { - try await self.xcodeEnvResolver.resolve(path: $0, additionalEnv: additionalEnv) + try await self.envVariablesResolver.resolveXcodeVariables(in: $0, additionalEnv: additionalEnv) } let (unresolvedFileLists, resolvedFileLists) = Set(pathsToFileLists).partition { $0.contains(dollarSymbol) } @@ -73,7 +73,7 @@ final class BuildPhaseHasher: Loggable { let content = try File.read(at: path) let pathsFromFile = content.components(separatedBy: "\n") return try await pathsFromFile.concurrentMap { - try await self.xcodeEnvResolver.resolve(path: $0, additionalEnv: additionalEnv) + try await self.envVariablesResolver.resolveXcodeVariables(in: $0, additionalEnv: additionalEnv) } } diff --git a/Sources/RugbyFoundation/Core/Common/Hashers/XcodeEnvResolver.swift b/Sources/RugbyFoundation/Core/Common/Hashers/XcodeEnvResolver.swift deleted file mode 100644 index ffb3a800..00000000 --- a/Sources/RugbyFoundation/Core/Common/Hashers/XcodeEnvResolver.swift +++ /dev/null @@ -1,42 +0,0 @@ -// MARK: - Interface - -protocol IXcodeEnvResolver: AnyObject { - func resolve( - path: String, - additionalEnv: [String: String] - ) async throws -> String -} - -// MARK: - Implementation - -final class XcodeEnvResolver: Loggable { - let logger: ILogger - private let env: [String: String] - private let regex = #"\$[\{|\(][^}]*?[\}|\)]"# - - init(logger: ILogger, env: [String: String]) { - self.logger = logger - self.env = env - } -} - -extension XcodeEnvResolver: IXcodeEnvResolver { - func resolve(path: String, additionalEnv: [String: String]) async throws -> String { - var resolvedPath = path - var replaced = true - while replaced { - replaced = false - let matches = try resolvedPath.groups(regex: regex) - resolvedPath = await matches.reduce(into: resolvedPath) { path, match in - let variable = String(match.dropFirst(2).dropLast(1)) // Drop ${ and } - guard let replace = env[variable] ?? additionalEnv[variable] else { - await log("Can't find \(match) environment variable.", level: .info) - return - } - path = path.replacingOccurrences(of: match, with: replace) - replaced = true - } - } - return resolvedPath - } -} diff --git a/Sources/RugbyFoundation/Core/Env/EnvVariablesResolver.swift b/Sources/RugbyFoundation/Core/Env/EnvVariablesResolver.swift new file mode 100644 index 00000000..b9877a88 --- /dev/null +++ b/Sources/RugbyFoundation/Core/Env/EnvVariablesResolver.swift @@ -0,0 +1,62 @@ +// MARK: - Interface + +protocol IEnvVariablesResolver: AnyObject { + func resolve(in string: String) async throws -> String + + func resolveXcodeVariables( + in string: String, + additionalEnv: [String: String] + ) async throws -> String +} + +// MARK: - Implementation + +final class EnvVariablesResolver: Loggable { + let logger: ILogger + private let env: [String: String] + + private let envVariablesRegex = #"\$[\{]?([a-zA-Z0-9_]+)[\}]?"# + private let xcodeVariablesRegex = #"\$[\{\(]?([a-zA-Z0-9_]+)[\}\)]?"# + + init(logger: ILogger, env: [String: String]) { + self.logger = logger + self.env = env + } + + private func resolve( + in string: String, + withRegex regex: String, + additionalEnv: [String: String] + ) async throws -> String { + var resolvedPath = string + var replaced = true + while replaced { + replaced = false + let groups = try resolvedPath.groups(regex: regex) + guard groups.count == 2 else { continue } + let (match, variable) = (groups[0], groups[1]) + guard let replace = env[variable] ?? additionalEnv[variable] else { + await log("Can't find \(match) environment variable.", level: .info) + continue + } + resolvedPath = resolvedPath.replacingOccurrences(of: match, with: replace) + replaced = true + } + return resolvedPath + } +} + +// MARK: - IEnvVariablesResolver + +extension EnvVariablesResolver: IEnvVariablesResolver { + func resolve(in string: String) async throws -> String { + try await resolve(in: string, withRegex: envVariablesRegex, additionalEnv: [:]) + } + + func resolveXcodeVariables( + in string: String, + additionalEnv: [String: String] + ) async throws -> String { + try await resolve(in: string, withRegex: xcodeVariablesRegex, additionalEnv: additionalEnv) + } +} diff --git a/Sources/RugbyFoundation/Core/Plans/PlansParser.swift b/Sources/RugbyFoundation/Core/Plans/PlansParser.swift index 049ed3a6..e0b3e733 100644 --- a/Sources/RugbyFoundation/Core/Plans/PlansParser.swift +++ b/Sources/RugbyFoundation/Core/Plans/PlansParser.swift @@ -8,17 +8,17 @@ import Yams public protocol IPlansParser: AnyObject { /// Returns plans list. /// - Parameter path: A path to plans file. - func plans(atPath path: String) throws -> [Plan] + func plans(atPath path: String) async throws -> [Plan] /// Returns the first plan from file at a path. /// - Parameter path: A path to plans file. - func topPlan(atPath path: String) throws -> Plan + func topPlan(atPath path: String) async throws -> Plan /// Returns a plan with the name. /// - Parameters: /// - name: A name of plan to find. /// - path: A path to plans file. - func planNamed(_ name: String, path: String) throws -> Plan + func planNamed(_ name: String, path: String) async throws -> Plan } /// The Rugby plan structure. @@ -76,18 +76,24 @@ enum PlansParserError: LocalizedError { final class PlansParser { private typealias Error = PlansParserError private typealias RawCommand = [String: Any] + + private let envVariablesResolver: IEnvVariablesResolver private var cache: [String: [Plan]] = [:] private let topPlanRegex = #"^([\w-]+)(?=:)"# - private let parsers: [FieldParser] = [ - StringFieldParser(), + private lazy var parsers: [FieldParser] = [ + StringFieldParser(envVariablesResolver: envVariablesResolver), BoolFieldParser(), IntFieldParser(), - StringsFieldParser() + StringsFieldParser(envVariablesResolver: envVariablesResolver) ] + init(envVariablesResolver: IEnvVariablesResolver) { + self.envVariablesResolver = envVariablesResolver + } + // MARK: - Methods - private func parse(path: String) throws -> [Plan] { + private func parse(path: String) async throws -> [Plan] { if let cachedPlans = cache[path] { return cachedPlans } let content = try File.read(at: path) @@ -100,8 +106,13 @@ final class PlansParser { // Bubbling up the 1st plan let sortedPlans = rawPlans.sorted { lhs, _ in lhs.key == firstPlan } - let plans = try sortedPlans.compactMap { name, commands in - try Plan(name: name, commands: commands.compactMap(parseCommand)) + var plans: [Plan] = [] + for (name, commands) in sortedPlans { + var parsedCommands: [Plan.Command] = [] + for command in commands { + try await parsedCommands.append(parseCommand(command)) + } + plans.append(Plan(name: name, commands: parsedCommands)) } cache[path] = plans return plans @@ -113,16 +124,20 @@ final class PlansParser { return firstPlan } - private func parseCommand(_ command: RawCommand) throws -> Plan.Command { + private func parseCommand(_ command: RawCommand) async throws -> Plan.Command { guard let commandName = command[.commandKey] as? String else { throw Error.missedCommandType } - let args: [String] = try command.keys.sorted().reduce(into: []) { args, key in - guard let value = command[key], key != .commandKey else { return } - guard parsers.contains(where: { $0.parse(value, ofField: key, toArgs: &args) }) else { - throw Error.unknownArgumentType(value) + var args: [String] = [] + for key in command.keys.sorted() { + guard let value = command[key], key != .commandKey else { continue } + var parsed = false + for parser in parsers { + parsed = try await parser.parse(value, ofField: key, toArgs: &args) + if parsed { break } } + guard parsed else { throw Error.unknownArgumentType(value) } } return Plan.Command(name: commandName, args: args) } @@ -139,17 +154,24 @@ private extension String { // MARK: - Field Parsers private protocol FieldParser: AnyObject { - func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) -> Bool + func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) async throws -> Bool } private final class StringFieldParser: FieldParser { - func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) -> Bool { + private let envVariablesResolver: IEnvVariablesResolver + + init(envVariablesResolver: IEnvVariablesResolver) { + self.envVariablesResolver = envVariablesResolver + } + + func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) async throws -> Bool { guard let string = value as? String else { return false } + let resolvedString = try await envVariablesResolver.resolve(in: string) if field == .argumentKey { - args.insert(string, at: 0) + args.insert(resolvedString, at: 0) } else { args.append("\(String.optionPrefix)\(field)") - args.append(string) + args.append(resolvedString) } return true } @@ -175,14 +197,21 @@ private final class IntFieldParser: FieldParser { } private final class StringsFieldParser: FieldParser { - func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) -> Bool { + private let envVariablesResolver: IEnvVariablesResolver + + init(envVariablesResolver: IEnvVariablesResolver) { + self.envVariablesResolver = envVariablesResolver + } + + func parse(_ value: Any, ofField field: String, toArgs args: inout [String]) async throws -> Bool { guard let strings = value as? [String] else { return false } guard strings.isNotEmpty else { return true } + let resolvedStrings = try await strings.concurrentMap(envVariablesResolver.resolve) if field == .argumentKey { - args.insert(contentsOf: strings, at: 0) + args.insert(contentsOf: resolvedStrings, at: 0) } else { args.append("\(String.optionPrefix)\(field)") - args.append(contentsOf: strings) + args.append(contentsOf: resolvedStrings) } return true } @@ -191,20 +220,20 @@ private final class StringsFieldParser: FieldParser { // MARK: - IPlansParser extension PlansParser: IPlansParser { - public func plans(atPath path: String) throws -> [Plan] { - try parse(path: path) + public func plans(atPath path: String) async throws -> [Plan] { + try await parse(path: path) } - public func topPlan(atPath path: String) throws -> Plan { - let plans = try plans(atPath: path) + public func topPlan(atPath path: String) async throws -> Plan { + let plans = try await plans(atPath: path) guard let plan = plans.first else { throw Error.noPlans } return plan } - public func planNamed(_ name: String, path: String) throws -> Plan { - let plans = try plans(atPath: path) + public func planNamed(_ name: String, path: String) async throws -> Plan { + let plans = try await plans(atPath: path) guard let plan = plans.first(where: { $0.name == name }) else { throw Error.noPlanWithName(name) } diff --git a/Sources/RugbyFoundation/Vault/Commands/Vault+Plan.swift b/Sources/RugbyFoundation/Vault/Commands/Vault+Plan.swift index c4fd1a0f..f3cd7af7 100644 --- a/Sources/RugbyFoundation/Vault/Commands/Vault+Plan.swift +++ b/Sources/RugbyFoundation/Vault/Commands/Vault+Plan.swift @@ -1,4 +1,4 @@ public extension Vault { /// The service to parse YAML files with Rugby plans. - var plansParser: IPlansParser { PlansParser() } + var plansParser: IPlansParser { PlansParser(envVariablesResolver: envVariablesResolver) } } diff --git a/Sources/RugbyFoundation/Vault/Vault.swift b/Sources/RugbyFoundation/Vault/Vault.swift index a4494d1c..0e3e5e61 100644 --- a/Sources/RugbyFoundation/Vault/Vault.swift +++ b/Sources/RugbyFoundation/Vault/Vault.swift @@ -1,4 +1,5 @@ import Fish +import Foundation import Rainbow /// The main container of Rugby stuff. @@ -105,6 +106,13 @@ public final class Vault { sharedPath: router.binFolderPath, keepHashYamls: env.keepHashYamls ) + private(set) lazy var envVariablesResolver: IEnvVariablesResolver = EnvVariablesResolver( + logger: logger, + env: ProcessInfo.processInfo.environment.merging([ + .SRCROOT: router.podsPath, + .BUILD_DIR: router.buildPath + ], uniquingKeysWith: { _, rhs in rhs }) + ) func targetsHasher() -> ITargetsHasher { let foundationHasher = SHA1Hasher() let fileContentHasher = FileContentHasher( @@ -118,13 +126,7 @@ public final class Vault { logger: logger, workingDirectoryPath: router.workingDirectory.path, fileContentHasher: fileContentHasher, - xcodeEnvResolver: XcodeEnvResolver( - logger: logger, - env: [ - .SRCROOT: router.podsPath, - .BUILD_DIR: router.buildPath - ] - ) + envVariablesResolver: envVariablesResolver ), cocoaPodsScriptsHasher: CocoaPodsScriptsHasher(fileContentHasher: fileContentHasher), configurationsHasher: ConfigurationsHasher(excludeKeys: [settings.hasBackupKey]), diff --git a/Tests/FoundationTests/Core/Common/Hashers/BuildPhaseHasherTests.swift b/Tests/FoundationTests/Core/Common/Hashers/BuildPhaseHasherTests.swift index b9e574da..44b0a9dc 100644 --- a/Tests/FoundationTests/Core/Common/Hashers/BuildPhaseHasherTests.swift +++ b/Tests/FoundationTests/Core/Common/Hashers/BuildPhaseHasherTests.swift @@ -8,7 +8,7 @@ final class BuildPhaseHasherTests: XCTestCase { private var logger: ILoggerMock! private let workingDirectoryPath = "/Users/swiftyfinch/Developer/Example" private var fileContentHasher: IFileContentHasherMock! - private var xcodeEnvResolver: IXcodeEnvResolverMock! + private var envVariablesResolver: IEnvVariablesResolverMock! private var fishSharedStorage: IFilesManagerMock! private var sut: IBuildPhaseHasher! @@ -16,12 +16,12 @@ final class BuildPhaseHasherTests: XCTestCase { super.setUp() logger = ILoggerMock() fileContentHasher = IFileContentHasherMock() - xcodeEnvResolver = IXcodeEnvResolverMock() + envVariablesResolver = IEnvVariablesResolverMock() sut = BuildPhaseHasher( logger: logger, workingDirectoryPath: workingDirectoryPath, fileContentHasher: fileContentHasher, - xcodeEnvResolver: xcodeEnvResolver + envVariablesResolver: envVariablesResolver ) fishSharedStorage = IFilesManagerMock() @@ -36,7 +36,7 @@ final class BuildPhaseHasherTests: XCTestCase { super.tearDown() logger = nil fileContentHasher = nil - xcodeEnvResolver = nil + envVariablesResolver = nil fishSharedStorage = nil sut = nil } @@ -50,7 +50,7 @@ extension BuildPhaseHasherTests { let resolvedFilesList = "/Users/swiftyfinch/Developer/Repos/Rugby/Example/Pods/Target Support Files/Realm-framework/Realm-framework-xcframeworks-input-files.xcfilelist" let resolvedInputFile0 = "/Users/swiftyfinch/Developer/Repos/Rugby/Example/Pods/Realm/core/realm-monorepo.xcframework" let resolvedInputFile1 = "/Users/swiftyfinch/Developer/Repos/Rugby/Example/Pods/Target Support Files/Realm-framework/Realm-framework-xcframeworks.sh" - xcodeEnvResolver.resolvePathAdditionalEnvClosure = { path, _ in + envVariablesResolver.resolveXcodeVariablesInAdditionalEnvClosure = { path, _ in switch path { case inputFilesList: return resolvedFilesList case inputFile0: return resolvedInputFile0 diff --git a/Tests/FoundationTests/Core/Common/Hashers/XcodeEnvResolverTests.swift b/Tests/FoundationTests/Core/Common/Hashers/XcodeEnvResolverTests.swift deleted file mode 100644 index 16f283d6..00000000 --- a/Tests/FoundationTests/Core/Common/Hashers/XcodeEnvResolverTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -@testable import RugbyFoundation -import XCTest - -final class XcodeEnvResolverTests: XCTestCase { - private var logger: ILoggerMock! - private let env = [ - "SRCROOT": "/Users/swiftyfinch/Example/Pods" - ] - private var sut: IXcodeEnvResolver! - - override func setUp() { - super.setUp() - logger = ILoggerMock() - sut = XcodeEnvResolver(logger: logger, env: env) - } - - override func tearDown() { - super.tearDown() - logger = nil - sut = nil - } -} - -extension XcodeEnvResolverTests { - func test_input_xcfilelist() async throws { - let resolvedPath = try await sut.resolve( - path: "${PODS_ROOT}/Target Support Files/Moya/Moya-xcframeworks-input-files.xcfilelist", - additionalEnv: ["PODS_ROOT": "${SRCROOT}"] - ) - - // Assert - XCTAssertEqual( - resolvedPath, - "/Users/swiftyfinch/Example/Pods/Target Support Files/Moya/Moya-xcframeworks-input-files.xcfilelist" - ) - } - - func test_xcframeworks_script() async throws { - let resolvedPath = try await sut.resolve( - path: "${PODS_ROOT}/Target Support Files/Kingfisher/Kingfisher-xcframeworks.sh", - additionalEnv: ["PODS_ROOT": "${SRCROOT}"] - ) - - // Assert - XCTAssertEqual( - resolvedPath, - "/Users/swiftyfinch/Example/Pods/Target Support Files/Kingfisher/Kingfisher-xcframeworks.sh" - ) - } - - func test_xcframework() async throws { - let resolvedPath = try await sut.resolve( - path: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", - additionalEnv: ["PODS_ROOT": "${SRCROOT}"] - ) - - // Assert - XCTAssertEqual( - resolvedPath, - "/Users/swiftyfinch/Example/Pods/SnapKit/SnapKit.xcframework" - ) - } - - func test_without_all_env_variables() async throws { - let resolvedPath = try await sut.resolve( - path: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", - additionalEnv: [:] - ) - - // Assert - XCTAssertEqual(resolvedPath, "${PODS_ROOT}/SnapKit/SnapKit.xcframework") - } - - func test_override_env_with_additionalEnv() async throws { - let resolvedPath = try await sut.resolve( - path: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", - additionalEnv: ["PODS_ROOT": "/Users/swiftyfinch/Pods"] - ) - - // Assert - XCTAssertEqual(resolvedPath, "/Users/swiftyfinch/Pods/SnapKit/SnapKit.xcframework") - } -} diff --git a/Tests/FoundationTests/Core/Env/XcodeEnvResolverTests.swift b/Tests/FoundationTests/Core/Env/XcodeEnvResolverTests.swift new file mode 100644 index 00000000..1105072d --- /dev/null +++ b/Tests/FoundationTests/Core/Env/XcodeEnvResolverTests.swift @@ -0,0 +1,108 @@ +@testable import RugbyFoundation +import XCTest + +final class EnvVariablesResolverTests: XCTestCase { + private var logger: ILoggerMock! + private let env = [ + "SRCROOT": "/Users/swiftyfinch/Example/Pods", + "SECRET_KEY": "qwerty123" + ] + private var sut: IEnvVariablesResolver! + + override func setUp() { + super.setUp() + logger = ILoggerMock() + sut = EnvVariablesResolver(logger: logger, env: env) + } + + override func tearDown() { + super.tearDown() + logger = nil + sut = nil + } +} + +extension EnvVariablesResolverTests { + func test_input_xcfilelist() async throws { + let resolved = try await sut.resolveXcodeVariables( + in: "${PODS_ROOT}/Target Support Files/Moya/Moya-xcframeworks-input-files.xcfilelist", + additionalEnv: ["PODS_ROOT": "${SRCROOT}"] + ) + + // Assert + XCTAssertEqual( + resolved, + "/Users/swiftyfinch/Example/Pods/Target Support Files/Moya/Moya-xcframeworks-input-files.xcfilelist" + ) + } + + func test_xcframeworks_script() async throws { + let resolved = try await sut.resolveXcodeVariables( + in: "${PODS_ROOT}/Target Support Files/Kingfisher/Kingfisher-xcframeworks.sh", + additionalEnv: ["PODS_ROOT": "${SRCROOT}"] + ) + + // Assert + XCTAssertEqual( + resolved, + "/Users/swiftyfinch/Example/Pods/Target Support Files/Kingfisher/Kingfisher-xcframeworks.sh" + ) + } + + func test_xcframework() async throws { + let resolved = try await sut.resolveXcodeVariables( + in: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", + additionalEnv: ["PODS_ROOT": "${SRCROOT}"] + ) + + // Assert + XCTAssertEqual( + resolved, + "/Users/swiftyfinch/Example/Pods/SnapKit/SnapKit.xcframework" + ) + } + + func test_without_all_env_variables() async throws { + let resolved = try await sut.resolveXcodeVariables( + in: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", + additionalEnv: [:] + ) + + // Assert + XCTAssertEqual(resolved, "${PODS_ROOT}/SnapKit/SnapKit.xcframework") + } + + func test_override_env_with_additionalEnv() async throws { + let resolved = try await sut.resolveXcodeVariables( + in: "${PODS_ROOT}/SnapKit/SnapKit.xcframework", + additionalEnv: ["PODS_ROOT": "/Users/swiftyfinch/Pods"] + ) + + // Assert + XCTAssertEqual(resolved, "/Users/swiftyfinch/Pods/SnapKit/SnapKit.xcframework") + } + + func test_allTypesXcodeResolving() async throws { + let resolved0 = try await sut.resolveXcodeVariables(in: "${SRCROOT}/SnapKit.xcframework", additionalEnv: [:]) + let resolved1 = try await sut.resolveXcodeVariables(in: "$SRCROOT/SnapKit.xcframework", additionalEnv: [:]) + let resolved2 = try await sut.resolveXcodeVariables(in: "$(SRCROOT)/SnapKit.xcframework", additionalEnv: [:]) + + // Assert + XCTAssertEqual(resolved0, "/Users/swiftyfinch/Example/Pods/SnapKit.xcframework") + XCTAssertEqual(resolved1, "/Users/swiftyfinch/Example/Pods/SnapKit.xcframework") + XCTAssertEqual(resolved2, "/Users/swiftyfinch/Example/Pods/SnapKit.xcframework") + } +} + +extension EnvVariablesResolverTests { + func test_resolvedNotXcodeVariables() async throws { + let resolved0 = try await sut.resolve(in: "secret_key: ${SECRET_KEY}") + let resolved1 = try await sut.resolve(in: "secret_key: $SECRET_KEY") + let resolved2 = try await sut.resolve(in: "secret_key: $(SECRET_KEY)") + + // Assert + XCTAssertEqual(resolved0, "secret_key: qwerty123") + XCTAssertEqual(resolved1, "secret_key: qwerty123") + XCTAssertEqual(resolved2, "secret_key: $(SECRET_KEY)") + } +} diff --git a/Tests/FoundationTests/Core/Plans/PlansParserTests.swift b/Tests/FoundationTests/Core/Plans/PlansParserTests.swift index b6846782..77db8358 100644 --- a/Tests/FoundationTests/Core/Plans/PlansParserTests.swift +++ b/Tests/FoundationTests/Core/Plans/PlansParserTests.swift @@ -5,6 +5,7 @@ import XCTest final class PlansParserTests: XCTestCase { private var sut: IPlansParser! private var fishSharedStorage: IFilesManagerMock! + private var envVariablesResolver: IEnvVariablesResolverMock! override func setUp() { super.setUp() @@ -14,7 +15,8 @@ final class PlansParserTests: XCTestCase { addTeardownBlock { Fish.sharedStorage = backupFishSharedStorage } - sut = PlansParser() + envVariablesResolver = IEnvVariablesResolverMock() + sut = PlansParser(envVariablesResolver: envVariablesResolver) } override func tearDown() { @@ -22,13 +24,14 @@ final class PlansParserTests: XCTestCase { sut = nil fishSharedStorage.readFileClosure = nil fishSharedStorage = nil + envVariablesResolver = nil } } // MARK: - Top Plan extension PlansParserTests { - func test_top_plan() throws { + func test_top_plan() async throws { let expected = Plan( name: "usual", commands: [Plan.Command(name: "cache", args: ["--arch", "x86_64", "--strip"])] @@ -43,9 +46,10 @@ extension PlansParserTests { } let path = "test" let pathURL = URL(fileURLWithPath: path) + envVariablesResolver.resolveInClosure = { $0 } // Act - let result = try sut.topPlan(atPath: path) + let result = try await sut.topPlan(atPath: path) // Assert XCTAssertEqual(result, expected) @@ -53,7 +57,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_top_plan_cached() throws { + func test_top_plan_cached() async throws { let expected = Plan( name: "usual", commands: [Plan.Command(name: "cache", args: ["--arch", "x86_64", "--strip"])] @@ -68,10 +72,11 @@ extension PlansParserTests { } let path = "test" let pathURL = URL(fileURLWithPath: path) + envVariablesResolver.resolveInClosure = { $0 } // Act - let result = try sut.topPlan(atPath: path) - let result2 = try sut.topPlan(atPath: path) + let result = try await sut.topPlan(atPath: path) + let result2 = try await sut.topPlan(atPath: path) // Assert XCTAssertEqual(result, expected) @@ -80,7 +85,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_top_plan_arguments() throws { + func test_top_plan_arguments() async throws { let expected = Plan( name: "usual", commands: [Plan.Command(name: "cache", args: ["A", "B"])] @@ -96,9 +101,10 @@ extension PlansParserTests { } let path = "test" let pathURL = URL(fileURLWithPath: path) + envVariablesResolver.resolveInClosure = { $0 } // Act - let result = try sut.topPlan(atPath: path) + let result = try await sut.topPlan(atPath: path) // Assert XCTAssertEqual(result, expected) @@ -106,7 +112,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_top_plan_noPlans() throws { + func test_top_plan_noPlans() async throws { let expectedError = PlansParserError.noPlans fishSharedStorage.readFileClosure = { _ in "" } let path = "test" @@ -116,7 +122,7 @@ extension PlansParserTests { var result: Plan? var resultError: Error? do { - result = try sut.topPlan(atPath: path) + result = try await sut.topPlan(atPath: path) } catch { resultError = error } @@ -132,7 +138,7 @@ extension PlansParserTests { // MARK: - Named Plan extension PlansParserTests { - func test_named_plan() throws { + func test_named_plan() async throws { let expected = Plan( name: "tests", commands: [Plan.Command(name: "warmup", args: ["s3.eu-west-2.amazonaws.com", "--timeout", "120"])] @@ -151,9 +157,10 @@ extension PlansParserTests { } let path = "test" let pathURL = URL(fileURLWithPath: path) + envVariablesResolver.resolveInClosure = { $0 } // Act - let result = try sut.planNamed("tests", path: path) + let result = try await sut.planNamed("tests", path: path) // Assert XCTAssertEqual(result, expected) @@ -161,7 +168,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_named_plan_noPlanWithName() throws { + func test_named_plan_noPlanWithName() async throws { let planName = "ci" let expectedError = PlansParserError.noPlanWithName(planName) fishSharedStorage.readFileClosure = { _ in @@ -177,7 +184,7 @@ extension PlansParserTests { var result: Plan? var resultError: Error? do { - result = try sut.planNamed(planName, path: path) + result = try await sut.planNamed(planName, path: path) } catch { resultError = error } @@ -189,7 +196,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_named_plan_missedCommandType() throws { + func test_named_plan_missedCommandType() async throws { let planName = "ci" let expectedError = PlansParserError.missedCommandType fishSharedStorage.readFileClosure = { _ in @@ -205,7 +212,7 @@ extension PlansParserTests { var result: Plan? var resultError: Error? do { - result = try sut.planNamed(planName, path: path) + result = try await sut.planNamed(planName, path: path) } catch { resultError = error } @@ -217,7 +224,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_named_plan_incorrectFormat() throws { + func test_named_plan_incorrectFormat() async throws { let planName = "ci" let expectedError = PlansParserError.incorrectFormat fishSharedStorage.readFileClosure = { _ in @@ -232,7 +239,7 @@ extension PlansParserTests { var result: Plan? var resultError: Error? do { - result = try sut.planNamed(planName, path: path) + result = try await sut.planNamed(planName, path: path) } catch { resultError = error } @@ -244,7 +251,7 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } - func test_named_plan_unknownArgumentType() throws { + func test_named_plan_unknownArgumentType() async throws { let planName = "ci" let testValue = 0.3 let expectedError = PlansParserError.unknownArgumentType(testValue) @@ -262,7 +269,7 @@ extension PlansParserTests { var result: Plan? var resultError: Error? do { - result = try sut.planNamed(planName, path: path) + result = try await sut.planNamed(planName, path: path) } catch { resultError = error } @@ -273,4 +280,36 @@ extension PlansParserTests { XCTAssertEqual(fishSharedStorage.readFileCallsCount, 1) XCTAssertEqual(fishSharedStorage.readFileReceivedFile, pathURL) } + + func test_resolveEnvVariablesInStrings() async throws { + fishSharedStorage.readFileClosure = { _ in + """ + plan_with_env: + - command: warmup + argument: s3.eu-west-2.amazonaws.com + timeout: 120 + headers: "secret_key: ${SECRET_KEY}" + targets: [Alamofire, $ANOTHER_TARGET] + except: $(ENV_VARIABLE) + config: $(SHARED_CONFIG) + """ + } + envVariablesResolver.resolveInClosure = { $0 } + + // Act + _ = try await sut.planNamed("plan_with_env", path: "") + + // Assert + XCTAssertEqual( + Set(envVariablesResolver.resolveInReceivedInvocations), + [ + "s3.eu-west-2.amazonaws.com", + "$(SHARED_CONFIG)", + "$(ENV_VARIABLE)", + "secret_key: ${SECRET_KEY}", + "Alamofire", + "$ANOTHER_TARGET" + ] + ) + } } diff --git a/Tests/FoundationTests/Mocks/IEnvVariablesResolverMock.generated.swift b/Tests/FoundationTests/Mocks/IEnvVariablesResolverMock.generated.swift new file mode 100644 index 00000000..8fc1c939 --- /dev/null +++ b/Tests/FoundationTests/Mocks/IEnvVariablesResolverMock.generated.swift @@ -0,0 +1,66 @@ +// Generated using Sourcery 2.1.1 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + +// swiftlint:disable all + +import Foundation +@testable import RugbyFoundation + +final class IEnvVariablesResolverMock: IEnvVariablesResolver { + + // MARK: - resolve + + var resolveInThrowableError: Error? + var resolveInCallsCount = 0 + var resolveInCalled: Bool { resolveInCallsCount > 0 } + var resolveInReceivedString: String? + var resolveInReceivedInvocations: [String] = [] + private let resolveInReceivedInvocationsLock = NSRecursiveLock() + var resolveInReturnValue: String! + var resolveInClosure: ((String) async throws -> String)? + + func resolve(in string: String) async throws -> String { + resolveInCallsCount += 1 + resolveInReceivedString = string + resolveInReceivedInvocationsLock.withLock { + resolveInReceivedInvocations.append(string) + } + if let error = resolveInThrowableError { + throw error + } + if let resolveInClosure = resolveInClosure { + return try await resolveInClosure(string) + } else { + return resolveInReturnValue + } + } + + // MARK: - resolveXcodeVariables + + var resolveXcodeVariablesInAdditionalEnvThrowableError: Error? + var resolveXcodeVariablesInAdditionalEnvCallsCount = 0 + var resolveXcodeVariablesInAdditionalEnvCalled: Bool { resolveXcodeVariablesInAdditionalEnvCallsCount > 0 } + var resolveXcodeVariablesInAdditionalEnvReceivedArguments: (string: String, additionalEnv: [String: String])? + var resolveXcodeVariablesInAdditionalEnvReceivedInvocations: [(string: String, additionalEnv: [String: String])] = [] + private let resolveXcodeVariablesInAdditionalEnvReceivedInvocationsLock = NSRecursiveLock() + var resolveXcodeVariablesInAdditionalEnvReturnValue: String! + var resolveXcodeVariablesInAdditionalEnvClosure: ((String, [String: String]) async throws -> String)? + + func resolveXcodeVariables(in string: String, additionalEnv: [String: String]) async throws -> String { + resolveXcodeVariablesInAdditionalEnvCallsCount += 1 + resolveXcodeVariablesInAdditionalEnvReceivedArguments = (string: string, additionalEnv: additionalEnv) + resolveXcodeVariablesInAdditionalEnvReceivedInvocationsLock.withLock { + resolveXcodeVariablesInAdditionalEnvReceivedInvocations.append((string: string, additionalEnv: additionalEnv)) + } + if let error = resolveXcodeVariablesInAdditionalEnvThrowableError { + throw error + } + if let resolveXcodeVariablesInAdditionalEnvClosure = resolveXcodeVariablesInAdditionalEnvClosure { + return try await resolveXcodeVariablesInAdditionalEnvClosure(string, additionalEnv) + } else { + return resolveXcodeVariablesInAdditionalEnvReturnValue + } + } +} + +// swiftlint:enable all diff --git a/Tests/FoundationTests/Mocks/IXcodeEnvResolverMock.generated.swift b/Tests/FoundationTests/Mocks/IXcodeEnvResolverMock.generated.swift deleted file mode 100644 index 4a628b02..00000000 --- a/Tests/FoundationTests/Mocks/IXcodeEnvResolverMock.generated.swift +++ /dev/null @@ -1,39 +0,0 @@ -// Generated using Sourcery 2.1.1 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT - -// swiftlint:disable all - -import Foundation -@testable import RugbyFoundation - -final class IXcodeEnvResolverMock: IXcodeEnvResolver { - - // MARK: - resolve - - var resolvePathAdditionalEnvThrowableError: Error? - var resolvePathAdditionalEnvCallsCount = 0 - var resolvePathAdditionalEnvCalled: Bool { resolvePathAdditionalEnvCallsCount > 0 } - var resolvePathAdditionalEnvReceivedArguments: (path: String, additionalEnv: [String: String])? - var resolvePathAdditionalEnvReceivedInvocations: [(path: String, additionalEnv: [String: String])] = [] - private let resolvePathAdditionalEnvReceivedInvocationsLock = NSRecursiveLock() - var resolvePathAdditionalEnvReturnValue: String! - var resolvePathAdditionalEnvClosure: ((String, [String: String]) async throws -> String)? - - func resolve(path: String, additionalEnv: [String: String]) async throws -> String { - resolvePathAdditionalEnvCallsCount += 1 - resolvePathAdditionalEnvReceivedArguments = (path: path, additionalEnv: additionalEnv) - resolvePathAdditionalEnvReceivedInvocationsLock.withLock { - resolvePathAdditionalEnvReceivedInvocations.append((path: path, additionalEnv: additionalEnv)) - } - if let error = resolvePathAdditionalEnvThrowableError { - throw error - } - if let resolvePathAdditionalEnvClosure = resolvePathAdditionalEnvClosure { - return try await resolvePathAdditionalEnvClosure(path, additionalEnv) - } else { - return resolvePathAdditionalEnvReturnValue - } - } -} - -// swiftlint:enable all diff --git a/Tests/FoundationTests/Mocks/Mocks.swift b/Tests/FoundationTests/Mocks/Mocks.swift index 08aa3a2d..90736f16 100644 --- a/Tests/FoundationTests/Mocks/Mocks.swift +++ b/Tests/FoundationTests/Mocks/Mocks.swift @@ -59,7 +59,7 @@ extension IProductHasher {} extension IBuildRulesHasher {} // sourcery: AutoMockable, testableImports = ["RugbyFoundation"] -extension IXcodeEnvResolver {} +extension IEnvVariablesResolver {} //// sourcery: AutoMockable, testableImports = ["RugbyFoundation"] extension ISwiftVersionProvider {}