diff --git a/Sources/NnexKit/Building/BuildConfig.swift b/Sources/NnexKit/Building/BuildConfig.swift index eefb51a..3561899 100644 --- a/Sources/NnexKit/Building/BuildConfig.swift +++ b/Sources/NnexKit/Building/BuildConfig.swift @@ -5,13 +5,13 @@ // Created by Nikolai Nobadi on 3/25/25. // -public struct BuildConfig: Sendable { +public struct BuildConfig { public let projectName: String public let projectPath: String public let buildType: BuildType public let extraBuildArgs: [String] public let skipClean: Bool - public let testCommand: CurrentSchema.TestCommand? + public let testCommand: HomebrewFormula.TestCommand? /// Initializes a new `BuildConfig` with the specified settings. /// @@ -22,7 +22,7 @@ public struct BuildConfig: Sendable { /// - extraBuildArgs: Additional arguments to pass to the build command. /// - shouldClean: Indicates whether the project should be cleaned before building. Defaults to `true`. /// - testCommand: An optional command to run tests after building. Defaults to `nil`, meaning no tests will be run. - public init(projectName: String, projectPath: String, buildType: BuildType, extraBuildArgs: [String], skipClean: Bool, testCommand: CurrentSchema.TestCommand?) { + public init(projectName: String, projectPath: String, buildType: BuildType, extraBuildArgs: [String], skipClean: Bool, testCommand: HomebrewFormula.TestCommand?) { self.projectName = projectName self.projectPath = projectPath.hasSuffix("/") ? projectPath : projectPath + "/" self.buildType = buildType diff --git a/Sources/NnexKit/Extensions/String+Matches.swift b/Sources/NnexKit/Extensions/String+Matches.swift new file mode 100644 index 0000000..1e72329 --- /dev/null +++ b/Sources/NnexKit/Extensions/String+Matches.swift @@ -0,0 +1,19 @@ +// +// String+Matches.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +public extension String { + /// Returns `true` if the string matches another value, case-insensitively. + /// - Parameter value: The optional string to compare against. + /// - Returns: `true` if both strings match when lowercased; otherwise, `false`. + func matches(_ value: String?) -> Bool { + guard let value else { + return false + } + + return self.lowercased() == value.lowercased() + } +} diff --git a/Sources/NnexKit/Formula/BrewFormula.swift b/Sources/NnexKit/Formula/DecodableFormulaTemplate.swift similarity index 92% rename from Sources/NnexKit/Formula/BrewFormula.swift rename to Sources/NnexKit/Formula/DecodableFormulaTemplate.swift index 8e33296..6eb845e 100644 --- a/Sources/NnexKit/Formula/BrewFormula.swift +++ b/Sources/NnexKit/Formula/DecodableFormulaTemplate.swift @@ -1,12 +1,12 @@ // -// BrewFormula.swift +// DecodableFormulaTemplate.swift // nnex // // Created by Nikolai Nobadi on 3/20/25. // /// Represents a Homebrew formula with metadata and version information. -public struct BrewFormula: Codable { +public struct DecodableFormulaTemplate: Codable { /// The name of the formula. public let name: String @@ -39,4 +39,4 @@ public struct BrewFormula: Codable { self.license = license self.versions = versions } -} \ No newline at end of file +} diff --git a/Sources/NnexKit/Formula/PublishUtilities.swift b/Sources/NnexKit/Formula/PublishUtilities.swift index 80c50fb..f01206a 100644 --- a/Sources/NnexKit/Formula/PublishUtilities.swift +++ b/Sources/NnexKit/Formula/PublishUtilities.swift @@ -14,7 +14,7 @@ public enum PublishUtilities { /// - shell: The shell instance to use for building. /// - Returns: The binary output including path(s) and hash(es). /// - Throws: An error if the build process fails. - public static func buildBinary(formula: SwiftDataHomebrewFormula, buildType: BuildType, skipTesting: Bool, shell: any NnexShell) throws -> BinaryOutput { + public static func buildBinary(formula: HomebrewFormula, buildType: BuildType, skipTesting: Bool, shell: any NnexShell) throws -> BinaryOutput { let testCommand = skipTesting ? nil : formula.testCommand let config = BuildConfig(projectName: formula.name, projectPath: formula.localProjectPath, buildType: buildType, extraBuildArgs: formula.extraBuildArgs, skipClean: false, testCommand: testCommand) let builder = ProjectBuilder(shell: shell, config: config) @@ -49,7 +49,7 @@ public enum PublishUtilities { /// - assetURLs: The asset URLs from the GitHub release. /// - Returns: The formula content as a string. /// - Throws: An error if formula generation fails. - public static func makeFormulaContent(formula: SwiftDataHomebrewFormula, version: String, archivedBinaries: [ArchivedBinary], assetURLs: [String]) throws -> String { + public static func makeFormulaContent(formula: HomebrewFormula, version: String, archivedBinaries: [ArchivedBinary], assetURLs: [String]) throws -> String { let formulaName = formula.name if archivedBinaries.count == 1 { diff --git a/Sources/NnexKit/Git/DefaultGitHandler.swift b/Sources/NnexKit/Git/DefaultGitHandler.swift index 207dec4..1e8c973 100644 --- a/Sources/NnexKit/Git/DefaultGitHandler.swift +++ b/Sources/NnexKit/Git/DefaultGitHandler.swift @@ -19,60 +19,33 @@ public struct DefaultGitHandler { // MARK: - Actions extension DefaultGitHandler: GitHandler { - /// Adds all changes, commits with a given message, and pushes to the remote repository. - /// - Parameters: - /// - message: The commit message describing the changes. - /// - path: The file path of the repository. public func commitAndPush(message: String, path: String) throws { try shell.runAndPrint(bash: makeGitCommand(.addAll, path: path)) try shell.runAndPrint(bash: makeGitCommand(.commit(message: message), path: path)) try shell.runAndPrint(bash: makeGitCommand(.push, path: path)) } - /// Retrieves the remote URL of the repository located at the given path. - /// - Parameter path: The file path of the repository. - /// - Returns: A string representing the remote URL. public func getRemoteURL(path: String) throws -> String { return try shell.getGitHubURL(at: path) } - /// Retrieves the previous release version from the repository at the given path. - /// - Parameter path: The file path of the repository. - /// - Returns: A string representing the previous release version. public func getPreviousReleaseVersion(path: String) throws -> String { return try shell.bash(makeGitHubCommand(.getPreviousReleaseVersion, path: path)) } - /// Initializes a new Git repository at the given path. - /// - Parameter path: The file path where the repository should be initialized. public func gitInit(path: String) throws { try GitStarter(path: path, shell: shell).gitInit() } - /// Initializes a new remote repository on GitHub with specified details and returns the repository URL. - /// - Parameters: - /// - tapName: The name of the remote repository. - /// - path: The file path where the repository is located. - /// - projectDetails: A description of the repository. - /// - visibility: The visibility of the repository (public or private). - /// - Returns: A string representing the repository URL. public func remoteRepoInit(tapName: String, path: String, projectDetails: String, visibility: RepoVisibility) throws -> String { let info = RepoInfo(name: tapName, details: projectDetails, visibility: visibility, canUploadFromNonMainBranch: false) return try GitHubRepoStarter(path: path, shell: shell, repoInfo: info).repoInit() } - /// Creates a new release with one or more archived binaries and returns all asset URLs. - /// - Parameters: - /// - version: The version number for the release. - /// - archivedBinaries: The archived binary files to upload to the release. - /// - releaseNoteInfo: Information for generating release notes. - /// - path: The file path of the repository. - /// - Returns: An array of asset URLs, with the primary asset URL first, followed by additional asset URLs. public func createNewRelease(version: String, archivedBinaries: [ArchivedBinary], releaseNoteInfo: ReleaseNoteInfo, path: String) throws -> [String] { return try createReleaseWithAllBinaries(version: version, archivedBinaries: archivedBinaries, releaseNoteInfo: releaseNoteInfo, path: path) } - /// Verifies if the GitHub CLI (gh) is installed and provides installation instructions if not. public func ghVerification() throws { if try shell.bash("which gh").contains("not found") { throw NnexError.missingGitHubCLI diff --git a/Sources/NnexKit/Models/HomebrewFormula.swift b/Sources/NnexKit/Models/HomebrewFormula.swift new file mode 100644 index 0000000..7709de2 --- /dev/null +++ b/Sources/NnexKit/Models/HomebrewFormula.swift @@ -0,0 +1,51 @@ +// +// HomebrewFormula.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +public struct HomebrewFormula { + public var name: String + public var details: String + public var homepage: String + public var license: String + public var localProjectPath: String + public var uploadType: FormulaUploadType + public var testCommand: TestCommand? + public var extraBuildArgs: [String] + + public init( + name: String, + details: String, + homepage: String, + license: String, + localProjectPath: String, + uploadType: FormulaUploadType, + testCommand: TestCommand?, + extraBuildArgs: [String] + ) { + self.name = name + self.details = details + self.homepage = homepage + self.license = license + self.localProjectPath = localProjectPath + self.uploadType = uploadType + self.testCommand = testCommand + self.extraBuildArgs = extraBuildArgs + } +} + + +// MARK: - Dependencies +public extension HomebrewFormula { + enum FormulaUploadType: String { + case binary + case tarball + } + + enum TestCommand { + case defaultCommand + case custom(String) + } +} diff --git a/Sources/NnexKit/Models/HomebrewTap.swift b/Sources/NnexKit/Models/HomebrewTap.swift new file mode 100644 index 0000000..c2afcb6 --- /dev/null +++ b/Sources/NnexKit/Models/HomebrewTap.swift @@ -0,0 +1,20 @@ +// +// HomebrewTap.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +public struct HomebrewTap { + public var name: String + public var localPath: String + public var remotePath: String + public var formulas: [HomebrewFormula] + + public init(name: String, localPath: String, remotePath: String, formulas: [HomebrewFormula]) { + self.name = name + self.localPath = localPath + self.remotePath = remotePath + self.formulas = formulas + } +} diff --git a/Sources/NnexKit/Releasing/ReleaseStore.swift b/Sources/NnexKit/Releasing/ReleaseStore.swift index d021311..2558465 100644 --- a/Sources/NnexKit/Releasing/ReleaseStore.swift +++ b/Sources/NnexKit/Releasing/ReleaseStore.swift @@ -45,12 +45,13 @@ private extension ReleaseStore { guard VersionHandler.isValidVersionNumber(number) else { throw NnexError.invalidVersionNumber } + return number - case .increment(let part): guard let previousVersion = info.previousVersion else { throw NnexError.noPreviousVersionToIncrement } + return try VersionHandler.incrementVersion( for: part, path: info.projectPath, diff --git a/Sources/NnexKit/Shared/HomebrewTapStore.swift b/Sources/NnexKit/Shared/HomebrewTapStore.swift new file mode 100644 index 0000000..c9027b8 --- /dev/null +++ b/Sources/NnexKit/Shared/HomebrewTapStore.swift @@ -0,0 +1,12 @@ +// +// HomebrewTapStore.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +public protocol HomebrewTapStore { + func loadTaps() throws -> [HomebrewTap] + func updateFormula(_ formula: HomebrewFormula) throws + func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws +} diff --git a/Sources/NnexKit/SwiftData/Adapters/HomebrewTapStoreAdapter.swift b/Sources/NnexKit/SwiftData/Adapters/HomebrewTapStoreAdapter.swift new file mode 100644 index 0000000..173c5ea --- /dev/null +++ b/Sources/NnexKit/SwiftData/Adapters/HomebrewTapStoreAdapter.swift @@ -0,0 +1,65 @@ +// +// HomebrewTapStoreAdapter.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +import Foundation + +public final class HomebrewTapStoreAdapter: HomebrewTapStore { + private let context: NnexContext + + public init(context: NnexContext) { + self.context = context + } + + public func loadTaps() throws -> [HomebrewTap] { + let swiftDataTaps = try context.loadTaps() + + return swiftDataTaps.map(HomebrewTapMapper.toDomain) + } + + public func updateFormula(_ formula: HomebrewFormula) throws { + let swiftDataFormulas = try context.loadFormulas() + guard let target = swiftDataFormulas.first(where: { $0.name == formula.name }) else { + return + } + + target.details = formula.details + target.homepage = formula.homepage + target.license = formula.license + target.localProjectPath = formula.localProjectPath + target.uploadType = CurrentSchema.FormulaUploadType(rawValue: formula.uploadType.rawValue) ?? .binary + target.testCommand = toSwiftDataTestCommand(formula.testCommand) + target.extraBuildArgs = formula.extraBuildArgs + + try context.saveChanges() + } + + public func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws { + let swiftDataTaps = try context.loadTaps() + guard let swiftDataTap = swiftDataTaps.first(where: { $0.name == tap.name }) else { + throw NnexError.missingTap + } + + let swiftDataFormula = HomebrewFormulaMapper.toSwiftData(formula) + + try context.saveNewFormula(swiftDataFormula, in: swiftDataTap) + } +} + + +// MARK: - Helpers +private extension HomebrewTapStoreAdapter { + func toSwiftDataTestCommand(_ testCommand: HomebrewFormula.TestCommand?) -> CurrentSchema.TestCommand? { + guard let testCommand else { return nil } + + switch testCommand { + case .defaultCommand: + return .defaultCommand + case .custom(let command): + return .custom(command) + } + } +} diff --git a/Sources/NnexKit/SwiftData/Mappers/HomebrewFormulaMapper.swift b/Sources/NnexKit/SwiftData/Mappers/HomebrewFormulaMapper.swift new file mode 100644 index 0000000..fe8dc35 --- /dev/null +++ b/Sources/NnexKit/SwiftData/Mappers/HomebrewFormulaMapper.swift @@ -0,0 +1,61 @@ +// +// HomebrewFormulaMapper.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +enum HomebrewFormulaMapper { + static func toDomain(_ formula: SwiftDataHomebrewFormula) -> HomebrewFormula { + return .init( + name: formula.name, + details: formula.details, + homepage: formula.homepage, + license: formula.license, + localProjectPath: formula.localProjectPath, + uploadType: .init(rawValue: formula.uploadType.rawValue) ?? .tarball, + testCommand: toDomainTestCommand(formula.testCommand), + extraBuildArgs: formula.extraBuildArgs + ) + } + + static func toSwiftData(_ formula: HomebrewFormula) -> SwiftDataHomebrewFormula { + return .init( + name: formula.name, + details: formula.details, + homepage: formula.homepage, + license: formula.license, + localProjectPath: formula.localProjectPath, + uploadType: .init(rawValue: formula.uploadType.rawValue) ?? .tarball, + testCommand: toSwiftDataTestCommand(formula.testCommand), + extraBuildArgs: formula.extraBuildArgs + ) + } +} + + +// MARK: - Helpers +private extension HomebrewFormulaMapper { + static func toDomainTestCommand(_ testCommand: CurrentSchema.TestCommand?) -> HomebrewFormula.TestCommand? { + guard let testCommand else { return nil } + + switch testCommand { + case .defaultCommand: + return .defaultCommand + case .custom(let command): + return .custom(command) + } + } + + static func toSwiftDataTestCommand(_ testCommand: HomebrewFormula.TestCommand?) -> CurrentSchema.TestCommand? { + guard let testCommand else { return nil } + + switch testCommand { + case .defaultCommand: + return .defaultCommand + case .custom(let command): + return .custom(command) + } + } +} + diff --git a/Sources/NnexKit/SwiftData/Mappers/HomebrewTapMapper.swift b/Sources/NnexKit/SwiftData/Mappers/HomebrewTapMapper.swift new file mode 100644 index 0000000..2e5248b --- /dev/null +++ b/Sources/NnexKit/SwiftData/Mappers/HomebrewTapMapper.swift @@ -0,0 +1,18 @@ +// +// HomebrewTapMapper.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +enum HomebrewTapMapper { + static func toDomain(_ tap: SwiftDataHomebrewTap) -> HomebrewTap { + let formulas = tap.formulas.map({ HomebrewFormulaMapper.toDomain($0) }) + + return .init(name: tap.name, localPath: tap.localPath, remotePath: tap.remotePath, formulas: formulas) + } + + static func toSwiftData(_ tap: HomebrewTap) -> SwiftDataHomebrewTap { + return .init(name: tap.name, localPath: tap.localPath, remotePath: tap.remotePath) + } +} diff --git a/Sources/NnexKit/SwiftData/Models/SwiftDataHomebrewFormula+Extensions.swift b/Sources/NnexKit/SwiftData/Models/SwiftDataHomebrewFormula+Extensions.swift index 336db88..c9b8491 100644 --- a/Sources/NnexKit/SwiftData/Models/SwiftDataHomebrewFormula+Extensions.swift +++ b/Sources/NnexKit/SwiftData/Models/SwiftDataHomebrewFormula+Extensions.swift @@ -6,9 +6,9 @@ // public extension SwiftDataHomebrewFormula { - /// Initializes a SwiftDataHomebrewFormula instance from a BrewFormula. - /// - Parameter brewFormula: The BrewFormula to convert. - convenience init(from brewFormula: BrewFormula) { + /// Initializes a SwiftDataHomebrewFormula instance from a DecodableFormulaTemplate. + /// - Parameter brewFormula: The DecodableFormulaTemplate to convert. + convenience init(from brewFormula: DecodableFormulaTemplate) { var uploadType = CurrentSchema.FormulaUploadType.binary if let stableURL = brewFormula.versions.stable { diff --git a/Sources/NnexKit/Version/AutoVersionHandler.swift b/Sources/NnexKit/Version/AutoVersionHandler.swift index 6634eba..9e6d0cc 100644 --- a/Sources/NnexKit/Version/AutoVersionHandler.swift +++ b/Sources/NnexKit/Version/AutoVersionHandler.swift @@ -5,13 +5,6 @@ // Created by Nikolai Nobadi on 8/12/25. // -// -// AutoVersionHandler.swift -// nnex -// -// Created by Nikolai Nobadi on 8/12/25. -// - import Foundation public struct AutoVersionHandler { diff --git a/Sources/nnex/Commands/Brew/ImportTap.swift b/Sources/nnex/Commands/Brew/ImportTap.swift index cf57882..71345b7 100644 --- a/Sources/nnex/Commands/Brew/ImportTap.swift +++ b/Sources/nnex/Commands/Brew/ImportTap.swift @@ -62,9 +62,9 @@ private extension Nnex.Brew.ImportTap { } /// Decodes a Homebrew formula from a file. /// - Parameter path: The path to the file containing the formula. - /// - Returns: A BrewFormula instance if decoding is successful, or nil otherwise. + /// - Returns: A DecodableFormulaTemplate instance if decoding is successful, or nil otherwise. /// - Throws: An error if the decoding process fails. - func decodeBrewFormula(at path: String, fileSystem: any FileSystem) throws -> BrewFormula? { + func decodeBrewFormula(at path: String, fileSystem: any FileSystem) throws -> DecodableFormulaTemplate? { let output: String do { output = try makeBrewOutput(filePath: path) @@ -82,7 +82,7 @@ private extension Nnex.Brew.ImportTap { return .init(name: name, desc: desc, homepage: homepage, license: license, versions: .init(stable: nil)) } else if let data = output.data(using: .utf8) { let decoder = JSONDecoder() - let rootObject = try decoder.decode([String: [BrewFormula]].self, from: data) + let rootObject = try decoder.decode([String: [DecodableFormulaTemplate]].self, from: data) return rootObject["formulae"]?.first } diff --git a/Sources/nnex/Commands/Brew/Publish.swift b/Sources/nnex/Commands/Brew/Publish.swift index 9dfddf7..139a518 100644 --- a/Sources/nnex/Commands/Brew/Publish.swift +++ b/Sources/nnex/Commands/Brew/Publish.swift @@ -41,9 +41,17 @@ extension Nnex.Brew { let context = try Nnex.makeContext() let fileSystem = Nnex.makeFileSystem() let folderBrowser = Nnex.makeFolderBrowser(picker: picker, fileSystem: fileSystem) - let buildType = buildType ?? context.loadDefaultBuildType() - let projectFolder = try Nnex.makeFileSystem().getProjectFolder(at: path) - let publishInfoLoader = PublishInfoLoader(shell: shell, picker: picker, projectFolder: projectFolder, context: context, gitHandler: gitHandler, skipTests: skipTests) + let resolvedBuildType = buildType ?? context.loadDefaultBuildType() + let projectFolder = try fileSystem.getProjectFolder(at: path) + let store = HomebrewTapStoreAdapter(context: context) + let publishInfoLoader = PublishInfoLoader( + shell: shell, + picker: picker, + gitHandler: gitHandler, + store: store, + projectFolder: projectFolder, + skipTests: skipTests + ) let manager = PublishExecutionManager( shell: shell, picker: picker, @@ -56,7 +64,7 @@ extension Nnex.Brew { try manager.executePublish( projectFolder: projectFolder, version: version, - buildType: buildType, + buildType: resolvedBuildType, notes: notes, notesFile: notesFile, message: message, diff --git a/Sources/nnex/Managers/PublishExecutionManager.swift b/Sources/nnex/Managers/PublishExecutionManager.swift index 5d39f63..62d843e 100644 --- a/Sources/nnex/Managers/PublishExecutionManager.swift +++ b/Sources/nnex/Managers/PublishExecutionManager.swift @@ -91,7 +91,7 @@ private extension PublishExecutionManager { /// - skipTests: Whether to skip tests during loading. /// - Returns: A tuple containing the tap, formula, and build type. /// - Throws: An error if the tap or formula cannot be found. - func getTapAndFormula(projectFolder: any Directory, buildType: BuildType, skipTests: Bool) throws -> (SwiftDataHomebrewTap, SwiftDataHomebrewFormula, BuildType) { + func getTapAndFormula(projectFolder: any Directory, buildType: BuildType, skipTests: Bool) throws -> (HomebrewTap, HomebrewFormula, BuildType) { let (tap, formula) = try publishInfoLoader.loadPublishInfo() // Note: The formula's localProjectPath update is now handled by PublishInfoLoader if needed @@ -109,6 +109,7 @@ private extension PublishExecutionManager { /// - Throws: An error if the upload fails. func uploadRelease(folder: any Directory, archivedBinaries: [ArchivedBinary], versionInfo: ReleaseVersionInfo, previousVersion: String?, releaseNotesSource: ReleaseNotesSource) throws -> (assetURLs: [String], versionNumber: String) { let handler = ReleaseHandler(picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser) + return try handler.uploadRelease(folder: folder, archivedBinaries: archivedBinaries, versionInfo: versionInfo, previousVersion: previousVersion, releaseNotesSource: releaseNotesSource) } @@ -120,7 +121,7 @@ private extension PublishExecutionManager { /// - message: An optional commit message. /// - tap: The Homebrew tap to publish to. /// - Throws: An error if the formula cannot be published. - func publishFormula(_ content: String, formulaName: String, message: String?, tap: SwiftDataHomebrewTap) throws { + func publishFormula(_ content: String, formulaName: String, message: String?, tap: HomebrewTap) throws { let publisher = FormulaPublisher(gitHandler: gitHandler, fileSystem: fileSystem) let commitMessage = try getMessage(message: message) let formulaPath = try publisher.publishFormula(content, formulaName: formulaName, commitMessage: commitMessage, tapFolderPath: tap.localPath) diff --git a/Sources/nnex/Picker/DisplayablePickerItemConformance.swift b/Sources/nnex/Picker/DisplayablePickerItemConformance.swift index 914d50f..8a61397 100644 --- a/Sources/nnex/Picker/DisplayablePickerItemConformance.swift +++ b/Sources/nnex/Picker/DisplayablePickerItemConformance.swift @@ -8,18 +8,15 @@ import NnexKit import SwiftPickerKit -extension SwiftDataHomebrewTap: DisplayablePickerItem { +// MARK: - HomebrewTap +extension HomebrewTap: DisplayablePickerItem { public var displayName: String { return name } } -extension SwiftDataHomebrewFormula: DisplayablePickerItem { - public var displayName: String { - return name - } -} +// MARK: - BuildType extension BuildType: DisplayablePickerItem { public var displayName: String { switch self { @@ -31,21 +28,23 @@ extension BuildType: DisplayablePickerItem { } } -extension ReleaseNotesHandler.NoteContentType: DisplayablePickerItem { - var displayName: String { + +// MARK: - BuildOutputLocation +extension BuildOutputLocation: DisplayablePickerItem { + public var displayName: String { switch self { - case .direct: - return "Type notes directly" - case .selectFile: - return "Browse and select file" - case .fromPath: - return "Enter path to release notes file" - case .createFile: - return "Create a new file" + case .currentDirectory(let buildType): + return "Current directory (.build/\(buildType.rawValue))" + case .desktop: + return "Desktop" + case .custom: + return "Custom location..." } } } + +// MARK: - FormulaTestType extension FormulaTestType: DisplayablePickerItem { var displayName: String { switch self { @@ -59,15 +58,29 @@ extension FormulaTestType: DisplayablePickerItem { } } -extension BuildOutputLocation: DisplayablePickerItem { +extension SwiftDataHomebrewTap: DisplayablePickerItem { + public var displayName: String { + return name + } +} + +extension SwiftDataHomebrewFormula: DisplayablePickerItem { public var displayName: String { + return name + } +} + +extension ReleaseNotesHandler.NoteContentType: DisplayablePickerItem { + var displayName: String { switch self { - case .currentDirectory(let buildType): - return "Current directory (.build/\(buildType.rawValue))" - case .desktop: - return "Desktop" - case .custom: - return "Custom location..." + case .direct: + return "Type notes directly" + case .selectFile: + return "Browse and select file" + case .fromPath: + return "Enter path to release notes file" + case .createFile: + return "Create a new file" } } } diff --git a/Sources/nnex/Utilities/PublishInfoLoader.swift b/Sources/nnex/Utilities/PublishInfoLoader.swift index 48abcc0..54218f0 100644 --- a/Sources/nnex/Utilities/PublishInfoLoader.swift +++ b/Sources/nnex/Utilities/PublishInfoLoader.swift @@ -10,23 +10,23 @@ import NnexKit struct PublishInfoLoader { private let shell: any NnexShell private let picker: any NnexPicker - private let projectFolder: any Directory private let gitHandler: any GitHandler - private let context: NnexContext + private let store: any HomebrewTapStore + private let projectFolder: any Directory private let skipTests: Bool init( shell: any NnexShell, picker: any NnexPicker, - projectFolder: any Directory, - context: NnexContext, gitHandler: any GitHandler, + store: any HomebrewTapStore, + projectFolder: any Directory, skipTests: Bool ) { + self.store = store self.shell = shell self.picker = picker self.projectFolder = projectFolder - self.context = context self.gitHandler = gitHandler self.skipTests = skipTests } @@ -38,15 +38,14 @@ extension PublishInfoLoader { /// Loads the publishing information, including the selected tap and formula. /// - Returns: A tuple containing the selected tap and formula. /// - Throws: An error if the loading process fails. - func loadPublishInfo() throws -> (SwiftDataHomebrewTap, SwiftDataHomebrewFormula) { - let allTaps = try context.loadTaps() - let tap = try getTap(allTaps: allTaps) ?? picker.requiredSingleSelection("\(projectFolder.name) does not yet have a formula. Select a tap for this formula.", items: allTaps) - - if let formula = tap.formulas.first(where: { $0.name.lowercased() == projectFolder.name.lowercased() }) { + func loadPublishInfo() throws -> (HomebrewTap, HomebrewFormula) { + let allTaps = try store.loadTaps() + let tap = try getTap(allTaps: allTaps) + if var formula = tap.formulas.first(where: { $0.name.matches(projectFolder.name) }) { // Update the formula's localProjectPath if needed if formula.localProjectPath.isEmpty || formula.localProjectPath != projectFolder.path { formula.localProjectPath = projectFolder.path - try context.saveChanges() + try store.updateFormula(formula) } return (tap, formula) } @@ -54,8 +53,8 @@ extension PublishInfoLoader { try picker.requiredPermission(prompt: "Could not find existing formula for \(projectFolder.name.yellow) in \(tap.name).\nWould you like to create a new one?") let newFormula = try createNewFormula(for: projectFolder) - try context.saveNewFormula(newFormula, in: tap) - + try store.saveNewFormula(newFormula, in: tap) + return (tap, newFormula) } } @@ -66,17 +65,21 @@ private extension PublishInfoLoader { /// Retrieves an existing tap matching the project name, if available. /// - Parameter allTaps: An array of available taps. /// - Returns: A SwiftDataHomebrewTap instance if a matching tap is found, or nil otherwise. - func getTap(allTaps: [SwiftDataHomebrewTap]) -> SwiftDataHomebrewTap? { - return allTaps.first { tap in - return tap.formulas.contains(where: { $0.name.lowercased() == projectFolder.name.lowercased() }) + func getTap(allTaps: [HomebrewTap]) throws -> HomebrewTap { + if let tap = allTaps.first(where: { tap in + tap.formulas.contains(where: { $0.name.matches(projectFolder.name) }) + }) { + return tap } + + return try picker.requiredSingleSelection("\(projectFolder.name) does not yet have a formula. Select a tap for this formula.", items: allTaps) } /// Creates a new formula for the given project folder. /// - Parameter folder: The project folder for which to create a formula. /// - Returns: A SwiftDataHomebrewFormula instance representing the created formula. /// - Throws: An error if the creation process fails. - func createNewFormula(for folder: any Directory) throws -> SwiftDataHomebrewFormula { + func createNewFormula(for folder: any Directory) throws -> HomebrewFormula { let name = try getExecutableName() let details = try picker.getRequiredInput(prompt: "Enter the description for this formula.") let homepage = try gitHandler.getRemoteURL(path: folder.path) @@ -90,7 +93,7 @@ private extension PublishInfoLoader { homepage: homepage, license: license, localProjectPath: folder.path, - uploadType: .binary, + uploadType: .tarball, testCommand: testCommand, extraBuildArgs: extraArgs ) @@ -113,7 +116,7 @@ private extension PublishInfoLoader { /// Retrieves the test command based on user input or configuration. /// - Returns: A `TestCommand` instance if tests are to be run, or `nil` if tests are skipped. /// - Throws: An error if the test command cannot be determined. - func getTestCommand() throws -> CurrentSchema.TestCommand? { + func getTestCommand() throws -> HomebrewFormula.TestCommand? { if skipTests { return nil } diff --git a/Tests/NnexKitTests/ProjectBuilderTests.swift b/Tests/NnexKitTests/ProjectBuilderTests.swift index bf2d6da..343ca50 100644 --- a/Tests/NnexKitTests/ProjectBuilderTests.swift +++ b/Tests/NnexKitTests/ProjectBuilderTests.swift @@ -252,7 +252,7 @@ extension ProjectBuilderTests { // MARK: - SUT private extension ProjectBuilderTests { - func makeSUT(buildType: BuildType = .universal, runResults: [String] = [], throwShellError: Bool = false, testCommand: CurrentSchema.TestCommand? = nil, skipClean: Bool = false) -> (sut: ProjectBuilder, shell: MockShell) { + func makeSUT(buildType: BuildType = .universal, runResults: [String] = [], throwShellError: Bool = false, testCommand: HomebrewFormula.TestCommand? = nil, skipClean: Bool = false) -> (sut: ProjectBuilder, shell: MockShell) { let shell = MockShell(results: runResults, shouldThrowErrorOnFinal: throwShellError) let config = BuildConfig( projectName: projectName, diff --git a/Tests/NnexKitTests/PublishUtilitiesTests.swift b/Tests/NnexKitTests/PublishUtilitiesTests.swift index bcd8822..0f19833 100644 --- a/Tests/NnexKitTests/PublishUtilitiesTests.swift +++ b/Tests/NnexKitTests/PublishUtilitiesTests.swift @@ -26,15 +26,14 @@ struct PublishUtilitiesTests { } -// MARK: - buildBinary Tests (using ProjectBuilder directly since it doesn't require SwiftData) +// MARK: - buildBinary Tests extension PublishUtilitiesTests { @Test("Builds binary with universal build type") func buildsBinaryWithUniversalType() throws { let shell = MockShell(results: ["", "", "", sha256Output, sha256Output]) // clean, build arm64, build x86_64, sha256 commands - let config = BuildConfig(projectName: projectName, projectPath: projectPath, buildType: .universal, extraBuildArgs: [], skipClean: false, testCommand: nil) - let builder = ProjectBuilder(shell: shell, config: config) + let formula = makeFormula() - let result = try builder.build() + let result = try PublishUtilities.buildBinary(formula: formula, buildType: .universal, skipTesting: true, shell: shell) // Should build for both architectures #expect(shell.executedCommands.count >= 3) // At least clean, build arm64, build x86_64 @@ -53,10 +52,9 @@ extension PublishUtilitiesTests { func buildsBinaryWithExtraBuildArgs() throws { let extraArgs = ["--verbose", "--enable-testing"] let shell = MockShell(results: ["", "", "", sha256Output, sha256Output]) - let config = BuildConfig(projectName: projectName, projectPath: projectPath, buildType: .universal, extraBuildArgs: extraArgs, skipClean: false, testCommand: nil) - let builder = ProjectBuilder(shell: shell, config: config) + let formula = makeFormula(extraBuildArgs: extraArgs) - _ = try builder.build() + _ = try PublishUtilities.buildBinary(formula: formula, buildType: .universal, skipTesting: true, shell: shell) // Verify extra args are included in build commands let buildCommands = shell.executedCommands.filter { $0.contains("swift build") } @@ -68,10 +66,9 @@ extension PublishUtilitiesTests { @Test("Skips tests when no test command provided") func skipsTestsWhenNoTestCommand() throws { let shell = MockShell(results: ["", "", "", sha256Output, sha256Output]) // No test command result needed - let config = BuildConfig(projectName: projectName, projectPath: projectPath, buildType: .universal, extraBuildArgs: [], skipClean: false, testCommand: nil) - let builder = ProjectBuilder(shell: shell, config: config) + let formula = makeFormula() - _ = try builder.build() + _ = try PublishUtilities.buildBinary(formula: formula, buildType: .universal, skipTesting: false, shell: shell) #expect(!shell.executedCommands.contains { $0.contains("swift test") }) } @@ -79,10 +76,9 @@ extension PublishUtilitiesTests { @Test("Runs tests when test command is provided") func runsTestsWhenTestCommandProvided() throws { let shell = MockShell(results: ["", "", "", "", sha256Output, sha256Output]) // Include test command result - let config = BuildConfig(projectName: projectName, projectPath: projectPath, buildType: .universal, extraBuildArgs: [], skipClean: false, testCommand: .defaultCommand) - let builder = ProjectBuilder(shell: shell, config: config) + let formula = makeFormula(testCommand: .defaultCommand) - _ = try builder.build() + _ = try PublishUtilities.buildBinary(formula: formula, buildType: .universal, skipTesting: false, shell: shell) #expect(shell.executedCommands.contains { $0.contains("swift test") }) } @@ -91,10 +87,9 @@ extension PublishUtilitiesTests { func usesCustomTestCommand() throws { let customTest = "xcodebuild test -scheme testScheme" let shell = MockShell(results: ["", "", "", "", sha256Output, sha256Output]) - let config = BuildConfig(projectName: projectName, projectPath: projectPath, buildType: .universal, extraBuildArgs: [], skipClean: false, testCommand: .custom(customTest)) - let builder = ProjectBuilder(shell: shell, config: config) + let formula = makeFormula(testCommand: .custom(customTest)) - _ = try builder.build() + _ = try PublishUtilities.buildBinary(formula: formula, buildType: .universal, skipTesting: false, shell: shell) #expect(shell.executedCommands.contains { $0.contains(customTest) }) } @@ -166,14 +161,14 @@ extension PublishUtilitiesTests { extension PublishUtilitiesTests { @Test("Creates formula content for single binary") func createsFormulaContentForSingleBinary() throws { - let content = FormulaContentGenerator.makeFormulaFileContent( - name: projectName, - details: details, - homepage: homepage, - license: license, + let formula = makeFormula() + let archives = [makeArchivedBinary(for: .arm, sha: sha256Value)] + + let content = try PublishUtilities.makeFormulaContent( + formula: formula, version: version, - assetURL: assetURL1, - sha256: sha256Value + archivedBinaries: archives, + assetURLs: [assetURL1] ) #expect(content.contains(projectName.capitalized)) @@ -186,16 +181,17 @@ extension PublishUtilitiesTests { @Test("Creates formula content for multiple binaries with ARM and Intel") func createsFormulaContentForMultipleBinaries() throws { - let content = FormulaContentGenerator.makeFormulaFileContent( - name: projectName, - details: details, - homepage: homepage, - license: license, + let formula = makeFormula() + let archives = [ + makeArchivedBinary(for: .arm, sha: "arm_sha256"), + makeArchivedBinary(for: .intel, sha: "intel_sha256") + ] + + let content = try PublishUtilities.makeFormulaContent( + formula: formula, version: version, - armURL: assetURL1, - armSHA256: "arm_sha256", - intelURL: assetURL2, - intelSHA256: "intel_sha256" + archivedBinaries: archives, + assetURLs: [assetURL1, assetURL2] ) #expect(content.contains(projectName.capitalized)) @@ -207,16 +203,14 @@ extension PublishUtilitiesTests { @Test("Creates formula content for ARM-only binary") func createsFormulaContentForArmOnly() throws { - let content = FormulaContentGenerator.makeFormulaFileContent( - name: projectName, - details: details, - homepage: homepage, - license: license, + let formula = makeFormula() + let archives = [makeArchivedBinary(for: .arm, sha: "arm_sha256")] + + let content = try PublishUtilities.makeFormulaContent( + formula: formula, version: version, - armURL: assetURL1, - armSHA256: "arm_sha256", - intelURL: nil, - intelSHA256: nil + archivedBinaries: archives, + assetURLs: [assetURL1] ) #expect(content.contains(projectName.capitalized)) @@ -228,16 +222,14 @@ extension PublishUtilitiesTests { @Test("Creates formula content for Intel-only binary") func createsFormulaContentForIntelOnly() throws { - let content = FormulaContentGenerator.makeFormulaFileContent( - name: projectName, - details: details, - homepage: homepage, - license: license, + let formula = makeFormula() + let archives = [makeArchivedBinary(for: .intel, sha: "intel_sha256")] + + let content = try PublishUtilities.makeFormulaContent( + formula: formula, version: version, - armURL: nil, - armSHA256: nil, - intelURL: assetURL1, - intelSHA256: "intel_sha256" + archivedBinaries: archives, + assetURLs: [assetURL1] ) #expect(content.contains(projectName.capitalized)) @@ -249,28 +241,27 @@ extension PublishUtilitiesTests { } -// MARK: - Mock Types -private struct MockFormula { - let name: String - let details: String - let homepage: String - let license: String - let localProjectPath: String - let testCommand: CurrentSchema.TestCommand? - let extraBuildArgs: [String] -} - // MARK: - Private Helpers private extension PublishUtilitiesTests { - func makeFormula(testCommand: CurrentSchema.TestCommand? = nil, extraBuildArgs: [String] = []) -> MockFormula { - return MockFormula( + func makeFormula(testCommand: HomebrewFormula.TestCommand? = nil, extraBuildArgs: [String] = []) -> HomebrewFormula { + return .init( name: projectName, details: details, homepage: homepage, license: license, localProjectPath: projectPath, + uploadType: .binary, testCommand: testCommand, extraBuildArgs: extraBuildArgs ) } + + func makeArchivedBinary(for architecture: ReleaseArchitecture, sha: String) -> ArchivedBinary { + let originalPath = "\(projectPath).build/\(architecture.name)-apple-macosx/release/\(projectName)" + return ArchivedBinary( + originalPath: originalPath, + archivePath: "\(originalPath).tar.gz", + sha256: sha + ) + } } diff --git a/Tests/nnexTests/BuildTests/BuildExecutableTests.swift b/Tests/nnexTests/BuildTests/BuildExecutableTests.swift index 242aae7..2748f42 100644 --- a/Tests/nnexTests/BuildTests/BuildExecutableTests.swift +++ b/Tests/nnexTests/BuildTests/BuildExecutableTests.swift @@ -1,3 +1,4 @@ +// // BuildTests.swift // nnex // diff --git a/Tests/nnexTests/Domain/Execution/BuildExecutionManagerTests.swift b/Tests/nnexTests/BuildTests/BuildExecutionManagerTests.swift similarity index 100% rename from Tests/nnexTests/Domain/Execution/BuildExecutionManagerTests.swift rename to Tests/nnexTests/BuildTests/BuildExecutionManagerTests.swift diff --git a/Tests/nnexTests/Domain/Handlers/ReleaseHandlerTests.swift b/Tests/nnexTests/Domain/Handlers/ReleaseHandlerTests.swift deleted file mode 100644 index ea1918d..0000000 --- a/Tests/nnexTests/Domain/Handlers/ReleaseHandlerTests.swift +++ /dev/null @@ -1,457 +0,0 @@ -//// -//// ReleaseHandlerTests.swift -//// nnex -//// -//// Created by Claude Code on 8/10/25. -//// -// -//import Testing -//import NnexKit -//import Foundation -//import GitCommandGen -//import NnexSharedTestHelpers -//@testable import nnex -//@preconcurrency import Files -// -//struct ReleaseHandlerTests { -// private let testProjectName = "TestProject" -// private let testBinaryPath = "/path/to/binary" -// private let testBinarySha256 = "abc123def456" -// private let testAssetURL = "https://github.com/test/repo/releases/download/v1.0.0/binary" -// private let testPreviousVersion = "v0.9.0" -// private let testVersionNumber = "1.0.0" -// private let testReleaseNotes = "Test release notes content" -// private let testReleaseNotesFile = "/path/to/notes.md" -//} -// -// -//// MARK: - Unit Tests -//extension ReleaseHandlerTests { -// @Test("Uploads release successfully with provided version info") -// func uploadsReleaseSuccessfullyWithProvidedVersionInfo() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, _, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseVersion == testVersionNumber) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == false) -// } -// -// @Test("Resolves version with increment when no version provided") -// func resolvesVersionWithIncrementWhenNoVersionProvided() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// inputResponses: ["minor"] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) -// } -// -// @Test("Resolves version with new number when no previous version exists") -// func resolvesVersionWithNewNumberWhenNoPreviousVersionExists() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: nil, -// inputResponses: [testVersionNumber] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseVersion == testVersionNumber) -// } -// -// @Test("Uses release notes from file when notesFile provided") -// func usesReleaseNotesFromFileWhenNotesFileProvided() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) -// -// let (sut, folder, gitHandler, _, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotesFile) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == true) -// } -// -// @Test("Uses direct release notes when notes provided") -// func usesDirectReleaseNotesWhenNotesProvided() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, _, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == false) -// } -// -// @Test("Falls back to ReleaseNotesHandler when no notes provided") -// func fallsBackToReleaseNotesHandlerWhenNoNotesProvided() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: nil) -// -// let (sut, folder, gitHandler, _, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// inputResponses: [testReleaseNotes], -// selectedIndices: [0] // Direct input option -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == false) -// } -// -// @Test("Handles version input with increment keyword") -// func handlesVersionInputWithIncrementKeyword() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// inputResponses: ["patch"] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// } -// -// @Test("Handles version input with specific version number") -// func handlesVersionInputWithSpecificVersionNumber() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// inputResponses: ["2.1.0"] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseVersion == "2.1.0") -// } -// -// @Test("Shows previous version in prompt when available") -// func showsPreviousVersionInPromptWhenAvailable() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// inputResponses: ["1.5.0"] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseVersion == "1.5.0") -// } -// -// @Test("Shows default version format when no previous version") -// func showsDefaultVersionFormatWhenNoPreviousVersion() throws { -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: nil, -// inputResponses: ["1.0.0"] -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// let (resolvedVersion, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: resolvedVersion, -// previousVersion: previousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseVersion == "1.0.0") -// } -// -// @Test("Throws error when git handler fails") -// func throwsErrorWhenGitHandlerFails() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, _, _, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// shouldThrowGitError: true -// ) -// -// #expect(throws: (any Error).self) { -// _ = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// } -// } -// -// @Test("Throws error when picker input fails") -// func throwsErrorWhenPickerInputFails() throws { -// let (_, folder, gitHandler, picker, _) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// shouldThrowPickerError: true -// ) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler) -// -// #expect(throws: (any Error).self) { -// try versionHandler.resolveVersionInfo(versionInfo: nil, projectPath: folder.path) -// } -// } -// -// @Test("Moves release notes file to trash when user confirms") -// func movesReleaseNotesFileToTrashWhenUserConfirms() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) -// -// let (sut, folder, gitHandler, _, trashHandler) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// permissionResponses: [true] -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotesFile) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == true) -// #expect(trashHandler.moveToTrashCalled == true) -// #expect(trashHandler.lastMovedPath == testReleaseNotesFile) -// } -// -// @Test("Does not move release notes file to trash when user declines") -// func doesNotMoveReleaseNotesFileToTrashWhenUserDeclines() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) -// -// let (sut, folder, gitHandler, _, trashHandler) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// permissionResponses: [false] -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotesFile) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == true) -// #expect(trashHandler.moveToTrashCalled == false) -// #expect(trashHandler.lastMovedPath == nil) -// } -// -// @Test("Does not attempt to move to trash when notes are inline") -// func doesNotAttemptToMoveToTrashWhenNotesAreInline() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) -// -// let (sut, folder, gitHandler, _, trashHandler) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// permissionResponses: [true] // This shouldn't matter since no file is involved -// ) -// -// let results = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// -// #expect(results.assetURLs.first == testAssetURL) -// #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) -// #expect(gitHandler.releaseNoteInfo?.isFromFile == false) -// #expect(trashHandler.moveToTrashCalled == false) -// #expect(trashHandler.lastMovedPath == nil) -// } -// -// @Test("Handles trash operation failure gracefully") -// func handlesTrashOperationFailureGracefully() throws { -// let versionInfo = ReleaseVersionInfo.version(testVersionNumber) -// let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) -// -// let (sut, folder, _, _, trashHandler) = try makeSUT( -// assetURL: testAssetURL, -// previousVersion: testPreviousVersion, -// permissionResponses: [true], -// shouldThrowTrashError: true -// ) -// -// // Even if trash fails, the release should still succeed -// #expect(throws: (any Error).self) { -// _ = try sut.uploadRelease( -// folder: folder, -// archivedBinaries: makeArchivedBinaries(), -// versionInfo: versionInfo, -// previousVersion: testPreviousVersion, -// releaseNotesSource: releaseNotesSource -// ) -// } -// -// #expect(trashHandler.moveToTrashCalled == true) -// #expect(trashHandler.lastMovedPath == testReleaseNotesFile) -// } -//} -// -// -//// MARK: - SUT -//private extension ReleaseHandlerTests { -// func makeSUT( -// assetURL: String = "", -// previousVersion: String? = nil, -// inputResponses: [String] = [], -// selectedIndices: [Int] = [], -// permissionResponses: [Bool] = [], -// shouldThrowGitError: Bool = false, -// shouldThrowPickerError: Bool = false, -// shouldThrowTrashError: Bool = false -// ) throws -> (sut: ReleaseHandler, folder: Folder, gitHandler: MockGitHandler, picker: MockPicker, trashHandler: MockTrashHandler) { -// -// let gitHandler = MockGitHandler( -// previousVersion: previousVersion ?? "", -// assetURL: assetURL, -// throwError: shouldThrowGitError -// ) -// -// let picker = MockPicker( -// selectedItemIndices: selectedIndices, -// inputResponses: inputResponses, -// permissionResponses: permissionResponses, -// shouldThrowError: shouldThrowPickerError -// ) -// -// let trashHandler = MockTrashHandler() -// trashHandler.shouldThrowError = shouldThrowTrashError -// -// let sut = ReleaseHandler(picker: picker, gitHandler: gitHandler, trashHandler: trashHandler) -// let tempFolder = try Folder.temporary.createSubfolder(named: "ReleaseHandlerTest-\(UUID().uuidString)") -// let folder = try tempFolder.createSubfolder(named: testProjectName) -// -// return (sut, folder, gitHandler, picker, trashHandler) -// } -// -// func makeArchivedBinaries() -> [ArchivedBinary] { -// return [ArchivedBinary(originalPath: testBinaryPath, archivePath: "/tmp/test.tar.gz", sha256: testBinarySha256)] -// } -//} diff --git a/Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerIntegrationTests.swift b/Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerIntegrationTests.swift deleted file mode 100644 index f8a64d1..0000000 --- a/Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerIntegrationTests.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// ReleaseVersionHandlerIntegrationTests.swift -// nnex -// -// Created by Nikolai Nobadi on 8/16/25. -// - -import Testing -import Foundation -import NnShellTesting -import SwiftPickerTesting -import NnexSharedTestHelpers -@testable import nnex -@preconcurrency import Files - -final class ReleaseVersionHandlerIntegrationTests { - private let oldVersion: String - private let newVersion = "2.0.0" - private let projectFolder: Folder - private let projectName = "TestProject" - private let mainCommandFilePath: String - - init() throws { - oldVersion = "1.0.0" - projectFolder = try Folder.temporary.createSubfolder(named: "AutoVersionHandler-\(UUID().uuidString)") - mainCommandFilePath = try createMockCommandFile(previousVersion: oldVersion, projectFolder: projectFolder) - } - - deinit { - deleteFolderContents(projectFolder) - } -} - - -// MARK: - Tests -extension ReleaseVersionHandlerIntegrationTests { - @Test("Updates source code version if it exists", .disabled()) // TODO: - - func updatesExistingVersionInSource() throws { - let sut = makeSUT().sut - let _ = try sut.resolveVersionInfo(versionInfo: .version(newVersion), projectPath: projectFolder.path) - let updatedFile = try File(path: mainCommandFilePath) - let contents = try updatedFile.readAsString() - - #expect(contents.contains(newVersion), "File should contain version 2.0.0") - } - - @Test("Commits changes to source code when updating version number in executable file", .disabled()) // TODO: - - func commitsNewVersionInSource() throws { - let (sut, gitHandler) = makeSUT() - let _ = try sut.resolveVersionInfo(versionInfo: .version(newVersion), projectPath: projectFolder.path) - let message = try #require(gitHandler.message) - let expectedMessage = "Update version to \(newVersion)" - - #expect(message == expectedMessage) - } -} - - -// MARK: - SUT -private extension ReleaseVersionHandlerIntegrationTests { - func makeSUT(previousVersion: String? = nil) -> (sut: ReleaseVersionHandler, gitHandler: MockGitHandler) { - let shell = MockShell() - let picker = MockSwiftPicker(permissionResult: .init(defaultValue: true)) - let fileSystem = MockFileSystem() - let gitHandler = makeGitHandler(previousVersion: previousVersion, throwError: false) - let sut = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler, shell: shell, fileSystem: fileSystem) - - return (sut, gitHandler) - } - - func makeGitHandler(previousVersion: String?, throwError: Bool) -> MockGitHandler { - if let previousVersion { - return .init(previousVersion: previousVersion, throwError: throwError) - } - - return .init(previousVersion: "", throwError: throwError) - } -} - - -// MARK: - Private Helpers -private func createMockCommandFile(previousVersion: String, projectFolder: Folder) throws -> String { - let fileContents = """ - import ArgumentParser - - @main - struct MockCommand: ParsableCommand { - static let configuration = CommandConfiguration( - abstract: "", - version: "\(previousVersion)", - ) - } - """ - - let file = try projectFolder.createSubfolderIfNeeded(withName: "Sources").createFile(named: "MockCommand.swift") - try file.write(fileContents) - - return file.path -} diff --git a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift new file mode 100644 index 0000000..b3e5a6e --- /dev/null +++ b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift @@ -0,0 +1,188 @@ +// +// ReleaseHandlerTests.swift +// nnex +// +// Created by Nikolai Nobadi on 8/10/25. +// + +import Testing +import NnexKit +import Foundation +import GitCommandGen +import NnShellTesting +import SwiftPickerTesting +import NnexSharedTestHelpers +@testable import nnex +@preconcurrency import Files + +struct ReleaseHandlerTests { + private let testProjectPath = "/Users/test/TestProject" + private let testProjectName = "TestProject" + private let testBinaryPath = "/path/to/binary" + private let testArchivePath = "/path/to/binary.tar.gz" + private let testBinarySha256 = "abc123def456" + private let testAssetURL = "https://github.com/test/repo/releases/download/v1.0.0/binary" + private let testPreviousVersion = "v0.9.0" + private let testVersionNumber = "1.0.0" + private let testReleaseNotes = "Test release notes content" + private let testReleaseNotesFile = "/path/to/notes.md" +} + + +// MARK: - Upload Release with Direct Notes +extension ReleaseHandlerTests { + @Test("Uploads release with direct notes string") + func uploadsReleaseWithDirectNotes() throws { + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (assetURLs, versionNumber) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(assetURLs == [testAssetURL]) + #expect(versionNumber == testVersionNumber) + #expect(gitHandler.releaseVersion == testVersionNumber) + #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotes) + #expect(gitHandler.releaseNoteInfo?.isFromFile == false) + } + + @Test("Uploads release with notes file path") + func uploadsReleaseWithNotesFile() throws { + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) + + let (assetURLs, versionNumber) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(assetURLs == [testAssetURL]) + #expect(versionNumber == testVersionNumber) + #expect(gitHandler.releaseNoteInfo?.content == testReleaseNotesFile) + #expect(gitHandler.releaseNoteInfo?.isFromFile == true) + } +} + + +// MARK: - Multiple Binaries +extension ReleaseHandlerTests { + @Test("Uploads release with single binary") + func uploadsReleaseWithSingleBinary() throws { + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (assetURLs, _) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(assetURLs.count == 1) + #expect(gitHandler.releaseVersion == testVersionNumber) + } + + @Test("Uploads release with multiple binaries") + func uploadsReleaseWithMultipleBinaries() throws { + let assetURL1 = "\(testAssetURL)-1" + let (sut, folder, gitHandler, _) = makeSUT(assetURL: assetURL1) + let binary1 = makeArchivedBinary(originalPath: "/path/to/binary1") + let binary2 = makeArchivedBinary(originalPath: "/path/to/binary2") + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (assetURLs, _) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary1, binary2], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(assetURLs.count == 2) + #expect(assetURLs[0] == assetURL1) + #expect(assetURLs[1].contains("additional")) + #expect(gitHandler.releaseVersion == testVersionNumber) + } +} + + +// MARK: - Trash Release Notes +extension ReleaseHandlerTests { + @Test("Trashes release notes file when user confirms") + func trashesNotesFileWhenConfirmed() throws { + let (sut, folder, _, fileSystem) = makeSUT(assetURL: testAssetURL, permissionResult: true) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) + + _ = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(fileSystem.pathToMoveToTrash == testReleaseNotesFile) + } + + @Test("Does not trash notes file when user declines") + func doesNotTrashNotesFileWhenDeclined() throws { + let (sut, folder, _, fileSystem) = makeSUT(assetURL: testAssetURL, permissionResult: false) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: nil, notesFile: testReleaseNotesFile) + + _ = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(fileSystem.pathToMoveToTrash == nil) + } + + @Test("Does not prompt to trash notes when notes are from string") + func doesNotPromptToTrashWhenNotesFromString() throws { + let (sut, folder, _, fileSystem) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(testVersionNumber) + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + _ = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(fileSystem.pathToMoveToTrash == nil) + } +} + + +// MARK: - Version Handling +extension ReleaseHandlerTests { + @Test("Extracts version string from version case") + func extractsVersionFromVersionCase() throws { + let releaseNumber = "v2.5.0" + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version(releaseNumber) + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (_, versionNumber) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(versionNumber == releaseNumber) + #expect(gitHandler.releaseVersion == releaseNumber) + } + + @Test("Handles version string without v prefix") + func handlesVersionWithoutVPrefix() throws { + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version("3.0.1") + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (_, versionNumber) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(versionNumber == "3.0.1") + #expect(gitHandler.releaseVersion == "3.0.1") + } +} + + +// MARK: - SUT +private extension ReleaseHandlerTests { + func makeSUT(assetURL: String = "", permissionResult: Bool = true) -> (sut: ReleaseHandler, folder: any Directory, gitHandler: MockGitHandler, fileSystem: MockFileSystem) { + let folder = MockDirectory(path: testProjectPath) + let picker = MockSwiftPicker(permissionResult: .init(defaultValue: permissionResult, type: .ordered([]))) + let gitHandler = MockGitHandler(assetURL: assetURL) + let fileSystem = MockFileSystem() + let folderBrowser = MockDirectoryBrowser(filePathToReturn: nil, directoryToReturn: nil) + let sut = ReleaseHandler(picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser) + + return (sut, folder, gitHandler, fileSystem) + } + + func makeArchivedBinary(originalPath: String? = nil, archivePath: String? = nil, sha256: String? = nil) -> ArchivedBinary { + return .init(originalPath: originalPath ?? testBinaryPath, archivePath: archivePath ?? testArchivePath, sha256: sha256 ?? testBinarySha256) + } +} diff --git a/Tests/nnexTests/Domain/Utilities/ReleaseNotesFileUtilityTests.swift b/Tests/nnexTests/HandlerTests/ReleaseNotesFileUtilityTests.swift similarity index 98% rename from Tests/nnexTests/Domain/Utilities/ReleaseNotesFileUtilityTests.swift rename to Tests/nnexTests/HandlerTests/ReleaseNotesFileUtilityTests.swift index 44adf22..8c1ec82 100644 --- a/Tests/nnexTests/Domain/Utilities/ReleaseNotesFileUtilityTests.swift +++ b/Tests/nnexTests/HandlerTests/ReleaseNotesFileUtilityTests.swift @@ -5,13 +5,6 @@ // Created by Nikolai Nobadi on 9/9/25. // -// -// ReleaseNotesFileUtilityTests.swift -// nnex -// -// Created by Nikolai Nobadi on 9/9/25. -// - import Testing import Foundation import SwiftPickerTesting diff --git a/Tests/nnexTests/Domain/Handlers/ReleaseNotesHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseNotesHandlerTests.swift similarity index 100% rename from Tests/nnexTests/Domain/Handlers/ReleaseNotesHandlerTests.swift rename to Tests/nnexTests/HandlerTests/ReleaseNotesHandlerTests.swift diff --git a/Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseVersionHandlerTests.swift similarity index 100% rename from Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerTests.swift rename to Tests/nnexTests/HandlerTests/ReleaseVersionHandlerTests.swift diff --git a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift index ff49513..07e5c4a 100644 --- a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift +++ b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift @@ -57,23 +57,29 @@ extension PublishExecutionManagerTests { ] ) - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - let existingFormula = SwiftDataHomebrewFormula( - name: executableName, - details: "Test formula", - homepage: "https://github.com/test/repo", - license: "MIT", - localProjectPath: projectFolder.path, - uploadType: .binary, - testCommand: nil, - extraBuildArgs: [] + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap( + name: tapName, + localPath: tapFolder.path, + remotePath: "", + formulas: [ + HomebrewFormula( + name: executableName, + details: "Test formula", + homepage: "https://github.com/test/repo", + license: "MIT", + localProjectPath: projectFolder.path, + uploadType: .binary, + testCommand: nil, + extraBuildArgs: [] + ) + ] + ) + ] ) - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) + let sut = try makeSUT(factory: factory, store: store) try sut.executePublish( projectFolder: FilesDirectoryAdapter(folder: projectFolder), @@ -113,12 +119,13 @@ extension PublishExecutionManagerTests { ] ) - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - - try context.saveNewTap(existingTap) + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "", formulas: []) + ] + ) - let sut = try makeSUT(factory: factory, context: context) + let sut = try makeSUT(factory: factory, store: store) try sut.executePublish( projectFolder: FilesDirectoryAdapter(folder: projectFolder), @@ -156,23 +163,29 @@ extension PublishExecutionManagerTests { permissionResponses: [true] // Commit and push to GitHub ) - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - let existingFormula = SwiftDataHomebrewFormula( - name: executableName, - details: "Test formula", - homepage: "https://github.com/test/repo", - license: "MIT", - localProjectPath: projectFolder.path, - uploadType: .binary, - testCommand: nil, - extraBuildArgs: [] + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap( + name: tapName, + localPath: tapFolder.path, + remotePath: "", + formulas: [ + HomebrewFormula( + name: executableName, + details: "Test formula", + homepage: "https://github.com/test/repo", + license: "MIT", + localProjectPath: projectFolder.path, + uploadType: .binary, + testCommand: nil, + extraBuildArgs: [] + ) + ] + ) + ] ) - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) + let sut = try makeSUT(factory: factory, store: store) try sut.executePublish( projectFolder: FilesDirectoryAdapter(folder: projectFolder), @@ -210,23 +223,29 @@ extension PublishExecutionManagerTests { ] ) - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - let existingFormula = SwiftDataHomebrewFormula( - name: executableName, - details: "Test formula", - homepage: "https://github.com/test/repo", - license: "MIT", - localProjectPath: projectFolder.path, - uploadType: .binary, - testCommand: nil, - extraBuildArgs: [] + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap( + name: tapName, + localPath: tapFolder.path, + remotePath: "", + formulas: [ + HomebrewFormula( + name: executableName, + details: "Test formula", + homepage: "https://github.com/test/repo", + license: "MIT", + localProjectPath: projectFolder.path, + uploadType: .binary, + testCommand: nil, + extraBuildArgs: [] + ) + ] + ) + ] ) - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) + let sut = try makeSUT(factory: factory, store: store) try sut.executePublish( projectFolder: FilesDirectoryAdapter(folder: projectFolder), @@ -257,8 +276,8 @@ extension PublishExecutionManagerTests { ) let folder = projectFolder - let context = try factory.makeContext() - let sut = try makeSUT(factory: factory, context: context) + let store = MockHomebrewTapStore(taps: []) + let sut = try makeSUT(factory: factory, store: store) #expect(throws: PublishExecutionError.uncommittedChanges) { try sut.executePublish( @@ -282,8 +301,8 @@ extension PublishExecutionManagerTests { ) let folder = FilesDirectoryAdapter(folder: projectFolder) - let context = try factory.makeContext() - let sut = try makeSUT(factory: factory, context: context) + let store = MockHomebrewTapStore(taps: []) + let sut = try makeSUT(factory: factory, store: store) #expect(throws: (any Error).self) { try sut.executePublish( @@ -303,24 +322,30 @@ extension PublishExecutionManagerTests { try createPackageSwift() let factory = MockContextFactory(shell: MockShell(shouldThrowErrorOnFinal: true)) - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - let existingFormula = SwiftDataHomebrewFormula( - name: executableName, - details: "Test formula", - homepage: "https://github.com/test/repo", - license: "MIT", - localProjectPath: projectFolder.path, - uploadType: .binary, - testCommand: nil, - extraBuildArgs: [] + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap( + name: tapName, + localPath: tapFolder.path, + remotePath: "", + formulas: [ + HomebrewFormula( + name: executableName, + details: "Test formula", + homepage: "https://github.com/test/repo", + license: "MIT", + localProjectPath: projectFolder.path, + uploadType: .binary, + testCommand: nil, + extraBuildArgs: [] + ) + ] + ) + ] ) - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - let folder = projectFolder - let sut = try makeSUT(factory: factory, context: context) + let sut = try makeSUT(factory: factory, store: store) #expect(throws: (any Error).self) { try sut.executePublish( @@ -339,14 +364,21 @@ extension PublishExecutionManagerTests { // MARK: - Private Methods private extension PublishExecutionManagerTests { - func makeSUT(factory: MockContextFactory, context: NnexContext) throws -> PublishExecutionManager { + func makeSUT(factory: MockContextFactory, store: MockHomebrewTapStore) throws -> PublishExecutionManager { let shell = factory.makeShell() let picker = factory.makePicker() let gitHandler = factory.makeGitHandler() let fileSystem = factory.makeFileSystem() let folderBrowser = factory.makeFolderBrowser(picker: picker, fileSystem: fileSystem) let folderAdapter = FilesDirectoryAdapter(folder: projectFolder) - let publishInfoLoader = PublishInfoLoader(shell: shell, picker: picker, projectFolder: folderAdapter, context: context, gitHandler: gitHandler, skipTests: true) + let publishInfoLoader = PublishInfoLoader( + shell: shell, + picker: picker, + gitHandler: gitHandler, + store: store, + projectFolder: folderAdapter, + skipTests: true + ) return .init(shell: shell, picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser, publishInfoLoader: publishInfoLoader) } diff --git a/Tests/nnexTests/PublishTests/PublishInfoLoaderTests.swift b/Tests/nnexTests/PublishTests/PublishInfoLoaderTests.swift index a438eb5..5d6ce43 100644 --- a/Tests/nnexTests/PublishTests/PublishInfoLoaderTests.swift +++ b/Tests/nnexTests/PublishTests/PublishInfoLoaderTests.swift @@ -29,14 +29,12 @@ final class PublishInfoLoaderTests: BasePublishTestSuite { extension PublishInfoLoaderTests { @Test("Creates new formula when project has no existing formula") func createsNewFormula() throws { - let factory = MockContextFactory() - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - - try context.saveNewTap(existingTap) + let store = MockHomebrewTapStore(taps: [ + HomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "", formulas: []) + ]) let sut = try makeSUT( - context: context, + store: store, inputResponses: ["Test formula description"], permissionResponses: [true], selectedItemIndices: [0, 2] // Index 0 for tap selection, index 2 for FormulaTestType.noTests @@ -45,19 +43,18 @@ extension PublishInfoLoaderTests { try createPackageSwift() let (tap, formula) = try sut.loadPublishInfo() + let newFormulaData = try #require(store.newFormulaData) #expect(tap.name == tapName) #expect(formula.name == projectName) + #expect(newFormulaData.tap.name.matches(tap.name)) + #expect(newFormulaData.formula.name.matches(formula.name)) } @Test("Updates formula localProjectPath when it doesn't match current project folder") func updatesFormulaProjectPath() throws { - let factory = MockContextFactory() - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - // Create a formula with a different project path - let existingFormula = SwiftDataHomebrewFormula( + let existingFormula = HomebrewFormula( name: projectName, details: "Test formula", homepage: "https://github.com/test/test", @@ -68,9 +65,11 @@ extension PublishInfoLoaderTests { extraBuildArgs: [] ) - try context.saveNewTap(existingTap, formulas: [existingFormula]) + let store = MockHomebrewTapStore(taps: [ + HomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "", formulas: [existingFormula]) + ]) - let sut = try makeSUT(context: context) + let sut = try makeSUT(store: store) // Create Package.swift file try createPackageSwift() @@ -80,21 +79,13 @@ extension PublishInfoLoaderTests { #expect(tap.name == tapName) #expect(formula.name == projectName) #expect(formula.localProjectPath == projectFolder.path) // Should be updated to current project path + #expect(store.formulaToUpdate?.localProjectPath == projectFolder.path) } @Test("Preserves formula localProjectPath when it matches current project folder") func preservesMatchingProjectPath() throws { - let factory = MockContextFactory() - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - - let sut = try makeSUT(context: context) - - // Create Package.swift file - try createPackageSwift() - // Create a formula with the same project path - let existingFormula = SwiftDataHomebrewFormula( + let existingFormula = HomebrewFormula( name: projectName, details: "Test formula", homepage: "https://github.com/test/test", @@ -105,33 +96,41 @@ extension PublishInfoLoaderTests { extraBuildArgs: [] ) - try context.saveNewTap(existingTap, formulas: [existingFormula]) + let store = MockHomebrewTapStore(taps: [ + HomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "", formulas: [existingFormula]) + ]) + let sut = try makeSUT(store: store) + + // Create Package.swift file + try createPackageSwift() let (tap, formula) = try sut.loadPublishInfo() #expect(tap.name == tapName) #expect(formula.name == projectName) #expect(formula.localProjectPath == projectFolder.path) // Should remain the same + #expect(store.formulaToUpdate == nil) } } // MARK: - SUT private extension PublishInfoLoaderTests { - func makeSUT(context: NnexContext, skipTests: Bool = false, inputResponses: [String] = [], permissionResponses: [Bool] = [], selectedItemIndices: [Int] = []) throws -> PublishInfoLoader { + func makeSUT(store: MockHomebrewTapStore, skipTests: Bool = false, inputResponses: [String] = [], permissionResponses: [Bool] = [], selectedItemIndices: [Int] = []) throws -> PublishInfoLoader { let shell = MockShell() let gitHandler = MockGitHandler() let picker = MockSwiftPicker( inputResult: .init(type: .ordered(inputResponses)), permissionResult: .init(type: .ordered(permissionResponses)), selectionResult: .init(singleType: .ordered(selectedItemIndices.map({ .index($0) }))) - ) + ) + let folderAdapter = FilesDirectoryAdapter(folder: projectFolder) let sut = PublishInfoLoader( shell: shell, picker: picker, - projectFolder: FilesDirectoryAdapter(folder: projectFolder), - context: context, gitHandler: gitHandler, + store: store, + projectFolder: folderAdapter, skipTests: skipTests ) diff --git a/Tests/nnexTests/Shared/MockDirectoryBrowser.swift b/Tests/nnexTests/Shared/MockDirectoryBrowser.swift new file mode 100644 index 0000000..7496c85 --- /dev/null +++ b/Tests/nnexTests/Shared/MockDirectoryBrowser.swift @@ -0,0 +1,36 @@ +// +// MockDirectoryBrowser.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +import NnexKit +import Foundation +@testable import nnex + +final class MockDirectoryBrowser: DirectoryBrowser { + private let filePathToReturn: String? + private let directoryToReturn: (any Directory)? + + init(filePathToReturn: String?, directoryToReturn: (any Directory)?) { + self.filePathToReturn = filePathToReturn + self.directoryToReturn = directoryToReturn + } + + func browseForFile(prompt: String) throws -> String { + guard let filePathToReturn else { + throw NSError(domain: "Test", code: 0) + } + + return filePathToReturn + } + + func browseForDirectory(prompt: String) throws -> any Directory { + guard let directoryToReturn else { + throw NSError(domain: "Test", code: 0) + } + + return directoryToReturn + } +} diff --git a/Tests/nnexTests/Shared/MockHomebrewTapStore.swift b/Tests/nnexTests/Shared/MockHomebrewTapStore.swift new file mode 100644 index 0000000..4178160 --- /dev/null +++ b/Tests/nnexTests/Shared/MockHomebrewTapStore.swift @@ -0,0 +1,32 @@ +// +// MockHomebrewTapStore.swift +// nnex +// +// Created by Nikolai Nobadi on 12/11/25. +// + +import NnexKit +@testable import nnex + +final class MockHomebrewTapStore: HomebrewTapStore { + private let tapsToLoad: [HomebrewTap] + + private(set) var formulaToUpdate: HomebrewFormula? + private(set) var newFormulaData: (formula: HomebrewFormula, tap: HomebrewTap)? + + init(taps: [HomebrewTap]) { + self.tapsToLoad = taps + } + + func loadTaps() throws -> [HomebrewTap] { + return tapsToLoad + } + + func updateFormula(_ formula: HomebrewFormula) throws { + formulaToUpdate = formula + } + + func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws { + newFormulaData = (formula, tap) + } +}