From b09a1dcda47a07a26829bce35ca0fcf8430bbe53 Mon Sep 17 00:00:00 2001 From: Vyacheslav Khorkov Date: Mon, 26 Feb 2024 21:20:43 +0500 Subject: [PATCH 1/3] Fix tests naming --- .../FoundationTests/Core/Clear/CleanerTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename "Tests/FoundationTests/Core/Clear/\320\241leanerTests.swift" => Tests/FoundationTests/Core/Clear/CleanerTests.swift (98%) diff --git "a/Tests/FoundationTests/Core/Clear/\320\241leanerTests.swift" b/Tests/FoundationTests/Core/Clear/CleanerTests.swift similarity index 98% rename from "Tests/FoundationTests/Core/Clear/\320\241leanerTests.swift" rename to Tests/FoundationTests/Core/Clear/CleanerTests.swift index a4bce8e7..376a8e57 100644 --- "a/Tests/FoundationTests/Core/Clear/\320\241leanerTests.swift" +++ b/Tests/FoundationTests/Core/Clear/CleanerTests.swift @@ -2,7 +2,7 @@ import Fish @testable import RugbyFoundation import XCTest -final class СleanerTests: XCTestCase { +final class CleanerTests: XCTestCase { private var sut: ICleaner! private var sharedRugbyFolder: IFolder! private var buildFolder: IFolder! @@ -28,7 +28,7 @@ final class СleanerTests: XCTestCase { } } -extension СleanerTests { +extension CleanerTests { func test_deleteSharedBinaries() async throws { try sharedRugbyFolder.createFolder(named: "Alamofire") .createFile(named: "Content", contents: "test_alamofire_content") From c9b1b658aee952e44e72cacf7d2ec1fc0f8adf7e Mon Sep 17 00:00:00 2001 From: Vyacheslav Khorkov Date: Mon, 26 Feb 2024 22:14:08 +0500 Subject: [PATCH 2/3] Refactoring XcodeBuild --- .../Core/Build/BuildManager.swift | 2 +- .../Core/Build/XcodeBuild/XcodeBuild.swift | 52 ++++++++++++------- .../Build/XcodeBuild/XcodeBuildExecutor.swift | 28 +++++++--- .../Vault/Commands/Build/Vault+Build.swift | 9 +--- .../Vault/Commands/Build/Vault+Prebuild.swift | 5 +- Sources/RugbyFoundation/Vault/Vault.swift | 12 +++++ .../XcodeBuild/XcodeBuildExecutorTests.swift | 18 ++++--- .../Build/XcodeBuild/XcodeBuildTests.swift | 16 +++--- .../Mocks/IXcodeBuildMock.generated.swift | 25 +++++++-- 9 files changed, 114 insertions(+), 53 deletions(-) diff --git a/Sources/RugbyFoundation/Core/Build/BuildManager.swift b/Sources/RugbyFoundation/Core/Build/BuildManager.swift index cfee68d1..800f45af 100644 --- a/Sources/RugbyFoundation/Core/Build/BuildManager.swift +++ b/Sources/RugbyFoundation/Core/Build/BuildManager.swift @@ -171,7 +171,7 @@ extension BuildManager: IInternalBuildManager { processInterruptionTask.cancel() cleanup() } - try self.xcodeBuild.build(target: target.name, options: options, paths: paths) + try await xcodeBuild.build(target: target.name, options: options, paths: paths) }) } } diff --git a/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuild.swift b/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuild.swift index e7226cfd..83427a02 100644 --- a/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuild.swift +++ b/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuild.swift @@ -5,7 +5,7 @@ import Fish protocol IXcodeBuild: AnyObject { func build(target: String, options: XcodeBuildOptions, - paths: XcodeBuildPaths) throws + paths: XcodeBuildPaths) async throws } /// Xcode build options. @@ -81,31 +81,47 @@ final class XcodeBuild { init(xcodeBuildExecutor: IXcodeBuildExecutor) { self.xcodeBuildExecutor = xcodeBuildExecutor } -} -extension XcodeBuild: IXcodeBuild { - func build(target: String, - options: XcodeBuildOptions, - paths: XcodeBuildPaths) throws { + private func run( + arguments: [String], + options: XcodeBuildOptions, + paths: XcodeBuildPaths + ) async throws { let command = "NSUnbufferedIO=YES xcodebuild" - var arguments = [ + var arguments = arguments + arguments.append(contentsOf: [ "-project \(paths.project.shellFriendly)", - "-target \(target)", - "-sdk \(options.sdk.xcodebuild)", - "-config \(options.config.shellFriendly)", - "ARCHS=\(options.arch)", - "SYMROOT=\(paths.symroot.shellFriendly)", - "-parallelizeTargets" - ] + "SYMROOT=\(paths.symroot.shellFriendly)" + ]) options.resultBundlePath.map { arguments.append("-resultBundlePath \($0.shellFriendly)") } arguments.append(contentsOf: options.xcargs) try Folder.create(at: paths.symroot) - try xcodeBuildExecutor.run(command, - rawLogPath: paths.rawLog, - logPath: paths.beautifiedLog, - args: arguments) + try await xcodeBuildExecutor.run(command, + rawLogPath: paths.rawLog, + logPath: paths.beautifiedLog, + args: arguments) + } +} + +// MARK: - IXcodeBuild + +extension XcodeBuild: IXcodeBuild { + func build(target: String, + options: XcodeBuildOptions, + paths: XcodeBuildPaths) async throws { + try await run( + arguments: [ + "-target \(target)", + "-sdk \(options.sdk.xcodebuild)", + "-config \(options.config.shellFriendly)", + "ARCHS=\(options.arch)", + "-parallelizeTargets" + ], + options: options, + paths: paths + ) } } diff --git a/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuildExecutor.swift b/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuildExecutor.swift index 261e592a..d09433b2 100644 --- a/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuildExecutor.swift +++ b/Sources/RugbyFoundation/Core/Build/XcodeBuild/XcodeBuildExecutor.swift @@ -11,36 +11,47 @@ protocol IXcodeBuildExecutor { rawLogPath: String, logPath: String, args: Any... - ) throws + ) async throws } // MARK: - Implementation -final class XcodeBuildExecutor: IXcodeBuildExecutor { +final class XcodeBuildExecutor: IXcodeBuildExecutor, Loggable { + let logger: ILogger private let shellExecutor: IShellExecutor private let logFormatter: IBuildLogFormatter - init(shellExecutor: IShellExecutor, + init(logger: ILogger, + shellExecutor: IShellExecutor, logFormatter: IBuildLogFormatter) { + self.logger = logger self.shellExecutor = shellExecutor self.logFormatter = logFormatter } - func run(_ command: String, rawLogPath: String, logPath: String, args: Any...) throws { + func run(_ command: String, rawLogPath: String, logPath: String, args: Any...) async throws { try Folder.create(at: URL(fileURLWithPath: rawLogPath).deletingLastPathComponent().path) try shellExecutor.throwingShell(command, args: args, "| tee '\(rawLogPath)'") - if let errors = try? beautifyLog(rawLogPath: rawLogPath, logPath: logPath), errors.isNotEmpty { + if let errors = try? await beautifyLog(rawLogPath: rawLogPath, logPath: logPath), errors.isNotEmpty { throw BuildError.buildFailed(errors: errors, buildLogPath: logPath, rawBuildLogPath: rawLogPath) } } // MARK: - Private - private func beautifyLog(rawLogPath: String, logPath: String) throws -> [String] { + private func beautifyLog(rawLogPath: String, logPath: String) async throws -> [String] { + var tests: [String] = [] var errors: [String] = [] let log = try File.create(at: logPath) let output: (String, OutputType) throws -> Void = { formattedLine, type in - if type == .error { errors.append(formattedLine) } + switch type { + case .error: + errors.append(formattedLine) + case .test, .testCase: + tests.append(formattedLine) + case .undefined, .task, .nonContextualError, .warning, .result: + break + } try log.append("\(formattedLine)\n") } @@ -49,6 +60,9 @@ final class XcodeBuildExecutor: IXcodeBuildExecutor { try logFormatter.format(line: line, output: output) } try logFormatter.finish(output: output) + for line in tests { + await logPlain(line) + } return errors } } diff --git a/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Build.swift b/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Build.swift index 1a3d2fa7..179c98e0 100644 --- a/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Build.swift +++ b/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Build.swift @@ -10,12 +10,6 @@ extension Vault { // MARK: - Internal func internalBuildManager() -> IInternalBuildManager { - let logFormatter = BuildLogFormatter(workingDirectory: router.workingDirectory, - colored: Rainbow.enabled) - let xcodeBuildExecutor = XcodeBuildExecutor( - shellExecutor: shellExecutor, - logFormatter: logFormatter - ) let xcodeProject = xcode.project(projectPath: router.podsProjectPath) let buildTargetsManager = BuildTargetsManager(xcodeProject: xcodeProject) let useBinariesManager = useBinariesManager(xcodeProject: xcodeProject, @@ -28,7 +22,6 @@ extension Vault { localRugbyFolderPath: router.rugbyPath, buildFolderPath: router.buildPath ) - let xcodeBuild = XcodeBuild(xcodeBuildExecutor: xcodeBuildExecutor) return BuildManager(logger: logger, buildTargetsManager: buildTargetsManager, librariesPatcher: LibrariesPatcher(logger: logger), @@ -36,7 +29,7 @@ extension Vault { rugbyXcodeProject: RugbyXcodeProject(xcodeProject: xcodeProject), backupManager: backupManager(), processMonitor: processMonitor, - xcodeBuild: xcodeBuild, + xcodeBuild: xcodeBuild(), binariesStorage: binariesStorage, targetsHasher: targetsHasher(), useBinariesManager: useBinariesManager, diff --git a/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Prebuild.swift b/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Prebuild.swift index f2c2faf0..3f20f480 100644 --- a/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Prebuild.swift +++ b/Sources/RugbyFoundation/Vault/Commands/Build/Vault+Prebuild.swift @@ -3,11 +3,12 @@ import Fish public extension Vault { /// The manager to prebuild CocoaPods project. func prebuildManager() -> IPrebuildManager { - PrebuildManager( + let xcodeProject = xcode.project(projectPath: router.podsProjectPath) + return PrebuildManager( logger: logger, xcodePhaseEditor: XcodePhaseEditor(), buildManager: internalBuildManager(), - xcodeProject: xcode.project(projectPath: router.podsProjectPath), + xcodeProject: xcodeProject, binariesStorage: binariesStorage ) } diff --git a/Sources/RugbyFoundation/Vault/Vault.swift b/Sources/RugbyFoundation/Vault/Vault.swift index fa349e35..7b34675c 100644 --- a/Sources/RugbyFoundation/Vault/Vault.swift +++ b/Sources/RugbyFoundation/Vault/Vault.swift @@ -1,4 +1,5 @@ import Fish +import Rainbow /// The main container of Rugby stuff. public final class Vault { @@ -135,6 +136,17 @@ public final class Vault { logsRotator: logsRotator, router: router ) + + func xcodeBuild() -> XcodeBuild { + let logFormatter = BuildLogFormatter(workingDirectory: router.workingDirectory, + colored: Rainbow.enabled) + let xcodeBuildExecutor = XcodeBuildExecutor( + logger: logger, + shellExecutor: shellExecutor, + logFormatter: logFormatter + ) + return XcodeBuild(xcodeBuildExecutor: xcodeBuildExecutor) + } } // MARK: - Keys diff --git a/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildExecutorTests.swift b/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildExecutorTests.swift index 7b161726..fe4b0e3a 100644 --- a/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildExecutorTests.swift +++ b/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildExecutorTests.swift @@ -6,6 +6,7 @@ import XCTest final class XcodeBuildExecutorTests: XCTestCase { private var sut: XcodeBuildExecutor! + private var logger: ILoggerMock! private var shellExecutor: IShellExecutorMock! private var buildLogFormatter: IBuildLogFormatterMock! private var fishSharedStorage: IFilesManagerMock! @@ -20,9 +21,11 @@ final class XcodeBuildExecutorTests: XCTestCase { Fish.sharedStorage = backupFishSharedStorage } + logger = ILoggerMock() shellExecutor = IShellExecutorMock() buildLogFormatter = IBuildLogFormatterMock() sut = XcodeBuildExecutor( + logger: logger, shellExecutor: shellExecutor, logFormatter: buildLogFormatter ) @@ -31,6 +34,7 @@ final class XcodeBuildExecutorTests: XCTestCase { override func tearDown() { super.tearDown() sut = nil + logger = nil shellExecutor = nil buildLogFormatter = nil fishSharedStorage = nil @@ -38,7 +42,7 @@ final class XcodeBuildExecutorTests: XCTestCase { } extension XcodeBuildExecutorTests { - func test_taskInLog() throws { + func test_taskInLog() async throws { let createFileMock = IFileMock() fishSharedStorage.createFileAtContentsReturnValue = createFileMock let createFolderMock = IFolderMock() @@ -64,7 +68,7 @@ extension XcodeBuildExecutorTests { } // Act - try sut.run( + try await sut.run( "test_command", rawLogPath: test_rawLogPath, logPath: test_logPath, @@ -94,7 +98,7 @@ extension XcodeBuildExecutorTests { XCTAssertEqual(createFileMock.appendReceivedInvocations, [expectedContent + "\n"]) } - func test_errorsInLog() throws { + func test_errorsInLog() async throws { let createFileMock = IFileMock() fishSharedStorage.createFileAtContentsReturnValue = createFileMock let createFolderMock = IFolderMock() @@ -125,14 +129,16 @@ extension XcodeBuildExecutorTests { // Act var resultError: Error? - try XCTAssertThrowsError( - sut.run( + do { + try await sut.run( "test_command", rawLogPath: test_rawLogPath, logPath: test_logPath, args: "arg0", "arg1" ) - ) { resultError = $0 } + } catch { + resultError = error + } // Assert XCTAssertEqual( diff --git a/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildTests.swift b/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildTests.swift index c47ec01e..341d1817 100644 --- a/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildTests.swift +++ b/Tests/FoundationTests/Core/Build/XcodeBuild/XcodeBuildTests.swift @@ -29,9 +29,9 @@ final class XcodeBuildTests: XCTestCase { } extension XcodeBuildTests { - func test_general() throws { + func test_general() async throws { fishSharedStorage.createFolderAtReturnValue = IFolderMock() - try sut.build( + try await sut.build( target: "Rugby", options: XcodeBuildOptions( sdk: .ios, @@ -61,13 +61,13 @@ extension XcodeBuildTests { XCTAssertEqual( arguments.args as? [[String]], [[ - "-project /Users/swiftyfinch/Developer/Repos/Rugby/Example/Pods/Pods.xcodeproj", "-target Rugby", "-sdk iphoneos", "-config Debug", "ARCHS=x86_64", - "SYMROOT=/Users/swiftyfinch/Developer/Repos/Rugby/Example/.rugby/build", "-parallelizeTargets", + "-project /Users/swiftyfinch/Developer/Repos/Rugby/Example/Pods/Pods.xcodeproj", + "SYMROOT=/Users/swiftyfinch/Developer/Repos/Rugby/Example/.rugby/build", "-resultBundlePath build.xcresult", "COMPILER_INDEX_STORE_ENABLE=NO", "SWIFT_COMPILATION_MODE=wholemodule", @@ -79,9 +79,9 @@ extension XcodeBuildTests { ) } - func test_spaceInPaths() throws { + func test_spaceInPaths() async throws { fishSharedStorage.createFolderAtReturnValue = IFolderMock() - try sut.build( + try await sut.build( target: "RugbyPods", options: XcodeBuildOptions( sdk: .sim, @@ -111,13 +111,13 @@ extension XcodeBuildTests { XCTAssertEqual( arguments.args as? [[String]], [[ - "-project /Users/swiftyfinch/Developer/Repos/Rugby/Exa\\ mple/Pods/Pods.xcodeproj", "-target RugbyPods", "-sdk iphonesimulator", "-config QA\\ Release", "ARCHS=arm64", - "SYMROOT=/Users/swiftyfinch/Developer/Repos/Rugby/Exa\\ mple/.rugby/build", "-parallelizeTargets", + "-project /Users/swiftyfinch/Developer/Repos/Rugby/Exa\\ mple/Pods/Pods.xcodeproj", + "SYMROOT=/Users/swiftyfinch/Developer/Repos/Rugby/Exa\\ mple/.rugby/build", "COMPILER_INDEX_STORE_ENABLE=NO", "SWIFT_COMPILATION_MODE=wholemodule" ]] diff --git a/Tests/FoundationTests/Mocks/IXcodeBuildMock.generated.swift b/Tests/FoundationTests/Mocks/IXcodeBuildMock.generated.swift index 5532ff11..d3f85e9c 100644 --- a/Tests/FoundationTests/Mocks/IXcodeBuildMock.generated.swift +++ b/Tests/FoundationTests/Mocks/IXcodeBuildMock.generated.swift @@ -15,16 +15,35 @@ final class IXcodeBuildMock: IXcodeBuild { var buildTargetOptionsPathsCalled: Bool { buildTargetOptionsPathsCallsCount > 0 } var buildTargetOptionsPathsReceivedArguments: (target: String, options: XcodeBuildOptions, paths: XcodeBuildPaths)? var buildTargetOptionsPathsReceivedInvocations: [(target: String, options: XcodeBuildOptions, paths: XcodeBuildPaths)] = [] - var buildTargetOptionsPathsClosure: ((String, XcodeBuildOptions, XcodeBuildPaths) throws -> Void)? + var buildTargetOptionsPathsClosure: ((String, XcodeBuildOptions, XcodeBuildPaths) async throws -> Void)? - func build(target: String, options: XcodeBuildOptions, paths: XcodeBuildPaths) throws { + func build(target: String, options: XcodeBuildOptions, paths: XcodeBuildPaths) async throws { buildTargetOptionsPathsCallsCount += 1 buildTargetOptionsPathsReceivedArguments = (target: target, options: options, paths: paths) buildTargetOptionsPathsReceivedInvocations.append((target: target, options: options, paths: paths)) if let error = buildTargetOptionsPathsThrowableError { throw error } - try buildTargetOptionsPathsClosure?(target, options, paths) + try await buildTargetOptionsPathsClosure?(target, options, paths) + } + + // MARK: - test + + var testSchemeTestPlanSimulatorNameOptionsPathsThrowableError: Error? + var testSchemeTestPlanSimulatorNameOptionsPathsCallsCount = 0 + var testSchemeTestPlanSimulatorNameOptionsPathsCalled: Bool { testSchemeTestPlanSimulatorNameOptionsPathsCallsCount > 0 } + var testSchemeTestPlanSimulatorNameOptionsPathsReceivedArguments: (scheme: String, testPlan: String, simulatorName: String, options: XcodeBuildOptions, paths: XcodeBuildPaths)? + var testSchemeTestPlanSimulatorNameOptionsPathsReceivedInvocations: [(scheme: String, testPlan: String, simulatorName: String, options: XcodeBuildOptions, paths: XcodeBuildPaths)] = [] + var testSchemeTestPlanSimulatorNameOptionsPathsClosure: ((String, String, String, XcodeBuildOptions, XcodeBuildPaths) async throws -> Void)? + + func test(scheme: String, testPlan: String, simulatorName: String, options: XcodeBuildOptions, paths: XcodeBuildPaths) async throws { + if let error = testSchemeTestPlanSimulatorNameOptionsPathsThrowableError { + throw error + } + testSchemeTestPlanSimulatorNameOptionsPathsCallsCount += 1 + testSchemeTestPlanSimulatorNameOptionsPathsReceivedArguments = (scheme: scheme, testPlan: testPlan, simulatorName: simulatorName, options: options, paths: paths) + testSchemeTestPlanSimulatorNameOptionsPathsReceivedInvocations.append((scheme: scheme, testPlan: testPlan, simulatorName: simulatorName, options: options, paths: paths)) + try await testSchemeTestPlanSimulatorNameOptionsPathsClosure?(scheme, testPlan, simulatorName, options, paths) } } From d2431198f22d12f6517091dcd2195102b6d141cc Mon Sep 17 00:00:00 2001 From: Vyacheslav Khorkov Date: Mon, 26 Feb 2024 22:14:21 +0500 Subject: [PATCH 3/3] Refactoring BuildError --- Sources/RugbyFoundation/Core/Build/BuildError.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Sources/RugbyFoundation/Core/Build/BuildError.swift b/Sources/RugbyFoundation/Core/Build/BuildError.swift index 0a170392..1bbe5c26 100644 --- a/Sources/RugbyFoundation/Core/Build/BuildError.swift +++ b/Sources/RugbyFoundation/Core/Build/BuildError.swift @@ -8,11 +8,11 @@ enum BuildError: LocalizedError, Equatable { switch self { case let .buildFailed(errors, buildLogPath, rawBuildLogPath): return """ - \("Build failed.".red) + \("Xcodebuild failed.".red) \(errors.map(formatBuildError).joined(separator: "\n").white) \("🚑 More information in build logs:".yellow) - \("[Beautified]".yellow) \("cat \(buildLogPath.homeFinderRelativePath())".white) - \("[Raw]".yellow) \("open \(rawBuildLogPath.homeFinderRelativePath())".white) + \("[Beautified]".yellow) \("cat \(buildLogPath.homeFinderRelativePath())".white.applyingStyle(.default)) + \("[Raw]".yellow) \("open \(rawBuildLogPath.homeFinderRelativePath())".white.applyingStyle(.default)) """ case .cantFindBuildTargets: return "Couldn't find any build targets." @@ -20,7 +20,8 @@ enum BuildError: LocalizedError, Equatable { } private func formatBuildError(_ errorText: String) -> String { - "\("\u{2716}\u{0000FE0E}".red) \(errorText)" + let errorText = errorText.raw.hasPrefix("✖") ? errorText : "\("✖".red) \(errorText)" + return errorText .components(separatedBy: "\n") .map { " \($0)" } .joined(separator: "\n")