diff --git a/Piper.xcodeproj/project.pbxproj b/Piper.xcodeproj/project.pbxproj index c7de080..55a9a07 100644 --- a/Piper.xcodeproj/project.pbxproj +++ b/Piper.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ membershipExceptions = ( UITests/UITests.swift, UITests/UITestsLaunchTests.swift, + UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift, UnitTests/UnitTests.swift, ); target = 46D4BE762CB06ED300FCFB84 /* Piper */; @@ -42,6 +43,7 @@ 46D4BEF72CB07CCB00FCFB84 /* Exceptions for "Tests" folder in "UnitTests" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( + UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift, UnitTests/UnitTests.swift, ); target = 46D4BED32CB07C7A00FCFB84 /* UnitTests */; @@ -492,6 +494,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 7FMNM89WKG; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.piper.tests.unit"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -509,6 +512,7 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 7FMNM89WKG; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.rock-n-code.piper.tests.unit"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Piper/Sources/Logic/UseCases/RunProcessUseCase.swift b/Piper/Sources/Logic/UseCases/RunProcessUseCase.swift new file mode 100644 index 0000000..29e70eb --- /dev/null +++ b/Piper/Sources/Logic/UseCases/RunProcessUseCase.swift @@ -0,0 +1,87 @@ +// +// RunProcessUseCase.swift +// Piper ~ App +// +// Created by Javier Cicchelli on 05/10/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import Foundation + +struct RunProcessUseCase { + + // MARK: Type aliases + + typealias Output = String + + // MARK: Properties + + private let path: String + private let arguments: [String]? + + // MARK: Initialisers + + init( + path: String, + arguments: [String] = [] + ) { + self.path = path + self.arguments = arguments + } + + // MARK: Functions + + func callAsFunction() async throws -> Output { + let process = Process() + + process.executableURL = URL(fileURLWithPath: path) + process.arguments = arguments + + let pipeError = Pipe() + let pipeOutput = Pipe() + + process.standardError = pipeError + process.standardOutput = pipeOutput + + async let streamOutput = pipeOutput.availableData.append() + async let streamError = pipeError.availableData.append() + + do { + try process.run() + + let dataOutput = await streamOutput + let dataError = await streamError + + guard dataError.isEmpty else { + guard let errorOutput = String(data: dataError, encoding: .utf8) else { + throw RunProcessError.unexpected + } + + throw RunProcessError.output(errorOutput) + } + + guard let output = String(data: dataOutput, encoding: .utf8) else { + throw RunProcessError.unexpected + } + + return await withCheckedContinuation { continuation in + process.terminationHandler = { _ in + continuation.resume(returning: output) + } + } + } catch let error as RunProcessError { + throw error + } catch { + throw RunProcessError.captured(error) + } + } + +} + +// MARK: - Errors + +enum RunProcessError: Error { + case captured(_ error: Error) + case output(_ output: String) + case unexpected +} diff --git a/Tests/UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift b/Tests/UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift new file mode 100644 index 0000000..320ad4b --- /dev/null +++ b/Tests/UnitTests/Tests/UseCases/RunProcessUseCaseTests.swift @@ -0,0 +1,69 @@ +// +// RunProcessUseCaseTests.swift +// UnitTests +// +// Created by Javier Cicchelli on 05/10/2024. +// Copyright © 2024 Röck+Cöde. All rights reserved. +// + +import Testing + +@testable import Piper + +struct RunProcessUseCaseTests { + + // MARK: Functions tests + + @Test("Run a simple process without arguments") + func simpleCommand_withoutArguments() async throws { + // GIVEN + let useCase = RunProcessUseCase(path: .ls) + + // WHEN + let output = try await useCase() + + // THEN + #expect(output.isEmpty == false) + } + + @Test("Run a simple process with arguments") + func simpleCommand_withArguments() async throws { + // GIVEN + let useCase = RunProcessUseCase( + path: .ls, + arguments: ["-la", "."] + ) + + // WHEN + let output = try await useCase() + + // THEN + #expect(output.isEmpty == false) + } + + @Test("Run a simple command with arguments that throws an error") + func simpleCommand_withArguments_throwsError() async throws { + // GIVEN + let useCase = RunProcessUseCase( + path: .ls, + arguments: ["-la", "~"] + ) + + // WHEN + // THEN + await #expect(throws: RunProcessError.self) { + try await useCase() + } + } + +} + +// MARK: - String+Constants + +private extension String { + + // MARK: Constants + + static let ls = "/bin/ls" + +}