feat: Adds createProject and createJson tests for playbook-client.

This commit is contained in:
2024-12-15 11:59:50 -05:00
parent bc0b740f95
commit 6d0108da0c
5 changed files with 326 additions and 35 deletions

View File

@@ -109,6 +109,7 @@ let package = Package(
.target( .target(
name: "PlaybookClient", name: "PlaybookClient",
dependencies: [ dependencies: [
"CodersClient",
"CommandClient", "CommandClient",
"ConfigurationClient", "ConfigurationClient",
"FileClient", "FileClient",

View File

@@ -28,9 +28,16 @@ extension PlaybookClient.RunPlaybook.BuildOptions {
extension PlaybookClient.RunPlaybook.CreateOptions { extension PlaybookClient.RunPlaybook.CreateOptions {
func run() async throws { func run(encoder jsonEncoder: JSONEncoder?) async throws {
try await shared.run { arguments, configuration in try await shared.run { arguments, configuration in
let json = try createJSONData(configuration: configuration) let jsonData = try createJSONData(
configuration: configuration,
encoder: jsonEncoder
)
guard let json = String(data: jsonData, encoding: .utf8) else {
throw PlaybookClientError.encodingError
}
arguments.append(contentsOf: [ arguments.append(contentsOf: [
"--tags", "setup-project", "--tags", "setup-project",
@@ -130,17 +137,20 @@ public extension PlaybookClient.RunPlaybook {
// want the output to be `prettyPrinted` or anything, unless we're running // want the output to be `prettyPrinted` or anything, unless we're running
// tests, so we use a supplied json encoder. // tests, so we use a supplied json encoder.
// //
extension PlaybookClient.RunPlaybook.CreateOptions { @_spi(Internal)
public extension PlaybookClient.RunPlaybook.CreateOptions {
func createJSONData( func createJSONData(
configuration: Configuration, configuration: Configuration,
encoder: JSONEncoder = .init() encoder: JSONEncoder?
) throws -> Data { ) throws -> Data {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
let templateDir = template.directory ?? configuration.template.directory let encoder = encoder ?? jsonEncoder
let templateRepo = template.url ?? configuration.template.url
let version = template.version ?? configuration.template.version let templateDir = template?.directory ?? configuration.template.directory
let templateRepo = template?.url ?? configuration.template.url
let version = template?.version ?? configuration.template.version
logger.debug(""" logger.debug("""
(\(useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo))) (\(useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo)))
@@ -203,3 +213,9 @@ private struct TemplateRepo: Encodable {
let version: String let version: String
} }
} }
private let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.withoutEscapingSlashes, .sortedKeys]
return encoder
}()

View File

@@ -43,7 +43,11 @@ public extension PlaybookClient {
@DependencyClient @DependencyClient
struct RunPlaybook: Sendable { struct RunPlaybook: Sendable {
public var buildProject: @Sendable (BuildOptions) async throws -> Void public var buildProject: @Sendable (BuildOptions) async throws -> Void
public var createProject: @Sendable (CreateOptions) async throws -> Void public var createProject: @Sendable (CreateOptions, JSONEncoder?) async throws -> Void
public func createProject(_ options: CreateOptions) async throws {
try await createProject(options, nil)
}
public struct SharedRunOptions: Equatable, Sendable { public struct SharedRunOptions: Equatable, Sendable {
public let extraOptions: [String]? public let extraOptions: [String]?
@@ -53,11 +57,11 @@ public extension PlaybookClient {
public let shell: String? public let shell: String?
public init( public init(
extraOptions: [String]?, extraOptions: [String]? = nil,
inventoryFilePath: String?, inventoryFilePath: String? = nil,
loggingOptions: LoggingOptions, loggingOptions: LoggingOptions,
quiet: Bool, quiet: Bool = false,
shell: String? shell: String? = nil
) { ) {
self.extraOptions = extraOptions self.extraOptions = extraOptions
self.inventoryFilePath = inventoryFilePath self.inventoryFilePath = inventoryFilePath
@@ -73,12 +77,20 @@ public extension PlaybookClient {
public let shared: SharedRunOptions public let shared: SharedRunOptions
public init( public init(
extraOptions: [String]?, projectDirectory: String? = nil,
inventoryFilePath: String?, shared: SharedRunOptions
) {
self.projectDirectory = projectDirectory
self.shared = shared
}
public init(
extraOptions: [String]? = nil,
inventoryFilePath: String? = nil,
loggingOptions: LoggingOptions, loggingOptions: LoggingOptions,
quiet: Bool, quiet: Bool = false,
shell: String?, shell: String? = nil,
projectDirectory: String projectDirectory: String? = nil
) { ) {
self.projectDirectory = projectDirectory self.projectDirectory = projectDirectory
self.shared = .init( self.shared = .init(
@@ -99,18 +111,30 @@ public extension PlaybookClient {
public struct CreateOptions: Equatable, Sendable { public struct CreateOptions: Equatable, Sendable {
public let projectDirectory: String public let projectDirectory: String
public let shared: SharedRunOptions public let shared: SharedRunOptions
public let template: Configuration.Template public let template: Configuration.Template?
public let useLocalTemplateDirectory: Bool public let useLocalTemplateDirectory: Bool
public init( public init(
extraOptions: [String]?, projectDirectory: String,
inventoryFilePath: String?, shared: SharedRunOptions,
template: Configuration.Template? = nil,
useLocalTemplateDirectory: Bool
) {
self.projectDirectory = projectDirectory
self.shared = shared
self.template = template
self.useLocalTemplateDirectory = useLocalTemplateDirectory
}
public init(
extraOptions: [String]? = nil,
inventoryFilePath: String? = nil,
loggingOptions: LoggingOptions, loggingOptions: LoggingOptions,
projectDirectory: String, projectDirectory: String,
quiet: Bool, quiet: Bool = false,
shell: String?, shell: String? = nil,
template: Configuration.Template, template: Configuration.Template? = nil,
useLocalTemplateDirectory: Bool useLocalTemplateDirectory: Bool = false
) { ) {
self.projectDirectory = projectDirectory self.projectDirectory = projectDirectory
self.template = template self.template = template
@@ -145,7 +169,7 @@ extension PlaybookClient.RunPlaybook: DependencyKey {
public static var liveValue: PlaybookClient.RunPlaybook { public static var liveValue: PlaybookClient.RunPlaybook {
.init( .init(
buildProject: { try await $0.run() }, buildProject: { try await $0.run() },
createProject: { try await $0.run() } createProject: { try await $0.run(encoder: $1) }
) )
} }
} }

View File

@@ -1,6 +1,7 @@
import Foundation import Foundation
enum PlaybookClientError: Error { enum PlaybookClientError: Error {
case encodingError
case projectDirectoryNotFound case projectDirectoryNotFound
case templateDirectoryNotFound case templateDirectoryNotFound
case templateDirectoryOrRepoNotSpecified case templateDirectoryOrRepoNotSpecified

View File

@@ -1,3 +1,5 @@
import CodersClient
@_spi(Internal) import CommandClient
import ConfigurationClient import ConfigurationClient
import Dependencies import Dependencies
import FileClient import FileClient
@@ -8,13 +10,24 @@ import Testing
import TestSupport import TestSupport
@Suite("PlaybookClientTests") @Suite("PlaybookClientTests")
struct PlaybookClientTests { struct PlaybookClientTests: TestCase {
@Test static let loggingOptions: LoggingOptions = {
func installation() async throws { let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug"
let logLevel = Logger.Level(rawValue: levelString) ?? .debug
return .init(commandName: "PlaybookClientTests", logLevel: logLevel)
}()
static var sharedRunOptions: PlaybookClient.RunPlaybook.SharedRunOptions {
.init(loggingOptions: loggingOptions)
}
@Test(.tags(.repository))
func repositoryInstallation() async throws {
try await withDependencies { try await withDependencies {
$0.fileClient = .liveValue $0.fileClient = .liveValue
$0.asyncShellClient = .liveValue $0.asyncShellClient = .liveValue
$0.commandClient = .liveValue
} operation: { } operation: {
try await withTemporaryDirectory { tempDirectory in try await withTemporaryDirectory { tempDirectory in
let pathUrl = tempDirectory.appending(path: "playbook") let pathUrl = tempDirectory.appending(path: "playbook")
@@ -23,20 +36,256 @@ struct PlaybookClientTests {
let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath)) let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath))
try? FileManager.default.removeItem(at: pathUrl) try? FileManager.default.removeItem(at: pathUrl)
try await playbookClient.installPlaybook(configuration) try await playbookClient.repository.install(configuration)
let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath) let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath)
#expect(exists) #expect(exists)
} }
} }
} }
@Test(arguments: [ @Test(
(Configuration(), PlaybookClient.Constants.defaultInstallationPath), .tags(.repository),
(Configuration(playbook: .init(directory: "playbook")), "playbook") arguments: [
]) (Configuration(), PlaybookClient.Constants.defaultInstallationPath),
func playbookDirectory(configuration: Configuration, expected: String) async throws { (Configuration(playbook: .init(directory: "playbook")), "playbook")
]
)
func repositoryDirectory(configuration: Configuration, expected: String) async throws {
let client = PlaybookClient.liveValue let client = PlaybookClient.liveValue
let result = try await client.playbookDirectory(configuration) let result = try await client.repository.directory(configuration)
#expect(result == expected) #expect(result == expected)
} }
@Test(.tags(.run))
func runBuildProject() async throws {
let captured = CommandClient.CapturingClient()
try await withMockConfiguration(captured, key: "runBuildProject") {
@Dependency(\.playbookClient) var playbookClient
let configuration = Configuration.mock
try await playbookClient.run.buildProject(.init(projectDirectory: "/foo", shared: Self.sharedRunOptions))
let arguments = await captured.options!.arguments
print(arguments)
#expect(arguments == [
"ansible-playbook", "~/.local/share/hpa/playbook/main.yml",
"--inventory", "~/.local/share/hpa/playbook/inventory.ini",
configuration.vault.args!.first!,
"--tags", "build-project",
"--extra-vars", "project_dir=/foo"
])
}
}
@Test(
.tags(.run),
arguments: [
(true, "\'{\"template\":{\"path\":\"\(Configuration.mock.template.directory!)\"}}\'"),
(false, "\'{\"template\":{\"repo\":{\"url\":\"\(Configuration.mock.template.url!)\",\"version\":\"\(Configuration.mock.template.version!)\"}}}\'")
]
)
func runCreateProject(useLocalTemplateDirectory: Bool, json: String) async throws {
let captured = CommandClient.CapturingClient()
try await withMockConfiguration(captured, key: "runBuildProject") {
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
let configuration = Configuration.mock
try await playbookClient.run.createProject(
.init(
projectDirectory: "/project",
shared: Self.sharedRunOptions,
useLocalTemplateDirectory: useLocalTemplateDirectory
)
)
let arguments = await captured.options!.arguments
logger.debug("\(arguments)")
#expect(arguments == [
"ansible-playbook", "~/.local/share/hpa/playbook/main.yml",
"--inventory", "~/.local/share/hpa/playbook/inventory.ini",
configuration.vault.args!.first!,
"--tags", "setup-project",
"--extra-vars", "project_dir=/project",
"--extra-vars", json
])
}
}
@Test(arguments: CreateJsonTestOption.testCases)
func createJson(input: CreateJsonTestOption) {
withTestLogger(key: "generateJson") {
$0.coders.jsonEncoder = { jsonEncoder }
$0.configurationClient = .mock(input.configuration)
} operation: {
@Dependency(\.coders) var coders
let jsonData = try? input.options.createJSONData(
configuration: input.configuration,
encoder: coders.jsonEncoder()
)
switch input.expectation {
case let .success(expected):
let json = String(data: jsonData!, encoding: .utf8)!
if json != expected {
print("json:", json)
print("expected:", expected)
}
#expect(json == expected)
case .failure:
#expect(jsonData == nil)
}
}
}
func withMockConfiguration(
_ capturing: CommandClient.CapturingClient,
configuration: Configuration = .mock,
key: String,
logLevel: Logger.Level = .trace,
depednencies setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
operation: @Sendable @escaping () async throws -> Void
) async rethrows {
try await withDependencies {
$0.configurationClient = .mock(configuration)
$0.commandClient = .capturing(capturing)
$0.playbookClient = .liveValue
setupDependencies(&$0)
} operation: {
try await operation()
}
}
} }
struct CreateJsonTestOption: Sendable {
let options: PlaybookClient.RunPlaybook.CreateOptions
let configuration: Configuration
let expectation: Result<String, TestError>
static let testCases: [Self] = [
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: nil, version: nil, directory: nil),
useLocalTemplateDirectory: true
),
configuration: .init(),
expectation: .failing
),
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: nil, version: nil, directory: nil),
useLocalTemplateDirectory: false
),
configuration: .init(),
expectation: .failing
),
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: nil, version: nil, directory: "/template"),
useLocalTemplateDirectory: true
),
configuration: .init(template: .init(directory: "/template")),
expectation: .success("""
{
"template" : {
"path" : "/template"
}
}
""")
),
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: nil, version: nil, directory: "/template"),
useLocalTemplateDirectory: true
),
configuration: .init(template: .init(directory: "/template")),
expectation: .success("""
{
"template" : {
"path" : "/template"
}
}
""")
),
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: "https://git.example.com/template.git", version: "main", directory: nil),
useLocalTemplateDirectory: false
),
configuration: .init(),
expectation: .success("""
{
"template" : {
"repo" : {
"url" : "https://git.example.com/template.git",
"version" : "main"
}
}
}
""")
),
CreateJsonTestOption(
options: .init(
projectDirectory: "/project",
shared: PlaybookClientTests.sharedRunOptions,
template: .init(url: "https://git.example.com/template.git", version: "v0.1.0", directory: nil),
useLocalTemplateDirectory: false
),
configuration: .init(template: .init(url: "https://git.example.com/template.git", version: "v0.1.0")),
expectation: .success("""
{
"template" : {
"repo" : {
"url" : "https://git.example.com/template.git",
"version" : "v0.1.0"
}
}
}
""")
)
]
}
extension Result where Failure == TestError {
static var failing: Self { .failure(TestError()) }
}
extension ConfigurationClient {
static func mock(_ configuration: Configuration) -> Self {
var mock = Self.testValue
mock.find = { throw TestError() }
mock.load = { _ in configuration }
return mock
}
}
struct TestError: Error {}
extension Tag {
@Tag static var repository: Self
@Tag static var run: Self
}
let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
return encoder
}()