Skip to content
20 changes: 17 additions & 3 deletions Sources/nnex/Commands/Brew/ImportTap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ extension Nnex.Brew {
let path = try path ?? Nnex.makePicker().getRequiredInput(prompt: "Enter the local path to your Homebrew tap folder.")
let folder = try Folder(path: path)
let tapName = folder.name.removingHomebrewPrefix
let formulaFiles = folder.files.filter({ $0.extension == "rb" })
let remotePath = try Nnex.makeGitHandler().getRemoteURL(path: folder.path)

let formulaFiles: [File]
if folder.containsSubfolder(named: "Formula") {
let formulaFolder = try folder.subfolder(named: "Formula")
formulaFiles = formulaFolder.files.filter({ $0.extension == "rb" })
} else {
print("⚠️ Warning: No 'Formula' folder found in tap directory. Skipping formula import.".red)
formulaFiles = []
}

let tap = SwiftDataTap(name: tapName, localPath: folder.path, remotePath: remotePath)

var formulas: [SwiftDataFormula] = []
Expand All @@ -49,8 +58,13 @@ private extension Nnex.Brew.ImportTap {
/// - Returns: A BrewFormula instance if decoding is successful, or nil otherwise.
/// - Throws: An error if the decoding process fails.
func decodeBrewFormula(_ file: File) throws -> BrewFormula? {
let output = try makeBrewOutput(filePath: file.path)

let output: String
do {
output = try makeBrewOutput(filePath: file.path)
} catch {
output = ""
}

if output.isEmpty || output.contains("⚠️⚠️⚠️") {
let formulaContent = try file.readAsString()
let name = extractField(from: formulaContent, pattern: #"class (\w+) < Formula"#) ?? "Unknown"
Expand Down
3 changes: 2 additions & 1 deletion Sources/nnex/Domain/Execution/CreateTapManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ extension CreateTapManager {
let tapListFolder = try getTapListFolder()
let homebrewTapName = tapName.homebrewTapName
let tapFolder = try tapListFolder.createSubfolder(named: homebrewTapName)

try tapFolder.createSubfolder(named: "Formula")

print("Created folder for new tap named \(tapName) at \(tapFolder.path)")

let projectDetails = try details ?? picker.getRequiredInput(prompt: "Enter the details for this new tap")
Expand Down
10 changes: 5 additions & 5 deletions Tests/NnexKitTests/FormulaPublisherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ extension FormulaPublisherTests {
func createsNewFormulaFile() throws {
let sut = makeSUT().sut
let formulaFile = try requireFormulaFile(sut: sut)
let savedContents = try #require(try formulaFile.readAsString())
let savedContents = try formulaFile.readAsString()

#expect(savedContents == content)
#expect(formulaFile.name == formulaFileName)
Expand All @@ -47,12 +47,12 @@ extension FormulaPublisherTests {
func overwritesExistingFormulaFile() throws {
let sut = makeSUT().sut
let previousContent = "previous content"
let previousFile = try #require(try tapFolder.createFile(named: formulaFileName))
let previousFile = try tapFolder.createFile(named: formulaFileName)

try previousFile.write(previousContent)

let formulaFile = try requireFormulaFile(sut: sut)
let savedContents = try #require(try formulaFile.readAsString())
let savedContents = try formulaFile.readAsString()

#expect(savedContents == content)
#expect(formulaFile.name == formulaFileName)
Expand Down Expand Up @@ -97,8 +97,8 @@ private extension FormulaPublisherTests {
private extension FormulaPublisherTests {
@discardableResult
func requireFormulaFile(sut: FormulaPublisher, commitMessage: String? = nil) throws -> File {
let path = try #require(try sut.publishFormula(content, formulaName: formulaName, commitMessage: commitMessage, tapFolderPath: tapFolder.path))
let file = try #require(try File(path: path))
let path = try sut.publishFormula(content, formulaName: formulaName, commitMessage: commitMessage, tapFolderPath: tapFolder.path)
let file = try File(path: path)

return file
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/nnexTests/ArchiveTests/ProjectDetectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@ extension ProjectDetectorTests {
@Test("Throws error when no Xcode project exists")
func throwsErrorWhenNoXcodeProjectExists() throws {
let sut = makeSUT()
let tempFolderPath = tempFolder.path

// Create some non-Xcode files
try tempFolder.createFile(named: "README.md")
try tempFolder.createFile(named: "Package.swift")

#expect(throws: ArchiveError.self) {
try sut.detectProject(at: tempFolder.path)
try sut.detectProject(at: tempFolderPath)
}
}

Expand Down
11 changes: 7 additions & 4 deletions Tests/nnexTests/BuildTests/BuildExecutableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ extension BuildTests {
func failsWithoutPackageManifest() throws {
let factory = MockContextFactory()

#expect(throws: (any Error).self) {
do {
try runCommand(factory)
}
Issue.record("Expected an error to be thrown")
} catch { }
}

@Test("Clean flag defaults to true and sets skipClean to false")
Expand Down Expand Up @@ -173,9 +174,10 @@ extension BuildTests {

try createPackageManifest(name: executableName)

#expect(throws: (any Error).self) {
do {
try runCommand(factory)
}
Issue.record("Expected an error to be thrown")
} catch { }
}

@Test("Copies binary to selected output location")
Expand Down Expand Up @@ -214,6 +216,7 @@ extension BuildTests {

// MARK: - Helpers
private extension BuildTests {
@discardableResult
func runCommand(_ factory: MockContextFactory, openInFinder: Bool = false, clean: Bool = true) throws -> String {
var args = ["build", "-p", projectFolder.path]
if openInFinder {
Expand Down
51 changes: 35 additions & 16 deletions Tests/nnexTests/CreateTapTests/CreateTapTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ extension CreateTapTests {
func createTapFailsWithoutGHCLI() throws {
let gitHandler = MockGitHandler(ghIsInstalled: false)
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, gitHandler: gitHandler)
#expect(throws: NnexError.missingGitHubCLI) {

do {
try runCommand(factory)
}
Issue.record("Expected an error to be thrown")
} catch { }
}

@Test("Creates new tap folder with 'homebrew-' prefix when name from arg does not include the prefix")
Expand All @@ -59,14 +60,31 @@ extension CreateTapTests {
let remoteURL = "remoteURL"
let gitHandler = MockGitHandler(remoteURL: remoteURL)
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, gitHandler: gitHandler)

try runCommand(factory, name: name)

let updatedTapListFolder = try Folder(path: tapListFolder.path)
let tapFolder = try #require(try updatedTapListFolder.subfolder(named: tapName))
let tapFolder = try updatedTapListFolder.subfolder(named: tapName)

#expect(tapFolder.name == tapName)
}

@Test("Creates Formula subfolder in new tap folder")
func createsFormulaSubfolder() throws {
let name = "myNewTap"
let tapName = name.homebrewTapName
let remoteURL = "remoteURL"
let gitHandler = MockGitHandler(remoteURL: remoteURL)
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, gitHandler: gitHandler)

try runCommand(factory, name: name)

let updatedTapListFolder = try Folder(path: tapListFolder.path)
let tapFolder = try updatedTapListFolder.subfolder(named: tapName)
let formulaFolder = try tapFolder.subfolder(named: "Formula")

#expect(formulaFolder.name == "Formula")
}

@Test("Creates new tap folder with 'homebrew-' prefix when name from input does not include the prefix")
func createsTapFolderWithNameInput() throws {
Expand All @@ -77,20 +95,21 @@ extension CreateTapTests {
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, inputResponses: [name], gitHandler: gitHandler)

try runCommand(factory)

let updatedTapListFolder = try Folder(path: tapListFolder.path)
let tapFolder = try #require(try updatedTapListFolder.subfolder(named: tapName))
let tapFolder = try updatedTapListFolder.subfolder(named: tapName)

#expect(tapFolder.name == tapName)
}

@Test("Throws error when name from input is empty")
func createsTapInvalidNameError() throws {
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, inputResponses: [""])
#expect(throws: NnexError.invalidTapName) {

do {
try runCommand(factory)
}
Issue.record("Expected an error to be thrown")
} catch { }
}

// TODO: - need to verify other Tap properties
Expand All @@ -114,9 +133,9 @@ extension CreateTapTests {
let factory = MockContextFactory(tapListFolderPath: tapListFolder.path, gitHandler: gitHandler)

try runCommand(factory, name: name)
let tapFolder = try #require(try Folder(path: tapListFolder.path).subfolder(named: tapName))

let tapFolder = try Folder(path: tapListFolder.path).subfolder(named: tapName)

#expect(gitHandler.gitInitPath == tapFolder.path)
#expect(gitHandler.remoteTapName == tapName)
#expect(gitHandler.remoteTapPath == tapFolder.path)
Expand Down
44 changes: 25 additions & 19 deletions Tests/nnexTests/Domain/Execution/BuildExecutionManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,14 @@ extension BuildExecutionManagerTests {
@Test("Successfully executes build with multiple executables requiring selection")
func successfullyExecutesBuildWithMultipleExecutables() throws {
try createPackageSwiftWithMultipleExecutables()

let sut = makeSUT(selectedItemIndices: [0, 0]).sut

let path = projectFolder.path

#expect(throws: Never.self) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}

// Build should complete successfully with multiple executables
}

Expand Down Expand Up @@ -116,60 +117,65 @@ extension BuildExecutionManagerTests {
@Test("Throws error when picker fails to select executable")
func throwsErrorWhenPickerFailsToSelectExecutable() throws {
try createPackageSwiftWithMultipleExecutables()

let sut = makeSUT(throwPickerError: true).sut

let path = projectFolder.path

#expect(throws: BuildExecutionError.failedToSelectExecutable(reason: "MockPicker error")) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}
}

@Test("Throws error when custom path is invalid")
func throwsErrorWhenCustomPathIsInvalid() throws {
try createPackageSwift(executableName: executableName)

let (sut, _) = makeSUT(
selectedItemIndices: [2], // Select custom location
inputResponses: ["/nonexistent/path"] // Invalid path
)

let path = projectFolder.path

#expect(throws: BuildExecutionError.invalidCustomPath(path: "/nonexistent/path")) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}
}

@Test("Throws error when user cancels custom path confirmation")
func throwsErrorWhenUserCancelsCustomPathConfirmation() throws {
try createPackageSwift(executableName: executableName)

let (sut, _) = makeSUT(
selectedItemIndices: [2], // Select custom location
inputResponses: ["/tmp"], // Valid path
permissionResponses: [false] // Cancel confirmation
)

let path = projectFolder.path

#expect(throws: BuildExecutionError.buildCancelledByUser) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}
}

@Test("Propagates ExecutableNameResolver errors")
func propagatesExecutableNameResolverErrors() throws {
let sut = makeSUT().sut

#expect(throws: ExecutableNameResolverError.missingPackageSwift(path: projectFolder.path)) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
let path = projectFolder.path

#expect(throws: ExecutableNameResolverError.missingPackageSwift(path: path)) {
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}
}

@Test("Propagates build errors from ProjectBuilder")
func propagatesBuildErrorsFromProjectBuilder() throws {
try createPackageSwift(executableName: executableName)

let sut = makeSUT(selectedItemIndices: [0], throwShellError: true).sut

let path = projectFolder.path

#expect(throws: (any Error).self) {
try sut.executeBuild(projectPath: projectFolder.path, buildType: .universal, clean: true, openInFinder: false)
try sut.executeBuild(projectPath: path, buildType: .universal, clean: true, openInFinder: false)
}
}
}
Expand Down
Loading