Implemented the TerminalService service for the library target

This commit is contained in:
Javier Cicchelli 2025-02-08 10:22:52 +01:00
parent 33ae67fc58
commit 07e8012faf
6 changed files with 243 additions and 0 deletions

View File

@ -0,0 +1,19 @@
import Foundation
public protocol TerminalServicing {
// MARK: Functions
@discardableResult
func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String
}
// MARK: - Errors
public enum TerminalServiceError: Error, Equatable {
case captured(_ output: String)
case output(_ output: String)
case unexpected
}

View File

@ -0,0 +1,60 @@
import Foundation
public struct TerminalService {
// MARK: Initialisers
public init() {}
}
// MARK: - TerminalServicing
extension TerminalService: TerminalServicing {
// MARK: Functions
public func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String {
let process = Process()
let standardError = Pipe()
let standardOutput = Pipe()
process.executableURL = executableURL
process.arguments = arguments
process.standardError = standardError
process.standardOutput = standardOutput
async let streamOutput = standardOutput.availableData.append()
async let streamError = standardError.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 TerminalServiceError.unexpected
}
throw TerminalServiceError.output(errorOutput)
}
guard let output = String(data: dataOutput, encoding: .utf8) else {
throw TerminalServiceError.unexpected
}
return await withCheckedContinuation { continuation in
process.terminationHandler = { _ in
continuation.resume(returning: output)
}
}
} catch let error as TerminalServiceError {
throw error
} catch {
throw .captured(error.localizedDescription)
}
}
}

View File

@ -26,6 +26,7 @@ struct TemplateServiceTests {
@Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered]) @Test(arguments: [TemplateServiceError.serviceNotInitialized, .resourcePathNotFound, .templateNotFound, .contentNotRendered])
func render(throws error: TemplateServiceError) async throws { func render(throws error: TemplateServiceError) async throws {
// GIVEN
let service = TemplateServiceMock(action: .error(error), spy: spy) let service = TemplateServiceMock(action: .error(error), spy: spy)
// WHEN // WHEN

View File

@ -0,0 +1,51 @@
import ColibriLibrary
import Foundation
import Testing
struct TerminalServiceTests {
// MARK: Properties
private let spy = TerminalServiceSpy()
// MARK: Functions
@Test(arguments: [URL.someNewFile], [[], ["--example"], ["--example", "--more", "--etc"]])
func run(with executableURL: URL, and arguments: [String]) async throws {
// GIVEN
let service = TerminalServiceMock(action: .run(executableURL, arguments), spy: spy)
// WHEN
let result = try await service.run(executableURL, arguments: arguments)
// THEN
#expect(result == .content)
#expect(spy.actions.isEmpty == false)
let action = try #require(spy.actions.last)
#expect(action == .ran(executableURL, arguments))
}
@Test(arguments: [TerminalServiceError.unexpected, .captured("Some captured error"), .output("Some output error")])
func run(throws error: TerminalServiceError) async throws {
// GIVEN
let service = TerminalServiceMock(action: .error(error), spy: spy)
// WHEN
// THEN
await #expect(throws: error) {
try await service.run(URL.someNewFile, arguments: [])
}
#expect(spy.actions.isEmpty == true)
}
}
// MARK: - String+Constants
private extension String {
static let content = ""
}

View File

@ -0,0 +1,73 @@
import ColibriLibrary
import Foundation
final class TerminalServiceMock {
// MARK: Properties
private var actions: [Action] = []
private weak var spy: TerminalServiceSpy?
// MARK: Initialisers
init(
action: Action,
spy: TerminalServiceSpy? = nil
) {
self.actions.append(action)
self.spy = spy
}
}
// MARK: - TerminalServicing
extension TerminalServiceMock: TerminalServicing {
// MARK: Functions
func run(_ executableURL: URL, arguments: [String]) async throws (TerminalServiceError) -> String {
guard let nextAction else { return .empty }
switch nextAction {
case .error(let error):
throw error
case let .run(executableURL, arguments):
try await spy?.run(executableURL, arguments: arguments)
return .empty
}
}
}
// MARK: - Helpers
private extension TerminalServiceMock {
// MARK: Computed
var nextAction: Action? {
guard !actions.isEmpty else {
return nil
}
return actions.removeFirst()
}
}
// MARK: - Actions
extension TerminalServiceMock {
enum Action {
case error(TerminalServiceError)
case run(URL, [String])
}
}
// MARK: - String+Constants
private extension String {
static let empty = ""
}

View File

@ -0,0 +1,39 @@
import ColibriLibrary
import Foundation
final class TerminalServiceSpy {
// MARK: Properties
private(set) var actions: [Action] = []
}
// MARK: - TerminalServicing
extension TerminalServiceSpy: TerminalServicing {
// MARK: Functions
@discardableResult
func run(_ executableURL: URL, arguments: [String]) async throws(TerminalServiceError) -> String {
actions.append(.ran(executableURL, arguments))
return .content
}
}
// MARK: - Actions
extension TerminalServiceSpy {
enum Action: Equatable {
case ran(_ executableURL: URL, _ arguments: [String])
}
}
// MARK: - String+Constants
private extension String {
static let content = ""
}