diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a9000..c4505fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.5] - 2025-10-19 + +### Fixed +- Formula files are now correctly published to Formula subfolder within tap directory +- Remove-formula command now properly deletes formula files from Formula subfolder + ## [0.9.4] - 2025-10-18 ### Fixed diff --git a/Sources/NnexKit/Extensions/String+Homebrew.swift b/Sources/NnexKit/Extensions/String+Homebrew.swift index bca1ba6..55541d6 100644 --- a/Sources/NnexKit/Extensions/String+Homebrew.swift +++ b/Sources/NnexKit/Extensions/String+Homebrew.swift @@ -20,4 +20,12 @@ public extension String { return String(self.dropFirst("homebrew-".count)) } -} \ No newline at end of file + + func appendingPathComponent(_ path: String) -> String { + if self.hasSuffix("/") { + return self + path + } + + return self + "/" + path + } +} diff --git a/Sources/NnexKit/Utilities/FormulaPublisher.swift b/Sources/NnexKit/Utilities/FormulaPublisher.swift index 0a2169c..a9d0d02 100644 --- a/Sources/NnexKit/Utilities/FormulaPublisher.swift +++ b/Sources/NnexKit/Utilities/FormulaPublisher.swift @@ -31,12 +31,13 @@ public extension FormulaPublisher { func publishFormula(_ content: String, formulaName: String, commitMessage: String?, tapFolderPath: String) throws -> String { let fileName = "\(formulaName).rb" let tapFolder = try Folder(path: tapFolderPath) + let formulaFolder = try tapFolder.createSubfolderIfNeeded(withName: "Formula") - if tapFolder.containsFile(named: fileName) { - try tapFolder.file(named: fileName).delete() + if formulaFolder.containsFile(named: fileName) { + try formulaFolder.file(named: fileName).delete() } - let newFile = try tapFolder.createFile(named: fileName) + let newFile = try formulaFolder.createFile(named: fileName) try newFile.write(content) if let commitMessage { diff --git a/Sources/nnex/Commands/Brew/RemoveFormula.swift b/Sources/nnex/Commands/Brew/RemoveFormula.swift index 1e80940..5f1de64 100644 --- a/Sources/nnex/Commands/Brew/RemoveFormula.swift +++ b/Sources/nnex/Commands/Brew/RemoveFormula.swift @@ -19,8 +19,8 @@ extension Nnex.Brew { let formulas = try context.loadFormulas() let selection = try picker.requiredSingleSelection(title: "Select a formula to remove", items: formulas) - if let tap = selection.tap, let tapFolder = try? Folder(path: tap.localPath), let formulaFile = try? tapFolder.file(named: "\(selection.name).rb"), picker.getPermission(prompt: "Would you also like to delete the formula file for \(selection.name)?") { - + if let tap = selection.tap, let tapFolder = try? Folder(path: tap.localPath), let formulaFolder = try? tapFolder.subfolder(named: "Formula"), let formulaFile = try? formulaFolder.file(named: "\(selection.name).rb"), picker.getPermission(prompt: "Would you also like to delete the formula file for \(selection.name)?") { + try formulaFile.delete() } diff --git a/Tests/NnexKitTests/FormulaPublisherTests.swift b/Tests/NnexKitTests/FormulaPublisherTests.swift index 31ecc57..62aa781 100644 --- a/Tests/NnexKitTests/FormulaPublisherTests.swift +++ b/Tests/NnexKitTests/FormulaPublisherTests.swift @@ -37,10 +37,11 @@ extension FormulaPublisherTests { let sut = makeSUT().sut let formulaFile = try requireFormulaFile(sut: sut) let savedContents = try formulaFile.readAsString() + let formulaFolder = try getFormulaFolder() #expect(savedContents == content) #expect(formulaFile.name == formulaFileName) - #expect(tapFolder.containsFile(named: formulaFileName)) + #expect(formulaFolder.containsFile(named: formulaFileName)) } @Test("Overwrites existing formula file") @@ -65,8 +66,10 @@ extension FormulaPublisherTests { try requireFormulaFile(sut: sut) + let formulaFolder = try getFormulaFolder() + #expect(gitHandler.message == nil) - #expect(tapFolder.files.count() == 1) + #expect(formulaFolder.files.count() == 1) } @Test("Commits changes and pushes tap folder when a commit message is provided") @@ -76,8 +79,10 @@ extension FormulaPublisherTests { try requireFormulaFile(sut: sut, commitMessage: commitMessage) + let formulaFolder = try getFormulaFolder() + #expect(gitHandler.message == commitMessage) - #expect(tapFolder.files.count() == 1) + #expect(formulaFolder.files.count() == 1) } } @@ -95,11 +100,14 @@ private extension FormulaPublisherTests { // MARK: - Helpers private extension FormulaPublisherTests { + func getFormulaFolder() throws -> Folder { + return try tapFolder.subfolder(named: "Formula") + } + @discardableResult func requireFormulaFile(sut: FormulaPublisher, commitMessage: String? = nil) throws -> File { let path = try sut.publishFormula(content, formulaName: formulaName, commitMessage: commitMessage, tapFolderPath: tapFolder.path) - let file = try File(path: path) - return file + return try .init(path: path) } } diff --git a/Tests/nnexTests/PublishTests/PublishTests.swift b/Tests/nnexTests/PublishTests/PublishTests.swift index e246130..8b679f0 100644 --- a/Tests/nnexTests/PublishTests/PublishTests.swift +++ b/Tests/nnexTests/PublishTests/PublishTests.swift @@ -64,7 +64,7 @@ extension PublishTests { try createTestTapAndFormula(factory: factory) try runCommand(factory, version: .version(versionNumber), message: commitMessage, notes: releaseNotes, buildType: .universal) - let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileName).readAsString() + let formulaFileContents = try getFormulaFolder().file(named: formulaFileName).readAsString() #expect(formulaFileContents.contains(projectName.capitalized)) #expect(formulaFileContents.contains(sha256)) @@ -86,7 +86,7 @@ extension PublishTests { let args = ["brew", "publish", "-p", projectFolderWithDashes.path, "-v", versionNumber, "-m", commitMessage, "-n", releaseNotes] try Nnex.testRun(contextFactory: factory, args: args) - let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileNameWithDashes).readAsString() + let formulaFileContents = try getFormulaFolder().file(named: formulaFileNameWithDashes).readAsString() #expect(formulaFileContents.contains("class \(expectedClassName)")) #expect(!formulaFileContents.contains("class \(projectWithDashes)")) @@ -243,7 +243,7 @@ extension PublishTests { // MARK: - Input Provided from User extension PublishTests { - @Test("Publishes a binary to Homebrew and verifies the formula file when infomation must be input and file path for release notes is input.") + @Test("Publishes a binary to Homebrew and verifies the formula file when infomation must be input and file path for release notes is input.") func publishCommandWithInputsAndFilePathReleaseNotes() throws { let releaseNoteFile = try projectFolder.createFile(named: "TestReleaseNotes.md") let filePath = releaseNoteFile.path @@ -257,7 +257,7 @@ extension PublishTests { try runCommand(factory) let releaseNoteInfo = try #require(gitHandler.releaseNoteInfo) - let formulaFileContents = try Folder(path: tapFolder.path).file(named: formulaFileName).readAsString() + let formulaFileContents = try getFormulaFolder().file(named: formulaFileName).readAsString() #expect(formulaFileContents.contains(projectName.capitalized)) #expect(formulaFileContents.contains(sha256)) @@ -322,6 +322,10 @@ private extension PublishTests { // MARK: - Helpers private extension PublishTests { + func getFormulaFolder() throws -> Folder { + return try tapFolder.subfolder(named: "Formula") + } + func createMockShell(projectName: String? = nil, projectPath: String? = nil, includeTestCommand: Bool = false, shouldThrowError: Bool = false) -> MockShell { if shouldThrowError { return .init(shouldThrowErrorOnFinal: true) diff --git a/Tests/nnexTests/RemoveFormulaTests/RemoveFormulaTests.swift b/Tests/nnexTests/RemoveFormulaTests/RemoveFormulaTests.swift new file mode 100644 index 0000000..729c350 --- /dev/null +++ b/Tests/nnexTests/RemoveFormulaTests/RemoveFormulaTests.swift @@ -0,0 +1,138 @@ +// +// RemoveFormulaTests.swift +// nnex +// +// Created by Nikolai Nobadi on 3/31/25. +// + +import NnexKit +import Testing +import Foundation +import NnexSharedTestHelpers +@testable import nnex +@preconcurrency import Files + +@MainActor +final class RemoveFormulaTests { + private let tapName = "testTap" + private let tempFolder: Folder + private let tapFolder: Folder + + init() throws { + self.tempFolder = try Folder.temporary.createSubfolder(named: "RemoveFormulaTests_\(UUID().uuidString)") + self.tapFolder = try tempFolder.createSubfolder(named: tapName.homebrewTapName) + } + + deinit { + deleteFolderContents(tempFolder) + try? tempFolder.delete() + } +} + + +// MARK: - Unit Tests +extension RemoveFormulaTests { + @Test("ensures no formulas exist in database") + func startingValuesEmpty() throws { + let testFactory = MockContextFactory() + let context = try testFactory.makeContext() + + #expect(try context.loadFormulas().isEmpty) + } + + @Test("Removes formula from database") + func removesFormulaFromDatabase() throws { + let name = "testFormula" + let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path) + + try createTestTapAndFormula(factory: testFactory, formulaName: name) + + let context = try testFactory.makeContext() + let formulasBeforeRemoval = try context.loadFormulas() + #expect(formulasBeforeRemoval.count == 1) + + try runCommand(testFactory) + + let formulasAfterRemoval = try context.loadFormulas() + #expect(formulasAfterRemoval.isEmpty) + } + + @Test("Deletes formula file from Formula folder when user confirms") + func deletesFormulaFileWhenUserConfirms() throws { + let name = "testFormula" + let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [true]) + + try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: true) + + let formulaFolder = try tapFolder.subfolder(named: "Formula") + #expect(formulaFolder.containsFile(named: "\(name).rb")) + + try runCommand(testFactory) + + let updatedFormulaFolder = try Folder(path: formulaFolder.path) + #expect(!updatedFormulaFolder.containsFile(named: "\(name).rb")) + + let context = try testFactory.makeContext() + #expect(try context.loadFormulas().isEmpty) + } + + @Test("Keeps formula file in Formula folder when user declines deletion") + func keepsFormulaFileWhenUserDeclines() throws { + let name = "testFormula" + let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [false]) + + try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: true) + + let formulaFolder = try tapFolder.subfolder(named: "Formula") + #expect(formulaFolder.containsFile(named: "\(name).rb")) + + try runCommand(testFactory) + + let updatedFormulaFolder = try Folder(path: formulaFolder.path) + #expect(updatedFormulaFolder.containsFile(named: "\(name).rb")) + + let context = try testFactory.makeContext() + #expect(try context.loadFormulas().isEmpty) + } + + @Test("Handles missing Formula folder gracefully") + func handlesMissingFormulaFolder() throws { + let name = "testFormula" + let testFactory = MockContextFactory(tapListFolderPath: tempFolder.path, permissionResponses: [true]) + + try createTestTapAndFormula(factory: testFactory, formulaName: name, createFormulaFile: false) + + let context = try testFactory.makeContext() + let formulasBeforeRemoval = try context.loadFormulas() + #expect(formulasBeforeRemoval.count == 1) + + try runCommand(testFactory) + + let formulasAfterRemoval = try context.loadFormulas() + #expect(formulasAfterRemoval.isEmpty) + } +} + + +// MARK: - Helper Methods +private extension RemoveFormulaTests { + func runCommand(_ testFactory: MockContextFactory) throws { + let args = ["brew", "remove-formula"] + try Nnex.testRun(contextFactory: testFactory, args: args) + } + + func createTestTapAndFormula(factory: MockContextFactory, formulaName: String, createFormulaFile: Bool = false) throws { + let context = try factory.makeContext() + let tap = SwiftDataTap(name: tapName, localPath: tapFolder.path, remotePath: "https://github.com/user/\(tapName)") + let formula = SwiftDataFormula(name: formulaName, details: "formula details", homepage: "https://github.com/user/\(formulaName)", license: "MIT", localProjectPath: "/path/to/project", uploadType: .binary, testCommand: nil, extraBuildArgs: []) + + try context.saveNewTap(tap, formulas: [formula]) + + if createFormulaFile { + let formulaFolder = try tapFolder.createSubfolderIfNeeded(withName: "Formula") + let formulaContent = FormulaContentGenerator.makeFormulaFileContent(name: formulaName, details: "formula details", homepage: "https://github.com/user/\(formulaName)", license: "MIT", assetURL: "https://example.com/asset", sha256: "abc123") + let formulaFile = try formulaFolder.createFile(named: "\(formulaName).rb") + try formulaFile.write(formulaContent) + } + } +}