diff --git a/Sources/TuistCore/Analytics/CommandEvent.swift b/Sources/TuistCore/Analytics/CommandEvent.swift index 016ddcfec28..5757e0ecfc2 100644 --- a/Sources/TuistCore/Analytics/CommandEvent.swift +++ b/Sources/TuistCore/Analytics/CommandEvent.swift @@ -21,6 +21,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { public let gitBranch: String? public let graph: RunGraph? public let previewId: String? + public let resultBundlePath: AbsolutePath? public enum Status: Codable, Equatable { case success, failure(String) @@ -49,6 +50,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { case gitBranch case graph case previewId + case resultBundlePath } public init( @@ -69,7 +71,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { gitRemoteURLOrigin: String?, gitBranch: String?, graph: RunGraph?, - previewId: String? + previewId: String?, + resultBundlePath: AbsolutePath? ) { self.runId = runId self.name = name @@ -89,6 +92,7 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { self.gitBranch = gitBranch self.graph = graph self.previewId = previewId + self.resultBundlePath = resultBundlePath } } @@ -111,7 +115,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { gitRemoteURLOrigin: String? = "https://github.com/tuist/tuist", gitBranch: String? = "main", graph: RunGraph = RunGraph(name: "Graph", projects: []), - previewId: String? = nil + previewId: String? = nil, + resultBundlePath: AbsolutePath? = nil ) -> CommandEvent { CommandEvent( runId: runId, @@ -131,7 +136,8 @@ public struct CommandEvent: Codable, Equatable, AsyncQueueEvent { gitRemoteURLOrigin: gitRemoteURLOrigin, gitBranch: gitBranch, graph: graph, - previewId: previewId + previewId: previewId, + resultBundlePath: resultBundlePath ) } } diff --git a/Sources/TuistCore/Analytics/RunMetadataStorage.swift b/Sources/TuistCore/Analytics/RunMetadataStorage.swift index 4775538a31e..b0e86032778 100644 --- a/Sources/TuistCore/Analytics/RunMetadataStorage.swift +++ b/Sources/TuistCore/Analytics/RunMetadataStorage.swift @@ -32,6 +32,12 @@ public actor RunMetadataStorage { public func update(previewId: String?) { self.previewId = previewId } + + /// Path to the result bundle that should be uploaded when running commands like `tuist xcodebuild test` + public private(set) var resultBundlePath: AbsolutePath? + public func update(resultBundlePath: AbsolutePath?) { + self.resultBundlePath = resultBundlePath + } } private enum RunMetadataStorageContextKey: ServiceContextKey { diff --git a/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift b/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift index c096fd64edb..f5f9e978789 100644 --- a/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift +++ b/Sources/TuistKit/Commands/TrackableCommand/CommandEventFactory.swift @@ -70,7 +70,8 @@ public final class CommandEventFactory { gitRemoteURLOrigin: gitRemoteURLOrigin, gitBranch: gitBranch, graph: graph, - previewId: info.previewId + previewId: info.previewId, + resultBundlePath: info.resultBundlePath ) return commandEvent } diff --git a/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift b/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift index 046d12de125..774418a398b 100644 --- a/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift +++ b/Sources/TuistKit/Commands/TrackableCommand/TrackableCommand.swift @@ -1,11 +1,13 @@ import ArgumentParser import Foundation +import OpenAPIRuntime import Path import ServiceContextModule import TuistAnalytics import TuistAsyncQueue import TuistCache import TuistCore +import TuistServer import TuistSupport import XcodeGraph @@ -21,6 +23,7 @@ public struct TrackableCommandInfo { let binaryCacheItems: [AbsolutePath: [String: CacheItem]] let selectiveTestingCacheItems: [AbsolutePath: [String: CacheItem]] let previewId: String? + let resultBundlePath: AbsolutePath? } /// A `TrackableCommand` wraps a `ParsableCommand` and reports its execution to an analytics provider @@ -31,6 +34,7 @@ public class TrackableCommand { private let commandEventFactory: CommandEventFactory private let asyncQueue: AsyncQueuing private let fileHandler: FileHandling + private let ciChecker: CIChecking public init( command: ParsableCommand, @@ -38,7 +42,8 @@ public class TrackableCommand { clock: Clock = WallClock(), commandEventFactory: CommandEventFactory = CommandEventFactory(), asyncQueue: AsyncQueuing = AsyncQueue.sharedInstance, - fileHandler: FileHandling = FileHandler.shared + fileHandler: FileHandling = FileHandler.shared, + ciChecker: CIChecking = CIChecker() ) { self.command = command self.commandArguments = commandArguments @@ -46,10 +51,11 @@ public class TrackableCommand { self.commandEventFactory = commandEventFactory self.asyncQueue = asyncQueue self.fileHandler = fileHandler + self.ciChecker = ciChecker } public func run( - analyticsEnabled: Bool + backend: TuistAnalyticsServerBackend? ) async throws { let timer = clock.startTimer() let pathIndex = commandArguments.firstIndex(of: "--path") @@ -69,23 +75,25 @@ public class TrackableCommand { } else { try command.run() } - if analyticsEnabled { + if let backend { try await dispatchCommandEvent( timer: timer, status: .success, runId: runMetadataStorage.runId, path: path, - runMetadataStorage: runMetadataStorage + runMetadataStorage: runMetadataStorage, + backend: backend ) } } catch { - if analyticsEnabled { + if let backend { try await dispatchCommandEvent( timer: timer, status: .failure("\(error)"), runId: await runMetadataStorage.runId, path: path, - runMetadataStorage: runMetadataStorage + runMetadataStorage: runMetadataStorage, + backend: backend ) } throw error @@ -98,7 +106,8 @@ public class TrackableCommand { status: CommandEvent.Status, runId: String, path: AbsolutePath, - runMetadataStorage: RunMetadataStorage + runMetadataStorage: RunMetadataStorage, + backend: TuistAnalyticsServerBackend ) async throws { let durationInSeconds = timer.stop() let durationInMs = Int(durationInSeconds * 1000) @@ -114,17 +123,29 @@ public class TrackableCommand { graph: runMetadataStorage.graph, binaryCacheItems: runMetadataStorage.binaryCacheItems, selectiveTestingCacheItems: runMetadataStorage.selectiveTestingCacheItems, - previewId: runMetadataStorage.previewId + previewId: runMetadataStorage.previewId, + resultBundlePath: runMetadataStorage.resultBundlePath ) let commandEvent = try commandEventFactory.make( from: info, path: path ) - try asyncQueue.dispatch(event: commandEvent) - if let command = command as? TrackableParsableCommand, command.analyticsRequired { - asyncQueue.wait() + if (command as? TrackableParsableCommand)?.analyticsRequired == true || ciChecker.isCI() { + ServiceContext.current?.logger?.info("Uploading run metadata...") + do { + let serverCommandEvent: ServerCommandEvent = try await backend.send(commandEvent: commandEvent) + ServiceContext.current?.logger? + .info( + "You can view a detailed run report at: \(serverCommandEvent.url.absoluteString)" + ) + } catch let error as ClientError { + ServiceContext.current?.logger? + .warning("Failed to upload run metadata: \(String(describing: error.underlyingError))") + } catch { + ServiceContext.current?.logger?.warning("Failed to upload run metadata: \(String(describing: error))") + } } else { - asyncQueue.waitIfCI() + try asyncQueue.dispatch(event: commandEvent) } } diff --git a/Sources/TuistKit/Commands/TuistCommand.swift b/Sources/TuistKit/Commands/TuistCommand.swift index 7e52772d956..647df31d700 100644 --- a/Sources/TuistKit/Commands/TuistCommand.swift +++ b/Sources/TuistKit/Commands/TuistCommand.swift @@ -78,17 +78,17 @@ public struct TuistCommand: AsyncParsableCommand { let config = try await ConfigLoader(warningController: WarningController.shared).loadConfig(path: path) let url = try ServerURLService().url(configServerURL: config.url) - let analyticsEnabled: Bool + let backend: TuistAnalyticsServerBackend? if let fullHandle = config.fullHandle { - let backend = TuistAnalyticsServerBackend( + let tuistAnalyticsServerBackend = TuistAnalyticsServerBackend( fullHandle: fullHandle, url: url ) - let dispatcher = TuistAnalyticsDispatcher(backend: backend) + let dispatcher = TuistAnalyticsDispatcher(backend: tuistAnalyticsServerBackend) try TuistAnalytics.bootstrap(dispatcher: dispatcher) - analyticsEnabled = true + backend = tuistAnalyticsServerBackend } else { - analyticsEnabled = false + backend = nil } try await CacheDirectoriesProvider.bootstrap() @@ -116,7 +116,7 @@ public struct TuistCommand: AsyncParsableCommand { commandArguments: processedArguments ) try await trackableCommand.run( - analyticsEnabled: analyticsEnabled + backend: backend ) } } catch { diff --git a/Sources/TuistKit/Commands/XcodeBuildCommand.swift b/Sources/TuistKit/Commands/XcodeBuildCommand.swift index 6441c9049ee..19b2518a311 100644 --- a/Sources/TuistKit/Commands/XcodeBuildCommand.swift +++ b/Sources/TuistKit/Commands/XcodeBuildCommand.swift @@ -6,7 +6,7 @@ import TuistHasher import TuistServer import XcodeGraph -public struct XcodeBuildCommand: AsyncParsableCommand { +public struct XcodeBuildCommand: AsyncParsableCommand, TrackableParsableCommand { public static var cacheStorageFactory: CacheStorageFactorying = EmptyCacheStorageFactory() public static var selectiveTestingGraphHasher: SelectiveTestingGraphHashing = EmptySelectiveTestingGraphHasher() public static var selectiveTestingService: SelectiveTestingServicing = EmptySelectiveTestingService() @@ -18,6 +18,8 @@ public struct XcodeBuildCommand: AsyncParsableCommand { ) } + var analyticsRequired: Bool { true } + public init() {} @Argument( diff --git a/Sources/TuistKit/Services/XcodeBuildService.swift b/Sources/TuistKit/Services/XcodeBuildService.swift index bd259683cc6..705126aef7c 100644 --- a/Sources/TuistKit/Services/XcodeBuildService.swift +++ b/Sources/TuistKit/Services/XcodeBuildService.swift @@ -1,4 +1,5 @@ import FileSystem +import Foundation import Path import ServiceContextModule import TuistAutomation @@ -43,6 +44,8 @@ struct XcodeBuildService { private let xcodeGraphMapper: XcodeGraphMapping private let xcodeBuildController: XcodeBuildControlling private let configLoader: ConfigLoading + private let cacheDirectoriesProvider: CacheDirectoriesProviding + private let uniqueIDGenerator: UniqueIDGenerating private let cacheStorageFactory: CacheStorageFactorying private let selectiveTestingGraphHasher: SelectiveTestingGraphHashing private let selectiveTestingService: SelectiveTestingServicing @@ -52,6 +55,8 @@ struct XcodeBuildService { xcodeGraphMapper: XcodeGraphMapping = XcodeGraphMapper(), xcodeBuildController: XcodeBuildControlling = XcodeBuildController(), configLoader: ConfigLoading = ConfigLoader(warningController: WarningController.shared), + cacheDirectoriesProvider: CacheDirectoriesProviding = CacheDirectoriesProvider(), + uniqueIDGenerator: UniqueIDGenerating = UniqueIDGenerator(), cacheStorageFactory: CacheStorageFactorying, selectiveTestingGraphHasher: SelectiveTestingGraphHashing, selectiveTestingService: SelectiveTestingServicing @@ -60,6 +65,8 @@ struct XcodeBuildService { self.xcodeGraphMapper = xcodeGraphMapper self.xcodeBuildController = xcodeBuildController self.configLoader = configLoader + self.cacheDirectoriesProvider = cacheDirectoriesProvider + self.uniqueIDGenerator = uniqueIDGenerator self.cacheStorageFactory = cacheStorageFactory self.selectiveTestingGraphHasher = selectiveTestingGraphHasher self.selectiveTestingService = selectiveTestingService @@ -92,7 +99,7 @@ struct XcodeBuildService { throw XcodeBuildServiceError.schemeNotPassed } let graph = try await xcodeGraphMapper.map(at: path) - try await ServiceContext.current?.runMetadataStorage?.update(graph: graph) + await ServiceContext.current?.runMetadataStorage?.update(graph: graph) let graphTraverser = GraphTraverser(graph: graph) guard let scheme = graphTraverser.schemes().first(where: { $0.name == schemeName @@ -174,7 +181,14 @@ struct XcodeBuildService { } try await xcodeBuildController - .run(arguments: passthroughXcodebuildArguments + skipTestingArguments) + .run( + arguments: [ + passthroughXcodebuildArguments, + skipTestingArguments, + resultBundlePathArguments(passthroughXcodebuildArguments: passthroughXcodebuildArguments), + ] + .flatMap { $0 } + ) try await storeTestableGraphTargets( testableGraphTargets, @@ -189,6 +203,30 @@ struct XcodeBuildService { ) } + private func resultBundlePathArguments( + passthroughXcodebuildArguments: [String] + ) async throws -> [String] { + if let resultBundlePathString = passedValue( + for: "-resultBundlePath", + arguments: passthroughXcodebuildArguments + ) { + let currentWorkingDirectory = try await fileSystem.currentWorkingDirectory() + let resultBundlePath = try AbsolutePath(validating: resultBundlePathString, relativeTo: currentWorkingDirectory) + await ServiceContext.current?.runMetadataStorage?.update( + resultBundlePath: resultBundlePath + ) + return [] + } else { + let resultBundlePath = try cacheDirectoriesProvider + .cacheDirectory(for: .runs) + .appending(components: uniqueIDGenerator.uniqueID()) + await ServiceContext.current?.runMetadataStorage?.update( + resultBundlePath: resultBundlePath + ) + return ["-resultBundlePath", resultBundlePath.pathString] + } + } + private func updateRunMetadataStorage( with testableGraphTargets: [GraphTarget], selectiveTestingHashes: [GraphTarget: String], diff --git a/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift b/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift index 081546d8280..ce9b7653ad3 100644 --- a/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift +++ b/Sources/TuistKit/Utils/TuistAnalyticsServerBackend.swift @@ -54,36 +54,37 @@ public class TuistAnalyticsServerBackend: TuistAnalyticsBackend { } public func send(commandEvent: CommandEvent) async throws { + let _: ServerCommandEvent = try await send(commandEvent: commandEvent) + } + + public func send(commandEvent: CommandEvent) async throws -> ServerCommandEvent { let serverCommandEvent = try await createCommandEventService.createCommandEvent( commandEvent: commandEvent, projectId: fullHandle, serverURL: url ) - - let runDirectory = try cacheDirectoriesProvider + let runsDirectory = try cacheDirectoriesProvider .cacheDirectory(for: .runs) - .appending(component: commandEvent.runId) - let resultBundle = runDirectory + let runDirectory = runsDirectory.appending(component: commandEvent.runId) + + let resultBundlePath = commandEvent.resultBundlePath ?? runDirectory .appending(component: "\(Constants.resultBundleName).xcresult") - if try await fileSystem.exists(resultBundle) { + if try await fileSystem.exists(resultBundlePath) { try await analyticsArtifactUploadService.uploadResultBundle( - resultBundle, + resultBundlePath, commandEventId: serverCommandEvent.id, serverURL: url ) } - if try await fileSystem.exists(runDirectory) { - try await fileSystem.remove(runDirectory) + if resultBundlePath.parentDirectory.commonAncestor(with: runsDirectory) == runsDirectory, + try await fileSystem.exists(resultBundlePath) + { + try await fileSystem.remove(resultBundlePath) } - if #available(macOS 13.0, *), ciChecker.isCI() { - ServiceContext.current?.logger? - .info( - "You can view a detailed report at: \(serverCommandEvent.url.absoluteString)" - ) - } + return serverCommandEvent } } diff --git a/Sources/TuistServer/Models/ServerCommandEvent.swift b/Sources/TuistServer/Models/ServerCommandEvent.swift index f5c8c90641c..da577868ae5 100644 --- a/Sources/TuistServer/Models/ServerCommandEvent.swift +++ b/Sources/TuistServer/Models/ServerCommandEvent.swift @@ -1,7 +1,7 @@ import Foundation /// Server command event -public struct ServerCommandEvent: Codable { +public struct ServerCommandEvent: Codable, Equatable { public let id: Int public let name: String public let url: URL diff --git a/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift b/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift index c6e02fe41e4..f93b13edec8 100644 --- a/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift +++ b/Sources/TuistServer/Services/AnalyticsArtifactUploadService.swift @@ -1,3 +1,4 @@ +import FileSystem import Foundation import Mockable import Path @@ -14,7 +15,7 @@ public protocol AnalyticsArtifactUploadServicing { } public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServicing { - private let fileHandler: FileHandling + private let fileSystem: FileSysteming private let xcresultToolController: XCResultToolControlling private let fileArchiver: FileArchivingFactorying private let retryProvider: RetryProviding @@ -26,7 +27,7 @@ public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServic public convenience init() { self.init( - fileHandler: FileHandler.shared, + fileSystem: FileSystem(), xcresultToolController: XCResultToolController(), fileArchiver: FileArchivingFactory(), retryProvider: RetryProvider(), @@ -41,7 +42,7 @@ public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServic } init( - fileHandler: FileHandling, + fileSystem: FileSysteming, xcresultToolController: XCResultToolControlling, fileArchiver: FileArchivingFactorying, retryProvider: RetryProviding, @@ -51,7 +52,7 @@ public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServic multipartUploadCompleteAnalyticsService: MultipartUploadCompleteAnalyticsServicing, completeAnalyticsArtifactsUploadsService: CompleteAnalyticsArtifactsUploadsServicing ) { - self.fileHandler = fileHandler + self.fileSystem = fileSystem self.xcresultToolController = xcresultToolController self.fileArchiver = fileArchiver self.retryProvider = retryProvider @@ -76,47 +77,49 @@ public final class AnalyticsArtifactUploadService: AnalyticsArtifactUploadServic serverURL: serverURL ) - let invocationRecordString = try await xcresultToolController.resultBundleObject(resultBundle) - let invocationRecordPath = resultBundle.parentDirectory.appending(component: "invocation_record.json") - try fileHandler.write(invocationRecordString, path: invocationRecordPath, atomically: true) + try await fileSystem.runInTemporaryDirectory(prefix: UUID().uuidString) { temporaryPath in + let invocationRecordString = try await xcresultToolController.resultBundleObject(resultBundle) + let invocationRecordPath = temporaryPath.appending(component: "invocation_record.json") + try await fileSystem.writeText(invocationRecordString, at: invocationRecordPath) + + let decoder = JSONDecoder() + let invocationRecord = try decoder.decode(InvocationRecord.self, from: invocationRecordString.data(using: .utf8)!) + for testActionRecord in invocationRecord.actions._values + .filter({ $0.schemeCommandName._value == "Test" }) + { + guard let id = testActionRecord.actionResult.testsRef?.id._value else { continue } + let resultBundleObjectString = try await xcresultToolController.resultBundleObject( + resultBundle, + id: id + ) + let filename = "\(id).json" + let resultBundleObjectPath = temporaryPath.appending(component: filename) + try await fileSystem.writeText(resultBundleObjectString, at: resultBundleObjectPath) + try await uploadAnalyticsArtifact( + ServerCommandEvent.Artifact( + type: .resultBundleObject, + name: id + ), + artifactPath: resultBundleObjectPath, + commandEventId: commandEventId, + serverURL: serverURL + ) + } - let decoder = JSONDecoder() - let invocationRecord = try decoder.decode(InvocationRecord.self, from: invocationRecordString.data(using: .utf8)!) - for testActionRecord in invocationRecord.actions._values - .filter({ $0.schemeCommandName._value == "Test" }) - { - guard let id = testActionRecord.actionResult.testsRef?.id._value else { continue } - let resultBundleObjectString = try await xcresultToolController.resultBundleObject( - resultBundle, - id: id - ) - let filename = "\(id).json" - let resultBundleObjectPath = resultBundle.parentDirectory.appending(component: filename) - try fileHandler.write(resultBundleObjectString, path: resultBundleObjectPath, atomically: true) try await uploadAnalyticsArtifact( ServerCommandEvent.Artifact( - type: .resultBundleObject, - name: id + type: .invocationRecord ), - artifactPath: resultBundleObjectPath, + artifactPath: invocationRecordPath, commandEventId: commandEventId, serverURL: serverURL ) - } - try await uploadAnalyticsArtifact( - ServerCommandEvent.Artifact( - type: .invocationRecord - ), - artifactPath: invocationRecordPath, - commandEventId: commandEventId, - serverURL: serverURL - ) - - try await completeAnalyticsArtifactsUploadsService.completeAnalyticsArtifactsUploads( - commandEventId: commandEventId, - serverURL: serverURL - ) + try await completeAnalyticsArtifactsUploadsService.completeAnalyticsArtifactsUploads( + commandEventId: commandEventId, + serverURL: serverURL + ) + } } private func uploadAnalyticsArtifact( diff --git a/Sources/TuistServer/Utilities/UniqueIDGenerator.swift b/Sources/TuistServer/Utilities/UniqueIDGenerator.swift deleted file mode 100644 index fb1103af5eb..00000000000 --- a/Sources/TuistServer/Utilities/UniqueIDGenerator.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Foundation -import Mockable - -@Mockable -protocol UniqueIDGenerating { - func uniqueID() -> String -} - -final class UniqueIDGenerator: UniqueIDGenerating { - func uniqueID() -> String { - UUID().uuidString - } -} diff --git a/Sources/TuistSupport/Utils/UniqueIDGenerator.swift b/Sources/TuistSupport/Utils/UniqueIDGenerator.swift new file mode 100644 index 00000000000..63cdc36ed0a --- /dev/null +++ b/Sources/TuistSupport/Utils/UniqueIDGenerator.swift @@ -0,0 +1,15 @@ +import Foundation +import Mockable + +@Mockable +public protocol UniqueIDGenerating { + func uniqueID() -> String +} + +public struct UniqueIDGenerator: UniqueIDGenerating { + public init() {} + + public func uniqueID() -> String { + UUID().uuidString + } +} diff --git a/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift b/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift index b3f1fabfa5e..0270d61d199 100644 --- a/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift +++ b/Tests/TuistKitTests/CommandTracking/CommandEventFactoryTests.swift @@ -111,7 +111,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { ), ], ], - previewId: nil + previewId: nil, + resultBundlePath: nil ) let expectedEvent = CommandEvent( runId: "run-id", @@ -189,7 +190,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { ), ] ), - previewId: nil + previewId: nil, + resultBundlePath: nil ) given(gitController) @@ -258,7 +260,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { graph: nil, binaryCacheItems: [:], selectiveTestingCacheItems: [:], - previewId: nil + previewId: nil, + resultBundlePath: nil ) given(gitController) @@ -294,7 +297,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { graph: nil, binaryCacheItems: [:], selectiveTestingCacheItems: [:], - previewId: nil + previewId: nil, + resultBundlePath: nil ) given(gitController) @@ -346,7 +350,8 @@ final class CommandEventFactoryTests: TuistUnitTestCase { graph: nil, binaryCacheItems: [:], selectiveTestingCacheItems: [:], - previewId: nil + previewId: nil, + resultBundlePath: nil ) given(gitController) diff --git a/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift b/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift index dd009217563..ab03bfa25c1 100644 --- a/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift +++ b/Tests/TuistKitTests/CommandTracking/TrackableCommandTests.swift @@ -5,6 +5,7 @@ import Path import TuistAnalytics import TuistAsyncQueue import TuistCore +import TuistServer import TuistSupport import XCTest @@ -69,7 +70,10 @@ final class TrackableCommandTests: TuistTestCase { // Given makeSubject(flag: false, shouldFail: true) // When - await XCTAssertThrowsSpecific(try await subject.run(analyticsEnabled: true), TestCommand.TestError.commandFailed) + await XCTAssertThrowsSpecific( + try await subject.run(backend: TuistAnalyticsServerBackend(fullHandle: "", url: .test())), + TestCommand.TestError.commandFailed + ) // Then verify(asyncQueue) @@ -84,7 +88,7 @@ final class TrackableCommandTests: TuistTestCase { makeSubject(commandArguments: ["cache", "warm", "--path", "/my-path"]) // When - try await subject.run(analyticsEnabled: true) + try await subject.run(backend: TuistAnalyticsServerBackend(fullHandle: "", url: .test())) // Then verify(asyncQueue) @@ -95,12 +99,12 @@ final class TrackableCommandTests: TuistTestCase { .called(1) } - func test_whenPathIsInArguments_and_analytics_are_disabled() async throws { + func test_whenPathIsInArguments_and_no_backend_is_set() async throws { // Given makeSubject(commandArguments: ["cache", "warm", "--path", "/my-path"]) // When - try await subject.run(analyticsEnabled: false) + try await subject.run(backend: nil) // Then verify(asyncQueue) @@ -116,7 +120,7 @@ final class TrackableCommandTests: TuistTestCase { makeSubject(commandArguments: ["cache", "warm"]) // When - try await subject.run(analyticsEnabled: true) + try await subject.run(backend: TuistAnalyticsServerBackend(fullHandle: "", url: .test())) // Then verify(asyncQueue) @@ -137,12 +141,12 @@ final class TrackableCommandTests: TuistTestCase { ) // When - try await subject.run(analyticsEnabled: true) + try await subject.run(backend: MockTuistServerAnalyticsBackend(fullHandle: "", url: .test())) // Then verify(asyncQueue) .wait() - .called(1) + .called(0) } } @@ -176,3 +180,9 @@ private struct TestCommand: TrackableParsableCommand, ParsableCommand { } } } + +final class MockTuistServerAnalyticsBackend: TuistAnalyticsServerBackend { + override func send(commandEvent _: CommandEvent) async throws -> ServerCommandEvent { + return .test() + } +} diff --git a/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift b/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift index 84e1f9e9abb..8054f990f00 100644 --- a/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift +++ b/Tests/TuistKitTests/Services/XcodeBuildServiceTests.swift @@ -10,6 +10,7 @@ import TuistCore import TuistHasher import TuistLoader import TuistServer +import TuistSupport import XcodeGraph import protocol XcodeGraphMapper.XcodeGraphMapping @@ -25,6 +26,8 @@ struct XcodeBuildServiceTests { private let cacheStorage = MockCacheStoring() private let selectiveTestingGraphHasher = MockSelectiveTestingGraphHashing() private let selectiveTestingService = MockSelectiveTestingServicing() + private let cacheDirectoriesProvider = MockCacheDirectoriesProviding() + private let uniqueIDGenerator = MockUniqueIDGenerating() private let subject: XcodeBuildService init() { let cacheStorageFactory = MockCacheStorageFactorying() @@ -40,11 +43,19 @@ struct XcodeBuildServiceTests { given(cacheStorage) .store(.any, cacheCategory: .any) .willReturn() + given(cacheDirectoriesProvider) + .cacheDirectory(for: .any) + .willReturn(try! AbsolutePath(validating: "/tmp/runs")) + given(uniqueIDGenerator) + .uniqueID() + .willReturn("unique-id") subject = XcodeBuildService( fileSystem: fileSystem, xcodeGraphMapper: xcodeGraphMapper, xcodeBuildController: xcodeBuildController, + cacheDirectoriesProvider: cacheDirectoriesProvider, + uniqueIDGenerator: uniqueIDGenerator, cacheStorageFactory: cacheStorageFactory, selectiveTestingGraphHasher: selectiveTestingGraphHasher, selectiveTestingService: selectiveTestingService @@ -341,6 +352,7 @@ struct XcodeBuildServiceTests { "test", "-scheme", "App", "-skip-testing:AUnitTests", + "-resultBundlePath", "/tmp/runs/unique-id", ] ) ) @@ -494,12 +506,102 @@ struct XcodeBuildServiceTests { "-scheme", "App", "-testPlan", "MyTestPlan", "-skip-testing:AUnitTests", + "-resultBundlePath", "/tmp/runs/unique-id", ] ) ) .called(1) } } + + @Test func preservesResultBundlePathWhenPassed() async throws { + try await fileSystem.runInTemporaryDirectory(prefix: "XcodeBuildServiceTests") { + temporaryPath in + var context = ServiceContext.current ?? ServiceContext.topLevel + let runMetadataStorage = RunMetadataStorage() + context.runMetadataStorage = runMetadataStorage + try await ServiceContext.withValue(context) { + // Given + let aUnitTestsTarget: Target = .test(name: "AUnitTests") + let bUnitTestsTarget: Target = .test(name: "BUnitTests") + let project: Project = .test( + path: temporaryPath, + targets: [ + aUnitTestsTarget, + bUnitTestsTarget, + ], + schemes: [ + .test( + name: "App", + testAction: .test( + targets: [ + .test( + target: TargetReference( + projectPath: temporaryPath, + name: "AUnitTests" + ) + ), + ] + ) + ), + ] + ) + + given(xcodeGraphMapper) + .map(at: .any) + .willReturn( + .test( + projects: [ + temporaryPath: project, + ] + ) + ) + + given(selectiveTestingGraphHasher) + .hash( + graph: .any, + additionalStrings: .any + ) + .willReturn([:]) + given(selectiveTestingService) + .cachedTests( + scheme: .any, + graph: .any, + selectiveTestingHashes: .any, + selectiveTestingCacheItems: .any + ) + .willReturn([]) + + given(cacheStorage) + .fetch(.any, cacheCategory: .any) + .willReturn([:]) + + // When + try await subject.run( + passthroughXcodebuildArguments: [ + "test", + "-scheme", "App", + "-resultBundlePath", "/custom-path", + ] + ) + + // Then + verify(xcodeBuildController) + .run( + arguments: .value( + [ + "test", + "-scheme", "App", + "-resultBundlePath", "/custom-path", + ] + ) + ) + .called(1) + let resultBundlePath = try AbsolutePath(validating: "/custom-path") + await #expect(runMetadataStorage.resultBundlePath == resultBundlePath) + } + } + } } @Mockable diff --git a/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift b/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift index ad8951fe800..76c3ccb25a4 100644 --- a/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift +++ b/Tests/TuistKitTests/Utils/TuistAnalyticsDispatcherTests.swift @@ -104,7 +104,8 @@ final class TuistAnalyticsDispatcherTests: TuistUnitTestCase { gitRemoteURLOrigin: "https://github.com/tuist/tuist", gitBranch: "main", graph: nil, - previewId: nil + previewId: nil, + resultBundlePath: nil ) } diff --git a/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift b/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift index 689492dca50..382986abafc 100644 --- a/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift +++ b/Tests/TuistKitTests/Utils/TuistAnalyticsServerBackendTests.swift @@ -70,7 +70,7 @@ final class TuistAnalyticsServerBackendTests: TuistUnitTestCase { ) // When - try await subject.send(commandEvent: event) + let _: ServerCommandEvent = try await subject.send(commandEvent: event) // Then XCTAssertPrinterOutputNotContains("You can view a detailed report at: https://tuist.dev/tuist-org/tuist/runs/10") @@ -87,24 +87,20 @@ final class TuistAnalyticsServerBackendTests: TuistUnitTestCase { .isCI() .willReturn(true) let event = CommandEvent.test() + let serverCommandEvent: ServerCommandEvent = .test(id: 10) given(createCommandEventService) .createCommandEvent( commandEvent: .value(event), projectId: .value(fullHandle), serverURL: .value(Constants.URLs.production) ) - .willReturn( - .test( - id: 10, - url: URL(string: "https://tuist.dev/tuist-org/tuist/runs/10")! - ) - ) + .willReturn(serverCommandEvent) // When - try await subject.send(commandEvent: event) + let got: ServerCommandEvent = try await subject.send(commandEvent: event) // Then - XCTAssertStandardOutput(pattern: "You can view a detailed report at: https://tuist.dev/tuist-org/tuist/runs/10") + XCTAssertEqual(got, serverCommandEvent) } } @@ -115,18 +111,14 @@ final class TuistAnalyticsServerBackendTests: TuistUnitTestCase { .isCI() .willReturn(true) let event = CommandEvent.test() + let serverCommandEvent: ServerCommandEvent = .test(id: 11) given(createCommandEventService) .createCommandEvent( commandEvent: .value(event), projectId: .value(fullHandle), serverURL: .value(Constants.URLs.production) ) - .willReturn( - .test( - id: 10, - url: URL(string: "https://tuist.dev/tuist-org/tuist/runs/10")! - ) - ) + .willReturn(serverCommandEvent) given(cacheDirectoriesProvider) .cacheDirectory(for: .value(.runs)) @@ -140,16 +132,16 @@ final class TuistAnalyticsServerBackendTests: TuistUnitTestCase { given(analyticsArtifactUploadService) .uploadResultBundle( .value(resultBundle), - commandEventId: .value(10), + commandEventId: .value(11), serverURL: .value(Constants.URLs.production) ) .willReturn(()) // When - try await subject.send(commandEvent: event) + let got: ServerCommandEvent = try await subject.send(commandEvent: event) // Then - XCTAssertStandardOutput(pattern: "You can view a detailed report at: https://tuist.dev/tuist-org/tuist/runs/10") + XCTAssertEqual(got, serverCommandEvent) let exists = try await fileSystem.exists(resultBundle) XCTAssertFalse(exists) } diff --git a/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift b/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift index b1a2a5c20a4..cfd1c7331c4 100644 --- a/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift +++ b/Tests/TuistServerTests/Services/AnalyticsArtifactUploadServiceTests.swift @@ -1,3 +1,4 @@ +import FileSystem import Foundation import Mockable import TuistCore @@ -20,6 +21,7 @@ final class AnalyticsArtifactUploadServiceTests: TuistTestCase { override func setUp() { super.setUp() + let fileSystem = FileSystem() xcresultToolController = .init() fileArchiverFactory = .init() multipartUploadStartAnalyticsService = .init() @@ -29,7 +31,7 @@ final class AnalyticsArtifactUploadServiceTests: TuistTestCase { completeAnalyticsArtifactsUploadsService = .init() subject = AnalyticsArtifactUploadService( - fileHandler: fileHandler, + fileSystem: fileSystem, xcresultToolController: xcresultToolController, fileArchiver: fileArchiverFactory, retryProvider: RetryProvider(), @@ -112,7 +114,7 @@ final class AnalyticsArtifactUploadServiceTests: TuistTestCase { given(multipartUploadArtifactService) .multipartUploadArtifact( - artifactPath: .value(resultBundle.parentDirectory.appending(component: "invocation_record.json")), + artifactPath: .matching { $0.basename == "invocation_record.json" }, generateUploadURL: .any ) .willReturn([(etag: "etag", partNumber: 1)]) @@ -145,7 +147,7 @@ final class AnalyticsArtifactUploadServiceTests: TuistTestCase { given(multipartUploadArtifactService) .multipartUploadArtifact( - artifactPath: .value(resultBundle.parentDirectory.appending(component: "\(testResultBundleObjectId).json")), + artifactPath: .matching { $0.basename == "\(testResultBundleObjectId).json" }, generateUploadURL: .any ) .willReturn([(etag: "etag", partNumber: 1)])