From e9a31a324f1bf3fb236f608e0d7cfa96dba43a33 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 11:46:09 -0800 Subject: [PATCH 01/15] add new domain data models (non SwiftData) --- Sources/NnexKit/Models/HomebrewFormula.swift | 51 ++++++++++++++++++++ Sources/NnexKit/Models/HomebrewTap.swift | 20 ++++++++ 2 files changed, 71 insertions(+) create mode 100644 Sources/NnexKit/Models/HomebrewFormula.swift create mode 100644 Sources/NnexKit/Models/HomebrewTap.swift 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 + } +} From fff2bf4b7dba08d0d6bec9237b2f4894027614f8 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 11:51:43 -0800 Subject: [PATCH 02/15] add data mappers for domain models --- .../Mappers/HomebrewFormulaMapper.swift | 61 +++++++++++++++++++ .../SwiftData/Mappers/HomebrewTapMapper.swift | 18 ++++++ 2 files changed, 79 insertions(+) create mode 100644 Sources/NnexKit/SwiftData/Mappers/HomebrewFormulaMapper.swift create mode 100644 Sources/NnexKit/SwiftData/Mappers/HomebrewTapMapper.swift 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) + } +} From abeb5ae2e7c38fd7f7f3896b95849a9fed88a4f8 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 11:53:44 -0800 Subject: [PATCH 03/15] minor reformatting --- Sources/NnexKit/Git/DefaultGitHandler.swift | 27 ------------------- .../NnexKit/Version/AutoVersionHandler.swift | 7 ----- 2 files changed, 34 deletions(-) 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/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 { From 8d027afd56be344d179847e73fb2c7c699a9e3e3 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:04:56 -0800 Subject: [PATCH 04/15] update PublishUtilities to work with domain models --- Sources/NnexKit/Building/BuildConfig.swift | 6 +- .../NnexKit/Formula/PublishUtilities.swift | 4 +- .../Managers/PublishExecutionManager.swift | 29 ++--- Tests/NnexKitTests/ProjectBuilderTests.swift | 2 +- .../NnexKitTests/PublishUtilitiesTests.swift | 117 ++++++++---------- .../PublishExecutionManagerTests.swift | 6 +- .../nnexTests/PublishTests/PublishTests.swift | 20 +-- 7 files changed, 88 insertions(+), 96 deletions(-) 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/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/nnex/Managers/PublishExecutionManager.swift b/Sources/nnex/Managers/PublishExecutionManager.swift index 5d39f63..e1a9bea 100644 --- a/Sources/nnex/Managers/PublishExecutionManager.swift +++ b/Sources/nnex/Managers/PublishExecutionManager.swift @@ -45,20 +45,21 @@ extension PublishExecutionManager { message: String?, skipTests: Bool ) throws { - try gitHandler.checkForGitHubCLI() - try ensureNoUncommittedChanges(at: projectFolder.path) - - let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler, shell: shell, fileSystem: fileSystem) - let (resolvedVersionInfo, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: version, projectPath: projectFolder.path) - - let (tap, formula, buildType) = try getTapAndFormula(projectFolder: projectFolder, buildType: buildType, skipTests: skipTests) - let binaryOutput = try PublishUtilities.buildBinary(formula: formula, buildType: buildType, skipTesting: skipTests, shell: shell) - let archivedBinaries = try PublishUtilities.createArchives(from: binaryOutput, shell: shell) - let (assetURLs, versionNumber) = try uploadRelease(folder: projectFolder, archivedBinaries: archivedBinaries, versionInfo: resolvedVersionInfo, previousVersion: previousVersion, releaseNotesSource: .init(notes: notes, notesFile: notesFile)) - - let formulaContent = try PublishUtilities.makeFormulaContent(formula: formula, version: versionNumber, archivedBinaries: archivedBinaries, assetURLs: assetURLs) - - try publishFormula(formulaContent, formulaName: formula.name, message: message, tap: tap) + // TODO: - +// try gitHandler.checkForGitHubCLI() +// try ensureNoUncommittedChanges(at: projectFolder.path) +// +// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler, shell: shell, fileSystem: fileSystem) +// let (resolvedVersionInfo, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: version, projectPath: projectFolder.path) +// +// let (tap, formula, buildType) = try getTapAndFormula(projectFolder: projectFolder, buildType: buildType, skipTests: skipTests) +// let binaryOutput = try PublishUtilities.buildBinary(formula: formula, buildType: buildType, skipTesting: skipTests, shell: shell) +// let archivedBinaries = try PublishUtilities.createArchives(from: binaryOutput, shell: shell) +// let (assetURLs, versionNumber) = try uploadRelease(folder: projectFolder, archivedBinaries: archivedBinaries, versionInfo: resolvedVersionInfo, previousVersion: previousVersion, releaseNotesSource: .init(notes: notes, notesFile: notesFile)) +// +// let formulaContent = try PublishUtilities.makeFormulaContent(formula: formula, version: versionNumber, archivedBinaries: archivedBinaries, assetURLs: assetURLs) +// +// try publishFormula(formulaContent, formulaName: formula.name, message: message, tap: tap) } } 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/PublishTests/PublishExecutionManagerTests.swift b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift index ff49513..4cd5f70 100644 --- a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift +++ b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift @@ -243,7 +243,7 @@ extension PublishExecutionManagerTests { // MARK: - Error Tests extension PublishExecutionManagerTests { - @Test("Throws error when there are uncommitted changes") + @Test("Throws error when there are uncommitted changes", .disabled()) // TODO: - func throwsErrorWhenUncommittedChanges() throws { try createPackageSwift() @@ -273,7 +273,7 @@ extension PublishExecutionManagerTests { } } - @Test("Throws error when GitHub CLI is not available") + @Test("Throws error when GitHub CLI is not available", .disabled()) // TODO: - func throwsErrorWhenGitHubCLINotAvailable() throws { try createPackageSwift() @@ -298,7 +298,7 @@ extension PublishExecutionManagerTests { } } - @Test("Propagates build errors from PublishUtilities") + @Test("Propagates build errors from PublishUtilities", .disabled()) // TODO: - func propagatesBuildErrors() throws { try createPackageSwift() diff --git a/Tests/nnexTests/PublishTests/PublishTests.swift b/Tests/nnexTests/PublishTests/PublishTests.swift index e171b1b..b63f3e1 100644 --- a/Tests/nnexTests/PublishTests/PublishTests.swift +++ b/Tests/nnexTests/PublishTests/PublishTests.swift @@ -35,7 +35,7 @@ final class PublishTests: BasePublishTestSuite { // MARK: - Unit Tests extension PublishTests { - @Test("Cannot publish if 'gh' is not installed") + @Test("Cannot publish if 'gh' is not installed", .disabled()) // TODO: - func publishFailsWithNoGHCLI() throws { let gitHandler = MockGitHandler(ghIsInstalled: false) let shell = createMockShell() @@ -55,7 +55,7 @@ extension PublishTests { #expect(tapFolder.containsFile(named: formulaFileName) == false) } - @Test("Creates formula file when publishing") + @Test("Creates formula file when publishing", .disabled()) // TODO: - func publishCommand() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -71,7 +71,7 @@ extension PublishTests { #expect(formulaFileContents.contains(assetURL)) } - @Test("Creates formula file with sanitized class name when project has dashes") + @Test("Creates formula file with sanitized class name when project has dashes", .disabled()) // TODO: - func publishCommandWithDashesInName() throws { let projectWithDashes = "test-project-with-dashes" let expectedClassName = "TestProjectWithDashes" @@ -98,7 +98,7 @@ extension PublishTests { // MARK: - Passing Info to Args extension PublishTests { - @Test("Commits changes when commit message is included in args") + @Test("Commits changes when commit message is included in args", .disabled()) // TODO: - func commitsChanges() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -110,7 +110,7 @@ extension PublishTests { #expect(gitHandler.message == commitMessage) } - @Test("Automatically updates localProjectPath for formula if it doesn't match project folder path") + @Test("Automatically updates localProjectPath for formula if it doesn't match project folder path", .disabled()) // TODO: - func updatesFormulaLocalPath() throws { let staleLocalPath = "~/Desktop/stale" let shell = createMockShell() @@ -134,7 +134,7 @@ extension PublishTests { #expect(updatedFormula.localProjectPath == projectFolder.path) } - @Test("Uploads with inline release notes when included in args") + @Test("Uploads with inline release notes when included in args", .disabled()) // TODO: - func uploadsDirectReleaseNotes() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -149,7 +149,7 @@ extension PublishTests { #expect(releaseNoteInfo.content == releaseNotes) } - @Test("Uploads release notes from file when included in args") + @Test("Uploads release notes from file when included in args", .disabled()) // TODO: - func uploadsReleaseNotesFromFile() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -179,7 +179,7 @@ extension PublishTests { #expect(!shell.executedCommands.contains(where: { $0.contains("swift test") })) } - @Test("Runs tests when formula includes default test command") + @Test("Runs tests when formula includes default test command", .disabled()) // TODO: - func runsTestsWithDefaultCommand() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell(includeTestCommand: true) @@ -191,7 +191,7 @@ extension PublishTests { #expect(shell.executedCommands.contains { $0.contains("swift test") }) } - @Test("Runs tests when formula includes custom test command") + @Test("Runs tests when formula includes custom test command", .disabled()) // TODO: - func runsTestsWithCustomCommand() throws { let testCommand = "xcodebuild test -scheme testScheme -destination 'platform=macOS'" let gitHandler = MockGitHandler(assetURL: assetURL) @@ -225,7 +225,7 @@ extension PublishTests { #expect(!shell.executedCommands.contains { $0.contains("swift test") }) } - @Test("Fails to publish when tests fail") + @Test("Fails to publish when tests fail", .disabled()) // TODO: - func failsToPublishWhenTestsFail() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell(includeTestCommand: false, shouldThrowError: true) From d760d910f6c94195c0629c3ee146f4e28e1f1c50 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:18:20 -0800 Subject: [PATCH 05/15] WIP -> decoupled PublishInfoLoader from SwiftData models --- .../NnexKit/Extensions/String+Matches.swift | 19 ++++++ Sources/nnex/Commands/Brew/Publish.swift | 55 ++++++++--------- .../Managers/PublishExecutionManager.swift | 5 +- .../DisplayablePickerItemConformance.swift | 59 +++++++++++-------- .../nnex/Utilities/PublishInfoLoader.swift | 58 ++++++++++++------ 5 files changed, 125 insertions(+), 71 deletions(-) create mode 100644 Sources/NnexKit/Extensions/String+Matches.swift 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/nnex/Commands/Brew/Publish.swift b/Sources/nnex/Commands/Brew/Publish.swift index 9dfddf7..80f09a4 100644 --- a/Sources/nnex/Commands/Brew/Publish.swift +++ b/Sources/nnex/Commands/Brew/Publish.swift @@ -35,33 +35,34 @@ extension Nnex.Brew { var skipTests = false func run() throws { - let shell = Nnex.makeShell() - let picker = Nnex.makePicker() - let gitHandler = Nnex.makeGitHandler() - 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 manager = PublishExecutionManager( - shell: shell, - picker: picker, - gitHandler: gitHandler, - fileSystem: fileSystem, - folderBrowser: folderBrowser, - publishInfoLoader: publishInfoLoader - ) - - try manager.executePublish( - projectFolder: projectFolder, - version: version, - buildType: buildType, - notes: notes, - notesFile: notesFile, - message: message, - skipTests: skipTests - ) + // TODO: - +// let shell = Nnex.makeShell() +// let picker = Nnex.makePicker() +// let gitHandler = Nnex.makeGitHandler() +// 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 manager = PublishExecutionManager( +// shell: shell, +// picker: picker, +// gitHandler: gitHandler, +// fileSystem: fileSystem, +// folderBrowser: folderBrowser, +// publishInfoLoader: publishInfoLoader +// ) +// +// try manager.executePublish( +// projectFolder: projectFolder, +// version: version, +// buildType: buildType, +// notes: notes, +// notesFile: notesFile, +// message: message, +// skipTests: skipTests +// ) } } } diff --git a/Sources/nnex/Managers/PublishExecutionManager.swift b/Sources/nnex/Managers/PublishExecutionManager.swift index e1a9bea..bd4564b 100644 --- a/Sources/nnex/Managers/PublishExecutionManager.swift +++ b/Sources/nnex/Managers/PublishExecutionManager.swift @@ -93,10 +93,11 @@ private extension PublishExecutionManager { /// - 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) { - let (tap, formula) = try publishInfoLoader.loadPublishInfo() + fatalError() // TODO: - +// let (tap, formula) = try publishInfoLoader.loadPublishInfo() // Note: The formula's localProjectPath update is now handled by PublishInfoLoader if needed - return (tap, formula, buildType) +// return (tap, formula, buildType) } /// Uploads a release to GitHub and returns the asset URLs and version number. 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..644b748 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 } @@ -139,7 +142,24 @@ private extension PublishInfoLoader { } +// MARK: - Dependencies +protocol HomebrewTapStore { + func loadTaps() throws -> [HomebrewTap] + func updateFormula(_ formula: HomebrewFormula) throws + func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws +} + + // MARK: - Extension Dependenies enum FormulaTestType: CaseIterable { case custom, packageDefault, noTests } + +final class FakeStore: HomebrewTapStore { + func loadTaps() throws -> [HomebrewTap] { + return [] + } + + func updateFormula(_ formula: HomebrewFormula) throws { } + func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws { } +} From 1f734d9e508d826ac26e7a898aa574cc7a034240 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:29:30 -0800 Subject: [PATCH 06/15] update PublishInfoLoaderTests --- .../PublishExecutionManagerTests.swift | 708 +++++++++--------- .../PublishTests/PublishInfoLoaderTests.swift | 55 +- .../Shared/MockHomebrewTapStore.swift | 32 + 3 files changed, 413 insertions(+), 382 deletions(-) create mode 100644 Tests/nnexTests/Shared/MockHomebrewTapStore.swift diff --git a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift index 4cd5f70..7be860b 100644 --- a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift +++ b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift @@ -1,357 +1,357 @@ +//// +//// PublishExecutionManagerTests.swift +//// nnex +//// +//// Created by Nikolai Nobadi on 8/26/25. +//// // -// PublishExecutionManagerTests.swift -// nnex +//import NnexKit +//import Testing +//import Foundation +//import NnShellTesting +//import NnexSharedTestHelpers +//@testable import nnex +//@preconcurrency import Files // -// Created by Nikolai Nobadi on 8/26/25. +//@MainActor +//final class PublishExecutionManagerTests: BasePublishTestSuite { +// private let projectName = "testProject-publishManager" +// private let tapName = "testTap" +// private let executableName = "testExecutable" +// +// init() throws { +// try super.init(tapName: tapName, projectName: projectName) +// } +//} // - -import NnexKit -import Testing -import Foundation -import NnShellTesting -import NnexSharedTestHelpers -@testable import nnex -@preconcurrency import Files - -@MainActor -final class PublishExecutionManagerTests: BasePublishTestSuite { - private let projectName = "testProject-publishManager" - private let tapName = "testTap" - private let executableName = "testExecutable" - - init() throws { - try super.init(tapName: tapName, projectName: projectName) - } -} - - -// MARK: - Tests -extension PublishExecutionManagerTests { - @Test("Successfully executes publish with existing formula") - func successfullyExecutesPublishWithExistingFormula() throws { - try createPackageSwift() - - let projectPath = projectFolder.path - let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" - let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" - let commandResults: [String: String] = [ - "swift build -c release --arch arm64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", - "swift build -c release --arch x86_64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", - "cd \"\(projectPath).build/arm64-apple-macosx/release\" && tar -czf \"\(executableName)-arm64.tar.gz\" \"\(executableName)\"": "", - "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", - "cd \"\(projectPath).build/x86_64-apple-macosx/release\" && tar -czf \"\(executableName)-x86_64.tar.gz\" \"\(executableName)\"": "", - "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", - "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" - ] - - let factory = MockContextFactory( - commandResults: commandResults, - selectedItemIndices: [], - inputResponses: [ - "formula details", - "release notes" - ], - permissionResponses: [ - true, // create a new formula - false // Don't commit formula 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: [] - ) - - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) - - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: projectFolder), - version: .version("2.0.0"), - buildType: BuildType.universal, - notes: nil as String?, - notesFile: nil as String?, - message: nil as String?, - skipTests: true - ) - } - - @Test("Successfully executes publish with new formula creation") - func successfullyExecutesPublishWithNewFormulaCreation() throws { - try createPackageSwift() - - let projectPath = projectFolder.path - let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" - let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" - - let commandResults: [String: String] = [ - "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", - "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", - "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" - ] - - let factory = MockContextFactory( - commandResults: commandResults, - selectedItemIndices: [0, 0], // Select tap, select no tests - inputResponses: [ - "formula details", - "release notes" - ], - permissionResponses: [ - true, // create a new formula - false // Don't commit formula to GitHub - ] - ) - - let context = try factory.makeContext() - let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") - - try context.saveNewTap(existingTap) - - let sut = try makeSUT(factory: factory, context: context) - - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: projectFolder), - version: .version("2.0.0"), - buildType: BuildType.universal, - notes: nil as String?, - notesFile: nil as String?, - message: nil as String?, - skipTests: true - ) - } - - @Test("Commits and pushes formula when user chooses to") - func commitsAndPushesFormulaWhenUserChooses() throws { - try createPackageSwift() - - let projectPath = projectFolder.path - let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" - let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" - - let commandResults: [String: String] = [ - "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", - "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", - "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" - // Git commands will be handled by MockGitHandler, not shell commands - ] - - let factory = MockContextFactory( - commandResults: commandResults, - selectedItemIndices: [], - inputResponses: [ - "release notes", - "Test commit message" // Commit message - ], - 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: [] - ) - - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) - - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: projectFolder), - version: .version("2.0.0"), - buildType: BuildType.universal, - notes: nil as String?, - notesFile: nil as String?, - message: nil as String?, - skipTests: true - ) - } - - @Test("Uses provided commit message instead of asking user") - func usesProvidedCommitMessage() throws { - try createPackageSwift() - - let projectPath = projectFolder.path - let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" - let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" - - let commandResults: [String: String] = [ - "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", - "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", - "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" - ] - - let factory = MockContextFactory( - commandResults: commandResults, - inputResponses: [ - "formula details", - "release notes" - ], - permissionResponses: [ - true // create new formula - ] - ) - - 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: [] - ) - - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let sut = try makeSUT(factory: factory, context: context) - - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: projectFolder), - version: .version("2.0.0"), - buildType: BuildType.universal, - notes: nil, - notesFile: nil, - message: "Provided commit message", - skipTests: true - ) - } -} - - -// MARK: - Error Tests -extension PublishExecutionManagerTests { - @Test("Throws error when there are uncommitted changes", .disabled()) // TODO: - - func throwsErrorWhenUncommittedChanges() throws { - try createPackageSwift() - - let projectPath = projectFolder.path - let commandResults: [String: String] = [ - "cd \"\(projectPath)\" && git status --porcelain": "M modified_file.swift" // Uncommitted changes present - ] - - let factory = MockContextFactory( - commandResults: commandResults - ) - - let folder = projectFolder - let context = try factory.makeContext() - let sut = try makeSUT(factory: factory, context: context) - - #expect(throws: PublishExecutionError.uncommittedChanges) { - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: folder), - version: .version("2.0.0"), - buildType: BuildType.universal, - notes: nil, - notesFile: nil, - message: nil, - skipTests: true - ) - } - } - - @Test("Throws error when GitHub CLI is not available", .disabled()) // TODO: - - func throwsErrorWhenGitHubCLINotAvailable() throws { - try createPackageSwift() - - let factory = MockContextFactory( - gitHandler: MockGitHandler(ghIsInstalled: false) - ) - - let folder = FilesDirectoryAdapter(folder: projectFolder) - let context = try factory.makeContext() - let sut = try makeSUT(factory: factory, context: context) - - #expect(throws: (any Error).self) { - try sut.executePublish( - projectFolder: folder, - version: nil as ReleaseVersionInfo?, - buildType: BuildType.universal, - notes: nil as String?, - notesFile: nil as String?, - message: nil as String?, - skipTests: true - ) - } - } - - @Test("Propagates build errors from PublishUtilities", .disabled()) // TODO: - - func propagatesBuildErrors() throws { - 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: [] - ) - - try context.saveNewTap(existingTap) - try context.saveNewFormula(existingFormula, in: existingTap) - - let folder = projectFolder - let sut = try makeSUT(factory: factory, context: context) - - #expect(throws: (any Error).self) { - try sut.executePublish( - projectFolder: FilesDirectoryAdapter(folder: folder), - version: nil as ReleaseVersionInfo?, - buildType: BuildType.universal, - notes: nil as String?, - notesFile: nil as String?, - message: nil as String?, - skipTests: true - ) - } - } -} - - -// MARK: - Private Methods -private extension PublishExecutionManagerTests { - func makeSUT(factory: MockContextFactory, context: NnexContext) 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) - - return .init(shell: shell, picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser, publishInfoLoader: publishInfoLoader) - } - - func createPackageSwift() throws { - try super.createPackageSwift(packageName: projectName, executableName: executableName) - } -} +// +//// MARK: - Tests +//extension PublishExecutionManagerTests { +// @Test("Successfully executes publish with existing formula") +// func successfullyExecutesPublishWithExistingFormula() throws { +// try createPackageSwift() +// +// let projectPath = projectFolder.path +// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" +// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" +// let commandResults: [String: String] = [ +// "swift build -c release --arch arm64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", +// "swift build -c release --arch x86_64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", +// "cd \"\(projectPath).build/arm64-apple-macosx/release\" && tar -czf \"\(executableName)-arm64.tar.gz\" \"\(executableName)\"": "", +// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", +// "cd \"\(projectPath).build/x86_64-apple-macosx/release\" && tar -czf \"\(executableName)-x86_64.tar.gz\" \"\(executableName)\"": "", +// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", +// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" +// ] +// +// let factory = MockContextFactory( +// commandResults: commandResults, +// selectedItemIndices: [], +// inputResponses: [ +// "formula details", +// "release notes" +// ], +// permissionResponses: [ +// true, // create a new formula +// false // Don't commit formula 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: [] +// ) +// +// try context.saveNewTap(existingTap) +// try context.saveNewFormula(existingFormula, in: existingTap) +// +// let sut = try makeSUT(factory: factory, context: context) +// +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: projectFolder), +// version: .version("2.0.0"), +// buildType: BuildType.universal, +// notes: nil as String?, +// notesFile: nil as String?, +// message: nil as String?, +// skipTests: true +// ) +// } +// +// @Test("Successfully executes publish with new formula creation") +// func successfullyExecutesPublishWithNewFormulaCreation() throws { +// try createPackageSwift() +// +// let projectPath = projectFolder.path +// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" +// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" +// +// let commandResults: [String: String] = [ +// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", +// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", +// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" +// ] +// +// let factory = MockContextFactory( +// commandResults: commandResults, +// selectedItemIndices: [0, 0], // Select tap, select no tests +// inputResponses: [ +// "formula details", +// "release notes" +// ], +// permissionResponses: [ +// true, // create a new formula +// false // Don't commit formula to GitHub +// ] +// ) +// +// let context = try factory.makeContext() +// let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") +// +// try context.saveNewTap(existingTap) +// +// let sut = try makeSUT(factory: factory, context: context) +// +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: projectFolder), +// version: .version("2.0.0"), +// buildType: BuildType.universal, +// notes: nil as String?, +// notesFile: nil as String?, +// message: nil as String?, +// skipTests: true +// ) +// } +// +// @Test("Commits and pushes formula when user chooses to") +// func commitsAndPushesFormulaWhenUserChooses() throws { +// try createPackageSwift() +// +// let projectPath = projectFolder.path +// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" +// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" +// +// let commandResults: [String: String] = [ +// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", +// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", +// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" +// // Git commands will be handled by MockGitHandler, not shell commands +// ] +// +// let factory = MockContextFactory( +// commandResults: commandResults, +// selectedItemIndices: [], +// inputResponses: [ +// "release notes", +// "Test commit message" // Commit message +// ], +// 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: [] +// ) +// +// try context.saveNewTap(existingTap) +// try context.saveNewFormula(existingFormula, in: existingTap) +// +// let sut = try makeSUT(factory: factory, context: context) +// +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: projectFolder), +// version: .version("2.0.0"), +// buildType: BuildType.universal, +// notes: nil as String?, +// notesFile: nil as String?, +// message: nil as String?, +// skipTests: true +// ) +// } +// +// @Test("Uses provided commit message instead of asking user") +// func usesProvidedCommitMessage() throws { +// try createPackageSwift() +// +// let projectPath = projectFolder.path +// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" +// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" +// +// let commandResults: [String: String] = [ +// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", +// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", +// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" +// ] +// +// let factory = MockContextFactory( +// commandResults: commandResults, +// inputResponses: [ +// "formula details", +// "release notes" +// ], +// permissionResponses: [ +// true // create new formula +// ] +// ) +// +// 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: [] +// ) +// +// try context.saveNewTap(existingTap) +// try context.saveNewFormula(existingFormula, in: existingTap) +// +// let sut = try makeSUT(factory: factory, context: context) +// +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: projectFolder), +// version: .version("2.0.0"), +// buildType: BuildType.universal, +// notes: nil, +// notesFile: nil, +// message: "Provided commit message", +// skipTests: true +// ) +// } +//} +// +// +//// MARK: - Error Tests +//extension PublishExecutionManagerTests { +// @Test("Throws error when there are uncommitted changes", .disabled()) // TODO: - +// func throwsErrorWhenUncommittedChanges() throws { +// try createPackageSwift() +// +// let projectPath = projectFolder.path +// let commandResults: [String: String] = [ +// "cd \"\(projectPath)\" && git status --porcelain": "M modified_file.swift" // Uncommitted changes present +// ] +// +// let factory = MockContextFactory( +// commandResults: commandResults +// ) +// +// let folder = projectFolder +// let context = try factory.makeContext() +// let sut = try makeSUT(factory: factory, context: context) +// +// #expect(throws: PublishExecutionError.uncommittedChanges) { +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: folder), +// version: .version("2.0.0"), +// buildType: BuildType.universal, +// notes: nil, +// notesFile: nil, +// message: nil, +// skipTests: true +// ) +// } +// } +// +// @Test("Throws error when GitHub CLI is not available", .disabled()) // TODO: - +// func throwsErrorWhenGitHubCLINotAvailable() throws { +// try createPackageSwift() +// +// let factory = MockContextFactory( +// gitHandler: MockGitHandler(ghIsInstalled: false) +// ) +// +// let folder = FilesDirectoryAdapter(folder: projectFolder) +// let context = try factory.makeContext() +// let sut = try makeSUT(factory: factory, context: context) +// +// #expect(throws: (any Error).self) { +// try sut.executePublish( +// projectFolder: folder, +// version: nil as ReleaseVersionInfo?, +// buildType: BuildType.universal, +// notes: nil as String?, +// notesFile: nil as String?, +// message: nil as String?, +// skipTests: true +// ) +// } +// } +// +// @Test("Propagates build errors from PublishUtilities", .disabled()) // TODO: - +// func propagatesBuildErrors() throws { +// 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: [] +// ) +// +// try context.saveNewTap(existingTap) +// try context.saveNewFormula(existingFormula, in: existingTap) +// +// let folder = projectFolder +// let sut = try makeSUT(factory: factory, context: context) +// +// #expect(throws: (any Error).self) { +// try sut.executePublish( +// projectFolder: FilesDirectoryAdapter(folder: folder), +// version: nil as ReleaseVersionInfo?, +// buildType: BuildType.universal, +// notes: nil as String?, +// notesFile: nil as String?, +// message: nil as String?, +// skipTests: true +// ) +// } +// } +//} +// +// +//// MARK: - Private Methods +//private extension PublishExecutionManagerTests { +// func makeSUT(factory: MockContextFactory, context: NnexContext) 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) +// +// return .init(shell: shell, picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser, publishInfoLoader: publishInfoLoader) +// } +// +// func createPackageSwift() throws { +// try super.createPackageSwift(packageName: projectName, executableName: executableName) +// } +//} 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/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) + } +} From d0ce3a312581cb477f304c30472ab0eaf8928b98 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:31:39 -0800 Subject: [PATCH 07/15] enabled PublishExecutionManager --- .../Managers/PublishExecutionManager.swift | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/Sources/nnex/Managers/PublishExecutionManager.swift b/Sources/nnex/Managers/PublishExecutionManager.swift index bd4564b..62d843e 100644 --- a/Sources/nnex/Managers/PublishExecutionManager.swift +++ b/Sources/nnex/Managers/PublishExecutionManager.swift @@ -45,21 +45,20 @@ extension PublishExecutionManager { message: String?, skipTests: Bool ) throws { - // TODO: - -// try gitHandler.checkForGitHubCLI() -// try ensureNoUncommittedChanges(at: projectFolder.path) -// -// let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler, shell: shell, fileSystem: fileSystem) -// let (resolvedVersionInfo, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: version, projectPath: projectFolder.path) -// -// let (tap, formula, buildType) = try getTapAndFormula(projectFolder: projectFolder, buildType: buildType, skipTests: skipTests) -// let binaryOutput = try PublishUtilities.buildBinary(formula: formula, buildType: buildType, skipTesting: skipTests, shell: shell) -// let archivedBinaries = try PublishUtilities.createArchives(from: binaryOutput, shell: shell) -// let (assetURLs, versionNumber) = try uploadRelease(folder: projectFolder, archivedBinaries: archivedBinaries, versionInfo: resolvedVersionInfo, previousVersion: previousVersion, releaseNotesSource: .init(notes: notes, notesFile: notesFile)) -// -// let formulaContent = try PublishUtilities.makeFormulaContent(formula: formula, version: versionNumber, archivedBinaries: archivedBinaries, assetURLs: assetURLs) -// -// try publishFormula(formulaContent, formulaName: formula.name, message: message, tap: tap) + try gitHandler.checkForGitHubCLI() + try ensureNoUncommittedChanges(at: projectFolder.path) + + let versionHandler = ReleaseVersionHandler(picker: picker, gitHandler: gitHandler, shell: shell, fileSystem: fileSystem) + let (resolvedVersionInfo, previousVersion) = try versionHandler.resolveVersionInfo(versionInfo: version, projectPath: projectFolder.path) + + let (tap, formula, buildType) = try getTapAndFormula(projectFolder: projectFolder, buildType: buildType, skipTests: skipTests) + let binaryOutput = try PublishUtilities.buildBinary(formula: formula, buildType: buildType, skipTesting: skipTests, shell: shell) + let archivedBinaries = try PublishUtilities.createArchives(from: binaryOutput, shell: shell) + let (assetURLs, versionNumber) = try uploadRelease(folder: projectFolder, archivedBinaries: archivedBinaries, versionInfo: resolvedVersionInfo, previousVersion: previousVersion, releaseNotesSource: .init(notes: notes, notesFile: notesFile)) + + let formulaContent = try PublishUtilities.makeFormulaContent(formula: formula, version: versionNumber, archivedBinaries: archivedBinaries, assetURLs: assetURLs) + + try publishFormula(formulaContent, formulaName: formula.name, message: message, tap: tap) } } @@ -92,12 +91,11 @@ 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) { - fatalError() // TODO: - -// let (tap, formula) = try publishInfoLoader.loadPublishInfo() + 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 -// return (tap, formula, buildType) + return (tap, formula, buildType) } /// Uploads a release to GitHub and returns the asset URLs and version number. @@ -111,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) } @@ -122,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) From 3d8da7c9b6532ba63fde995de538e5b37dc89035 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:36:01 -0800 Subject: [PATCH 08/15] fix PublishExecutionManagerTests --- .../PublishExecutionManagerTests.swift | 740 +++++++++--------- 1 file changed, 386 insertions(+), 354 deletions(-) diff --git a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift index 7be860b..07e5c4a 100644 --- a/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift +++ b/Tests/nnexTests/PublishTests/PublishExecutionManagerTests.swift @@ -1,357 +1,389 @@ -//// -//// PublishExecutionManagerTests.swift -//// nnex -//// -//// Created by Nikolai Nobadi on 8/26/25. -//// // -//import NnexKit -//import Testing -//import Foundation -//import NnShellTesting -//import NnexSharedTestHelpers -//@testable import nnex -//@preconcurrency import Files +// PublishExecutionManagerTests.swift +// nnex // -//@MainActor -//final class PublishExecutionManagerTests: BasePublishTestSuite { -// private let projectName = "testProject-publishManager" -// private let tapName = "testTap" -// private let executableName = "testExecutable" -// -// init() throws { -// try super.init(tapName: tapName, projectName: projectName) -// } -//} +// Created by Nikolai Nobadi on 8/26/25. // -// -//// MARK: - Tests -//extension PublishExecutionManagerTests { -// @Test("Successfully executes publish with existing formula") -// func successfullyExecutesPublishWithExistingFormula() throws { -// try createPackageSwift() -// -// let projectPath = projectFolder.path -// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" -// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" -// let commandResults: [String: String] = [ -// "swift build -c release --arch arm64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", -// "swift build -c release --arch x86_64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", -// "cd \"\(projectPath).build/arm64-apple-macosx/release\" && tar -czf \"\(executableName)-arm64.tar.gz\" \"\(executableName)\"": "", -// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", -// "cd \"\(projectPath).build/x86_64-apple-macosx/release\" && tar -czf \"\(executableName)-x86_64.tar.gz\" \"\(executableName)\"": "", -// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", -// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" -// ] -// -// let factory = MockContextFactory( -// commandResults: commandResults, -// selectedItemIndices: [], -// inputResponses: [ -// "formula details", -// "release notes" -// ], -// permissionResponses: [ -// true, // create a new formula -// false // Don't commit formula 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: [] -// ) -// -// try context.saveNewTap(existingTap) -// try context.saveNewFormula(existingFormula, in: existingTap) -// -// let sut = try makeSUT(factory: factory, context: context) -// -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: projectFolder), -// version: .version("2.0.0"), -// buildType: BuildType.universal, -// notes: nil as String?, -// notesFile: nil as String?, -// message: nil as String?, -// skipTests: true -// ) -// } -// -// @Test("Successfully executes publish with new formula creation") -// func successfullyExecutesPublishWithNewFormulaCreation() throws { -// try createPackageSwift() -// -// let projectPath = projectFolder.path -// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" -// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" -// -// let commandResults: [String: String] = [ -// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", -// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", -// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" -// ] -// -// let factory = MockContextFactory( -// commandResults: commandResults, -// selectedItemIndices: [0, 0], // Select tap, select no tests -// inputResponses: [ -// "formula details", -// "release notes" -// ], -// permissionResponses: [ -// true, // create a new formula -// false // Don't commit formula to GitHub -// ] -// ) -// -// let context = try factory.makeContext() -// let existingTap = SwiftDataHomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "") -// -// try context.saveNewTap(existingTap) -// -// let sut = try makeSUT(factory: factory, context: context) -// -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: projectFolder), -// version: .version("2.0.0"), -// buildType: BuildType.universal, -// notes: nil as String?, -// notesFile: nil as String?, -// message: nil as String?, -// skipTests: true -// ) -// } -// -// @Test("Commits and pushes formula when user chooses to") -// func commitsAndPushesFormulaWhenUserChooses() throws { -// try createPackageSwift() -// -// let projectPath = projectFolder.path -// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" -// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" -// -// let commandResults: [String: String] = [ -// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", -// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", -// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" -// // Git commands will be handled by MockGitHandler, not shell commands -// ] -// -// let factory = MockContextFactory( -// commandResults: commandResults, -// selectedItemIndices: [], -// inputResponses: [ -// "release notes", -// "Test commit message" // Commit message -// ], -// 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: [] -// ) -// -// try context.saveNewTap(existingTap) -// try context.saveNewFormula(existingFormula, in: existingTap) -// -// let sut = try makeSUT(factory: factory, context: context) -// -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: projectFolder), -// version: .version("2.0.0"), -// buildType: BuildType.universal, -// notes: nil as String?, -// notesFile: nil as String?, -// message: nil as String?, -// skipTests: true -// ) -// } -// -// @Test("Uses provided commit message instead of asking user") -// func usesProvidedCommitMessage() throws { -// try createPackageSwift() -// -// let projectPath = projectFolder.path -// let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" -// let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" -// -// let commandResults: [String: String] = [ -// "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", -// "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", -// "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" -// ] -// -// let factory = MockContextFactory( -// commandResults: commandResults, -// inputResponses: [ -// "formula details", -// "release notes" -// ], -// permissionResponses: [ -// true // create new formula -// ] -// ) -// -// 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: [] -// ) -// -// try context.saveNewTap(existingTap) -// try context.saveNewFormula(existingFormula, in: existingTap) -// -// let sut = try makeSUT(factory: factory, context: context) -// -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: projectFolder), -// version: .version("2.0.0"), -// buildType: BuildType.universal, -// notes: nil, -// notesFile: nil, -// message: "Provided commit message", -// skipTests: true -// ) -// } -//} -// -// -//// MARK: - Error Tests -//extension PublishExecutionManagerTests { -// @Test("Throws error when there are uncommitted changes", .disabled()) // TODO: - -// func throwsErrorWhenUncommittedChanges() throws { -// try createPackageSwift() -// -// let projectPath = projectFolder.path -// let commandResults: [String: String] = [ -// "cd \"\(projectPath)\" && git status --porcelain": "M modified_file.swift" // Uncommitted changes present -// ] -// -// let factory = MockContextFactory( -// commandResults: commandResults -// ) -// -// let folder = projectFolder -// let context = try factory.makeContext() -// let sut = try makeSUT(factory: factory, context: context) -// -// #expect(throws: PublishExecutionError.uncommittedChanges) { -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: folder), -// version: .version("2.0.0"), -// buildType: BuildType.universal, -// notes: nil, -// notesFile: nil, -// message: nil, -// skipTests: true -// ) -// } -// } -// -// @Test("Throws error when GitHub CLI is not available", .disabled()) // TODO: - -// func throwsErrorWhenGitHubCLINotAvailable() throws { -// try createPackageSwift() -// -// let factory = MockContextFactory( -// gitHandler: MockGitHandler(ghIsInstalled: false) -// ) -// -// let folder = FilesDirectoryAdapter(folder: projectFolder) -// let context = try factory.makeContext() -// let sut = try makeSUT(factory: factory, context: context) -// -// #expect(throws: (any Error).self) { -// try sut.executePublish( -// projectFolder: folder, -// version: nil as ReleaseVersionInfo?, -// buildType: BuildType.universal, -// notes: nil as String?, -// notesFile: nil as String?, -// message: nil as String?, -// skipTests: true -// ) -// } -// } -// -// @Test("Propagates build errors from PublishUtilities", .disabled()) // TODO: - -// func propagatesBuildErrors() throws { -// 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: [] -// ) -// -// try context.saveNewTap(existingTap) -// try context.saveNewFormula(existingFormula, in: existingTap) -// -// let folder = projectFolder -// let sut = try makeSUT(factory: factory, context: context) -// -// #expect(throws: (any Error).self) { -// try sut.executePublish( -// projectFolder: FilesDirectoryAdapter(folder: folder), -// version: nil as ReleaseVersionInfo?, -// buildType: BuildType.universal, -// notes: nil as String?, -// notesFile: nil as String?, -// message: nil as String?, -// skipTests: true -// ) -// } -// } -//} -// -// -//// MARK: - Private Methods -//private extension PublishExecutionManagerTests { -// func makeSUT(factory: MockContextFactory, context: NnexContext) 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) -// -// return .init(shell: shell, picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser, publishInfoLoader: publishInfoLoader) -// } -// -// func createPackageSwift() throws { -// try super.createPackageSwift(packageName: projectName, executableName: executableName) -// } -//} + +import NnexKit +import Testing +import Foundation +import NnShellTesting +import NnexSharedTestHelpers +@testable import nnex +@preconcurrency import Files + +@MainActor +final class PublishExecutionManagerTests: BasePublishTestSuite { + private let projectName = "testProject-publishManager" + private let tapName = "testTap" + private let executableName = "testExecutable" + + init() throws { + try super.init(tapName: tapName, projectName: projectName) + } +} + + +// MARK: - Tests +extension PublishExecutionManagerTests { + @Test("Successfully executes publish with existing formula") + func successfullyExecutesPublishWithExistingFormula() throws { + try createPackageSwift() + + let projectPath = projectFolder.path + let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" + let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" + let commandResults: [String: String] = [ + "swift build -c release --arch arm64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", + "swift build -c release --arch x86_64 -Xswiftc -Osize -Xswiftc -wmo -Xswiftc -gnone -Xswiftc -cross-module-optimization -Xlinker -dead_strip_dylibs --package-path \(projectPath) ": "", + "cd \"\(projectPath).build/arm64-apple-macosx/release\" && tar -czf \"\(executableName)-arm64.tar.gz\" \"\(executableName)\"": "", + "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", + "cd \"\(projectPath).build/x86_64-apple-macosx/release\" && tar -czf \"\(executableName)-x86_64.tar.gz\" \"\(executableName)\"": "", + "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", + "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" + ] + + let factory = MockContextFactory( + commandResults: commandResults, + selectedItemIndices: [], + inputResponses: [ + "formula details", + "release notes" + ], + permissionResponses: [ + true, // create a new formula + false // Don't commit formula to GitHub + ] + ) + + 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: [] + ) + ] + ) + ] + ) + + let sut = try makeSUT(factory: factory, store: store) + + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: projectFolder), + version: .version("2.0.0"), + buildType: BuildType.universal, + notes: nil as String?, + notesFile: nil as String?, + message: nil as String?, + skipTests: true + ) + } + + @Test("Successfully executes publish with new formula creation") + func successfullyExecutesPublishWithNewFormulaCreation() throws { + try createPackageSwift() + + let projectPath = projectFolder.path + let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" + let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" + + let commandResults: [String: String] = [ + "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", + "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", + "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" + ] + + let factory = MockContextFactory( + commandResults: commandResults, + selectedItemIndices: [0, 0], // Select tap, select no tests + inputResponses: [ + "formula details", + "release notes" + ], + permissionResponses: [ + true, // create a new formula + false // Don't commit formula to GitHub + ] + ) + + let store = MockHomebrewTapStore( + taps: [ + HomebrewTap(name: tapName, localPath: tapFolder.path, remotePath: "", formulas: []) + ] + ) + + let sut = try makeSUT(factory: factory, store: store) + + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: projectFolder), + version: .version("2.0.0"), + buildType: BuildType.universal, + notes: nil as String?, + notesFile: nil as String?, + message: nil as String?, + skipTests: true + ) + } + + @Test("Commits and pushes formula when user chooses to") + func commitsAndPushesFormulaWhenUserChooses() throws { + try createPackageSwift() + + let projectPath = projectFolder.path + let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" + let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" + + let commandResults: [String: String] = [ + "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", + "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", + "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" + // Git commands will be handled by MockGitHandler, not shell commands + ] + + let factory = MockContextFactory( + commandResults: commandResults, + selectedItemIndices: [], + inputResponses: [ + "release notes", + "Test commit message" // Commit message + ], + permissionResponses: [true] // Commit and push to GitHub + ) + + 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: [] + ) + ] + ) + ] + ) + + let sut = try makeSUT(factory: factory, store: store) + + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: projectFolder), + version: .version("2.0.0"), + buildType: BuildType.universal, + notes: nil as String?, + notesFile: nil as String?, + message: nil as String?, + skipTests: true + ) + } + + @Test("Uses provided commit message instead of asking user") + func usesProvidedCommitMessage() throws { + try createPackageSwift() + + let projectPath = projectFolder.path + let armArchivePath = "\(projectPath).build/arm64-apple-macosx/release/\(executableName)-arm64.tar.gz" + let intelArchivePath = "\(projectPath).build/x86_64-apple-macosx/release/\(executableName)-x86_64.tar.gz" + + let commandResults: [String: String] = [ + "shasum -a 256 \"\(armArchivePath)\"": "abc123def456 \(armArchivePath)", + "shasum -a 256 \"\(intelArchivePath)\"": "abc123def456 \(intelArchivePath)", + "gh release view --json assets": "asset1.tar.gz\nasset2.tar.gz" + ] + + let factory = MockContextFactory( + commandResults: commandResults, + inputResponses: [ + "formula details", + "release notes" + ], + permissionResponses: [ + true // create new formula + ] + ) + + 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: [] + ) + ] + ) + ] + ) + + let sut = try makeSUT(factory: factory, store: store) + + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: projectFolder), + version: .version("2.0.0"), + buildType: BuildType.universal, + notes: nil, + notesFile: nil, + message: "Provided commit message", + skipTests: true + ) + } +} + + +// MARK: - Error Tests +extension PublishExecutionManagerTests { + @Test("Throws error when there are uncommitted changes") + func throwsErrorWhenUncommittedChanges() throws { + try createPackageSwift() + + let projectPath = projectFolder.path + let commandResults: [String: String] = [ + "cd \"\(projectPath)\" && git status --porcelain": "M modified_file.swift" // Uncommitted changes present + ] + + let factory = MockContextFactory( + commandResults: commandResults + ) + + let folder = projectFolder + let store = MockHomebrewTapStore(taps: []) + let sut = try makeSUT(factory: factory, store: store) + + #expect(throws: PublishExecutionError.uncommittedChanges) { + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: folder), + version: .version("2.0.0"), + buildType: BuildType.universal, + notes: nil, + notesFile: nil, + message: nil, + skipTests: true + ) + } + } + + @Test("Throws error when GitHub CLI is not available") + func throwsErrorWhenGitHubCLINotAvailable() throws { + try createPackageSwift() + + let factory = MockContextFactory( + gitHandler: MockGitHandler(ghIsInstalled: false) + ) + + let folder = FilesDirectoryAdapter(folder: projectFolder) + let store = MockHomebrewTapStore(taps: []) + let sut = try makeSUT(factory: factory, store: store) + + #expect(throws: (any Error).self) { + try sut.executePublish( + projectFolder: folder, + version: nil as ReleaseVersionInfo?, + buildType: BuildType.universal, + notes: nil as String?, + notesFile: nil as String?, + message: nil as String?, + skipTests: true + ) + } + } + + @Test("Propagates build errors from PublishUtilities") + func propagatesBuildErrors() throws { + try createPackageSwift() + + let factory = MockContextFactory(shell: MockShell(shouldThrowErrorOnFinal: true)) + 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: [] + ) + ] + ) + ] + ) + + let folder = projectFolder + let sut = try makeSUT(factory: factory, store: store) + + #expect(throws: (any Error).self) { + try sut.executePublish( + projectFolder: FilesDirectoryAdapter(folder: folder), + version: nil as ReleaseVersionInfo?, + buildType: BuildType.universal, + notes: nil as String?, + notesFile: nil as String?, + message: nil as String?, + skipTests: true + ) + } + } +} + + +// MARK: - Private Methods +private extension PublishExecutionManagerTests { + 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, + gitHandler: gitHandler, + store: store, + projectFolder: folderAdapter, + skipTests: true + ) + + return .init(shell: shell, picker: picker, gitHandler: gitHandler, fileSystem: fileSystem, folderBrowser: folderBrowser, publishInfoLoader: publishInfoLoader) + } + + func createPackageSwift() throws { + try super.createPackageSwift(packageName: projectName, executableName: executableName) + } +} From e05c009b979ed82eb756e7e3212d4be6636e53a0 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:42:33 -0800 Subject: [PATCH 09/15] re-enable Publish command with new structure --- Sources/NnexKit/Shared/HomebrewTapStore.swift | 12 ++++ .../Adapters/HomebrewTapStoreAdapter.swift | 65 +++++++++++++++++++ Sources/nnex/Commands/Brew/Publish.swift | 63 ++++++++++-------- .../nnex/Utilities/PublishInfoLoader.swift | 17 ----- .../nnexTests/PublishTests/PublishTests.swift | 20 +++--- 5 files changed, 122 insertions(+), 55 deletions(-) create mode 100644 Sources/NnexKit/Shared/HomebrewTapStore.swift create mode 100644 Sources/NnexKit/SwiftData/Adapters/HomebrewTapStoreAdapter.swift 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/nnex/Commands/Brew/Publish.swift b/Sources/nnex/Commands/Brew/Publish.swift index 80f09a4..139a518 100644 --- a/Sources/nnex/Commands/Brew/Publish.swift +++ b/Sources/nnex/Commands/Brew/Publish.swift @@ -35,34 +35,41 @@ extension Nnex.Brew { var skipTests = false func run() throws { - // TODO: - -// let shell = Nnex.makeShell() -// let picker = Nnex.makePicker() -// let gitHandler = Nnex.makeGitHandler() -// 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 manager = PublishExecutionManager( -// shell: shell, -// picker: picker, -// gitHandler: gitHandler, -// fileSystem: fileSystem, -// folderBrowser: folderBrowser, -// publishInfoLoader: publishInfoLoader -// ) -// -// try manager.executePublish( -// projectFolder: projectFolder, -// version: version, -// buildType: buildType, -// notes: notes, -// notesFile: notesFile, -// message: message, -// skipTests: skipTests -// ) + let shell = Nnex.makeShell() + let picker = Nnex.makePicker() + let gitHandler = Nnex.makeGitHandler() + let context = try Nnex.makeContext() + let fileSystem = Nnex.makeFileSystem() + let folderBrowser = Nnex.makeFolderBrowser(picker: picker, fileSystem: fileSystem) + 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, + gitHandler: gitHandler, + fileSystem: fileSystem, + folderBrowser: folderBrowser, + publishInfoLoader: publishInfoLoader + ) + + try manager.executePublish( + projectFolder: projectFolder, + version: version, + buildType: resolvedBuildType, + notes: notes, + notesFile: notesFile, + message: message, + skipTests: skipTests + ) } } } diff --git a/Sources/nnex/Utilities/PublishInfoLoader.swift b/Sources/nnex/Utilities/PublishInfoLoader.swift index 644b748..54218f0 100644 --- a/Sources/nnex/Utilities/PublishInfoLoader.swift +++ b/Sources/nnex/Utilities/PublishInfoLoader.swift @@ -142,24 +142,7 @@ private extension PublishInfoLoader { } -// MARK: - Dependencies -protocol HomebrewTapStore { - func loadTaps() throws -> [HomebrewTap] - func updateFormula(_ formula: HomebrewFormula) throws - func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws -} - - // MARK: - Extension Dependenies enum FormulaTestType: CaseIterable { case custom, packageDefault, noTests } - -final class FakeStore: HomebrewTapStore { - func loadTaps() throws -> [HomebrewTap] { - return [] - } - - func updateFormula(_ formula: HomebrewFormula) throws { } - func saveNewFormula(_ formula: HomebrewFormula, in tap: HomebrewTap) throws { } -} diff --git a/Tests/nnexTests/PublishTests/PublishTests.swift b/Tests/nnexTests/PublishTests/PublishTests.swift index b63f3e1..e171b1b 100644 --- a/Tests/nnexTests/PublishTests/PublishTests.swift +++ b/Tests/nnexTests/PublishTests/PublishTests.swift @@ -35,7 +35,7 @@ final class PublishTests: BasePublishTestSuite { // MARK: - Unit Tests extension PublishTests { - @Test("Cannot publish if 'gh' is not installed", .disabled()) // TODO: - + @Test("Cannot publish if 'gh' is not installed") func publishFailsWithNoGHCLI() throws { let gitHandler = MockGitHandler(ghIsInstalled: false) let shell = createMockShell() @@ -55,7 +55,7 @@ extension PublishTests { #expect(tapFolder.containsFile(named: formulaFileName) == false) } - @Test("Creates formula file when publishing", .disabled()) // TODO: - + @Test("Creates formula file when publishing") func publishCommand() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -71,7 +71,7 @@ extension PublishTests { #expect(formulaFileContents.contains(assetURL)) } - @Test("Creates formula file with sanitized class name when project has dashes", .disabled()) // TODO: - + @Test("Creates formula file with sanitized class name when project has dashes") func publishCommandWithDashesInName() throws { let projectWithDashes = "test-project-with-dashes" let expectedClassName = "TestProjectWithDashes" @@ -98,7 +98,7 @@ extension PublishTests { // MARK: - Passing Info to Args extension PublishTests { - @Test("Commits changes when commit message is included in args", .disabled()) // TODO: - + @Test("Commits changes when commit message is included in args") func commitsChanges() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -110,7 +110,7 @@ extension PublishTests { #expect(gitHandler.message == commitMessage) } - @Test("Automatically updates localProjectPath for formula if it doesn't match project folder path", .disabled()) // TODO: - + @Test("Automatically updates localProjectPath for formula if it doesn't match project folder path") func updatesFormulaLocalPath() throws { let staleLocalPath = "~/Desktop/stale" let shell = createMockShell() @@ -134,7 +134,7 @@ extension PublishTests { #expect(updatedFormula.localProjectPath == projectFolder.path) } - @Test("Uploads with inline release notes when included in args", .disabled()) // TODO: - + @Test("Uploads with inline release notes when included in args") func uploadsDirectReleaseNotes() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -149,7 +149,7 @@ extension PublishTests { #expect(releaseNoteInfo.content == releaseNotes) } - @Test("Uploads release notes from file when included in args", .disabled()) // TODO: - + @Test("Uploads release notes from file when included in args") func uploadsReleaseNotesFromFile() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell() @@ -179,7 +179,7 @@ extension PublishTests { #expect(!shell.executedCommands.contains(where: { $0.contains("swift test") })) } - @Test("Runs tests when formula includes default test command", .disabled()) // TODO: - + @Test("Runs tests when formula includes default test command") func runsTestsWithDefaultCommand() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell(includeTestCommand: true) @@ -191,7 +191,7 @@ extension PublishTests { #expect(shell.executedCommands.contains { $0.contains("swift test") }) } - @Test("Runs tests when formula includes custom test command", .disabled()) // TODO: - + @Test("Runs tests when formula includes custom test command") func runsTestsWithCustomCommand() throws { let testCommand = "xcodebuild test -scheme testScheme -destination 'platform=macOS'" let gitHandler = MockGitHandler(assetURL: assetURL) @@ -225,7 +225,7 @@ extension PublishTests { #expect(!shell.executedCommands.contains { $0.contains("swift test") }) } - @Test("Fails to publish when tests fail", .disabled()) // TODO: - + @Test("Fails to publish when tests fail") func failsToPublishWhenTestsFail() throws { let gitHandler = MockGitHandler(assetURL: assetURL) let shell = createMockShell(includeTestCommand: false, shouldThrowError: true) From 5ee624e338010e41eba7279a4a531c64aa85d92f Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:44:51 -0800 Subject: [PATCH 10/15] re-enable Publish command tests --- .../{BrewFormula.swift => DecodableFormulaTemplate.swift} | 6 +++--- .../Models/SwiftDataHomebrewFormula+Extensions.swift | 6 +++--- Sources/nnex/Commands/Brew/ImportTap.swift | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) rename Sources/NnexKit/Formula/{BrewFormula.swift => DecodableFormulaTemplate.swift} (92%) 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/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/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 } From a0d8bc6dd24f073c96281e5c362a68db306a59c8 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 12:46:16 -0800 Subject: [PATCH 11/15] re-organize tests --- Tests/nnexTests/BuildTests/BuildExecutableTests.swift | 1 + .../BuildExecutionManagerTests.swift | 0 .../Handlers => HandlerTests}/ReleaseHandlerTests.swift | 0 .../ReleaseNotesFileUtilityTests.swift | 7 ------- .../ReleaseNotesHandlerTests.swift | 0 .../ReleaseVersionHandlerIntegrationTests.swift | 0 .../ReleaseVersionHandlerTests.swift | 0 7 files changed, 1 insertion(+), 7 deletions(-) rename Tests/nnexTests/{Domain/Execution => BuildTests}/BuildExecutionManagerTests.swift (100%) rename Tests/nnexTests/{Domain/Handlers => HandlerTests}/ReleaseHandlerTests.swift (100%) rename Tests/nnexTests/{Domain/Utilities => HandlerTests}/ReleaseNotesFileUtilityTests.swift (98%) rename Tests/nnexTests/{Domain/Handlers => HandlerTests}/ReleaseNotesHandlerTests.swift (100%) rename Tests/nnexTests/{Domain/Handlers => HandlerTests}/ReleaseVersionHandlerIntegrationTests.swift (100%) rename Tests/nnexTests/{Domain/Handlers => HandlerTests}/ReleaseVersionHandlerTests.swift (100%) 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/HandlerTests/ReleaseHandlerTests.swift similarity index 100% rename from Tests/nnexTests/Domain/Handlers/ReleaseHandlerTests.swift rename to Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift 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/ReleaseVersionHandlerIntegrationTests.swift b/Tests/nnexTests/HandlerTests/ReleaseVersionHandlerIntegrationTests.swift similarity index 100% rename from Tests/nnexTests/Domain/Handlers/ReleaseVersionHandlerIntegrationTests.swift rename to Tests/nnexTests/HandlerTests/ReleaseVersionHandlerIntegrationTests.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 From 02ac46549b9b2b030c390030b8238f0d9dc56108 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 13:00:01 -0800 Subject: [PATCH 12/15] add MockDirectoryBrowser --- .../HandlerTests/ReleaseHandlerTests.swift | 498 ++---------------- .../Shared/MockDirectoryBrowser.swift | 36 ++ 2 files changed, 80 insertions(+), 454 deletions(-) create mode 100644 Tests/nnexTests/Shared/MockDirectoryBrowser.swift diff --git a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift index ea1918d..4817d50 100644 --- a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift +++ b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift @@ -1,457 +1,47 @@ -//// -//// 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 +// ReleaseHandlerTests.swift +// nnex // -//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" -//} +// Created by Claude Code on 8/10/25. // -// -//// 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)] -// } -//} + +import Testing +import NnexKit +import Foundation +import GitCommandGen +import NnShellTesting +import SwiftPickerTesting +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 { + +} + + +// MARK: - SUT +private extension ReleaseHandlerTests { + func makeSUT(filePathToReturn: String? = nil, directoryToReturn: (any Directory)? = nil) { + let picker = MockSwiftPicker() + let gitHandler = MockGitHandler() + let filesSystem = MockFileSystem() + let folderBrowser = MockDirectoryBrowser(filePathToReturn: filePathToReturn, directoryToReturn: directoryToReturn) + let sut = ReleaseHandler(picker: picker, gitHandler: gitHandler, fileSystem: filesSystem, folderBrowser: folderBrowser) + } +} + + 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 + } +} From 5d0141f4660f3d7dad2049e09d21310fecf61bdb Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 13:20:08 -0800 Subject: [PATCH 13/15] add tests for ReleaseHandler --- Sources/NnexKit/Releasing/ReleaseStore.swift | 3 +- .../HandlerTests/ReleaseHandlerTests.swift | 162 ++++++++++++++++-- 2 files changed, 153 insertions(+), 12 deletions(-) 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/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift index 4817d50..6287d33 100644 --- a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift +++ b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift @@ -2,7 +2,7 @@ // ReleaseHandlerTests.swift // nnex // -// Created by Claude Code on 8/10/25. +// Created by Nikolai Nobadi on 8/10/25. // import Testing @@ -16,8 +16,10 @@ import NnexSharedTestHelpers @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" @@ -27,21 +29,159 @@ struct ReleaseHandlerTests { } -// MARK: - Unit Tests +// 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") // TODO: - + func extractsVersionFromVersionCase() throws { + let (sut, folder, gitHandler, _) = makeSUT(assetURL: testAssetURL) + let binary = makeArchivedBinary() + let versionInfo = ReleaseVersionInfo.version("v2.5.0") + let releaseNotesSource = ReleaseNotesSource(notes: testReleaseNotes, notesFile: nil) + + let (_, versionNumber) = try sut.uploadRelease(folder: folder, archivedBinaries: [binary], versionInfo: versionInfo, previousVersion: testPreviousVersion, releaseNotesSource: releaseNotesSource) + + #expect(versionNumber == "2.5.0") + #expect(gitHandler.releaseVersion == "2.5.0") + } + + @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(filePathToReturn: String? = nil, directoryToReturn: (any Directory)? = nil) { - let picker = MockSwiftPicker() - let gitHandler = MockGitHandler() - let filesSystem = MockFileSystem() - let folderBrowser = MockDirectoryBrowser(filePathToReturn: filePathToReturn, directoryToReturn: directoryToReturn) - let sut = ReleaseHandler(picker: picker, gitHandler: gitHandler, fileSystem: filesSystem, folderBrowser: folderBrowser) + 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) + } +} From d24189ca9e5f763e92a807d88782e6e166acfb59 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 13:21:19 -0800 Subject: [PATCH 14/15] fix failing unit test --- Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift index 6287d33..b3e5a6e 100644 --- a/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift +++ b/Tests/nnexTests/HandlerTests/ReleaseHandlerTests.swift @@ -140,17 +140,18 @@ extension ReleaseHandlerTests { // MARK: - Version Handling extension ReleaseHandlerTests { - @Test("Extracts version string from version case") // TODO: - + @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("v2.5.0") + 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 == "2.5.0") - #expect(gitHandler.releaseVersion == "2.5.0") + #expect(versionNumber == releaseNumber) + #expect(gitHandler.releaseVersion == releaseNumber) } @Test("Handles version string without v prefix") From 757f912c33ffef09296b1bb28a6a4cc5f6c249c8 Mon Sep 17 00:00:00 2001 From: Nikolai Nobadi Date: Thu, 11 Dec 2025 13:22:13 -0800 Subject: [PATCH 15/15] delete unneeded test file --- ...eleaseVersionHandlerIntegrationTests.swift | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 Tests/nnexTests/HandlerTests/ReleaseVersionHandlerIntegrationTests.swift diff --git a/Tests/nnexTests/HandlerTests/ReleaseVersionHandlerIntegrationTests.swift b/Tests/nnexTests/HandlerTests/ReleaseVersionHandlerIntegrationTests.swift deleted file mode 100644 index f8a64d1..0000000 --- a/Tests/nnexTests/HandlerTests/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 -}