Implemented the TerminalService service for the library target
This commit is contained in:
parent
33ae67fc58
commit
07e8012faf
19
Library/Sources/Public/Protocols/TerminalServicing.swift
Normal file
19
Library/Sources/Public/Protocols/TerminalServicing.swift
Normal 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
|
||||||
|
}
|
||||||
|
|
60
Library/Sources/Public/Services/TerminalService.swift
Normal file
60
Library/Sources/Public/Services/TerminalService.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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 = ""
|
||||||
|
}
|
73
Test/Sources/Helpers/Mocks/TerminalServiceMock.swift
Normal file
73
Test/Sources/Helpers/Mocks/TerminalServiceMock.swift
Normal 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 = ""
|
||||||
|
}
|
39
Test/Sources/Helpers/Spies/TerminalServiceSpy.swift
Normal file
39
Test/Sources/Helpers/Spies/TerminalServiceSpy.swift
Normal 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 = ""
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user