feat: Moves logging setup and generate-json for the create command to cli-client module.

This commit is contained in:
2024-12-12 11:16:22 -05:00
parent ce6eb3ec2f
commit 7b30b78b67
14 changed files with 449 additions and 230 deletions

View File

@@ -23,81 +23,91 @@ public extension CliClient {
try await runCommand(quiet: quiet, shell: shell, args) try await runCommand(quiet: quiet, shell: shell, args)
} }
func runPlaybookCommand(_ options: PlaybookOptions) async throws { func runPlaybookCommand(
@Dependency(\.configurationClient) var configurationClient _ options: PlaybookOptions,
@Dependency(\.logger) var logger logging loggingOptions: LoggingOptions
) async throws {
try await withLogger(loggingOptions) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
let configuration = try await configurationClient.ensuredConfiguration(options.configuration) let configuration = try await configurationClient.ensuredConfiguration(options.configuration)
logger.trace("Configuration: \(configuration)") logger.trace("Configuration: \(configuration)")
let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory) let playbookDirectory = try configuration.ensuredPlaybookDirectory(options.playbookDirectory)
let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)" let playbookPath = "\(playbookDirectory)/\(Constants.playbookFileName)"
logger.trace("Playbook path: \(playbookPath)") logger.trace("Playbook path: \(playbookPath)")
let inventoryPath = ensuredInventoryPath( let inventoryPath = ensuredInventoryPath(
options.inventoryFilePath, options.inventoryFilePath,
configuration: configuration, configuration: configuration,
playbookDirectory: playbookDirectory playbookDirectory: playbookDirectory
) )
logger.trace("Inventory path: \(inventoryPath)") logger.trace("Inventory path: \(inventoryPath)")
var arguments = [ var arguments = [
Constants.playbookCommand, playbookPath, Constants.playbookCommand, playbookPath,
"--inventory", inventoryPath "--inventory", inventoryPath
] + options.arguments ] + options.arguments
if let defaultArgs = configuration.args { if let defaultArgs = configuration.args {
arguments.append(contentsOf: defaultArgs) arguments.append(contentsOf: defaultArgs)
}
if configuration.useVaultArgs, let vaultArgs = configuration.vault.args {
arguments.append(contentsOf: vaultArgs)
}
logger.trace("Running playbook command with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.orDefault,
arguments
)
} }
if configuration.useVaultArgs, let vaultArgs = configuration.vault.args {
arguments.append(contentsOf: vaultArgs)
}
logger.trace("Running playbook command with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.orDefault,
arguments
)
} }
func runVaultCommand(_ options: VaultOptions) async throws { func runVaultCommand(
@Dependency(\.configurationClient) var configurationClient _ options: VaultOptions,
@Dependency(\.fileClient) var fileClient logging loggingOptions: LoggingOptions
@Dependency(\.logger) var logger ) async throws {
try await withLogger(loggingOptions) {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
let configuration = try await configurationClient.ensuredConfiguration(options.configuration) let configuration = try await configurationClient.ensuredConfiguration(options.configuration)
logger.trace("Configuration: \(configuration)") logger.trace("Configuration: \(configuration)")
let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath) let vaultFilePath = try await fileClient.ensuredVaultFilePath(options.vaultFilePath)
logger.trace("Vault file: \(vaultFilePath)") logger.trace("Vault file: \(vaultFilePath)")
var arguments = [ var arguments = [
Constants.vaultCommand Constants.vaultCommand
] + options.arguments ] + options.arguments
if let defaultArgs = configuration.vault.args { if let defaultArgs = configuration.vault.args {
arguments.append(contentsOf: defaultArgs) arguments.append(contentsOf: defaultArgs)
}
if arguments.contains("encrypt"),
!arguments.contains("--encrypt-vault-id"),
let id = configuration.vault.encryptId
{
arguments.append(contentsOf: ["--encrypt-vault-id", id])
}
arguments.append(vaultFilePath)
logger.trace("Running vault command with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.orDefault,
arguments
)
} }
if arguments.contains("encrypt"),
!arguments.contains("--encrypt-vault-id"),
let id = configuration.vault.encryptId
{
arguments.append(contentsOf: ["--encrypt-vault-id", id])
}
arguments.append(vaultFilePath)
logger.trace("Running vault command with arguments: \(arguments)")
try await runCommand(
quiet: options.quiet,
shell: options.shell.orDefault,
arguments
)
} }
} }
@@ -162,9 +172,4 @@ public extension FileClient {
} }
} }
enum CliClientError: Error {
case playbookDirectoryNotFound
case vaultFileNotFound
}
extension ShellCommand.Shell: @retroactive @unchecked Sendable {} extension ShellCommand.Shell: @retroactive @unchecked Sendable {}

View File

@@ -0,0 +1,9 @@
import Foundation
public enum CliClientError: Error {
case encodingError
case playbookDirectoryNotFound
case templateDirectoryNotFound
case templateDirectoryOrRepoNotSpecified
case vaultFileNotFound
}

View File

@@ -0,0 +1,80 @@
import ConfigurationClient
import Dependencies
import Foundation
// NOTE: We're not using the `Coders` client because we generally do not
// want the output to be `prettyPrinted` or anything, unless we're running
// tests, so we use a supplied json encoder.
func createJSONData(
_ options: CliClient.GenerateJsonOptions,
logging loggingOptions: CliClient.LoggingOptions,
encoder: JSONEncoder = .init()
) async throws -> Data {
try await CliClient.withLogger(loggingOptions) {
@Dependency(\.logger) var logger
@Dependency(\.configurationClient) var configurationClient
let configuration = try await configurationClient.findAndLoad()
let templateDir = options.templateDirectory ?? configuration.template.directory
let templateRepo = options.templateRepo ?? configuration.template.url
let version = options.version ?? configuration.template.version
logger.debug("""
(\(options.useLocalTemplateDirectory), \(String(describing: templateDir)), \(String(describing: templateRepo)))
""")
switch (options.useLocalTemplateDirectory, templateDir, templateRepo) {
case (true, .none, _):
// User supplied they wanted to use a local template directory, but we could not find
// the path set from command line or in configuration.
throw CliClientError.templateDirectoryNotFound
case let (false, _, .some(repo)):
// User did not supply they wanted to use a local template directory, and we found a repo url that was
// either set by the command line or found in the configuration.
logger.debug("Using repo.")
return try encoder.encode(TemplateRepo(repo: repo, version: version))
case let (true, .some(templateDir), _):
// User supplied they wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case let (false, .some(templateDir), _):
// User supplied they did not wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration, and no repo was found / handled previously.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case (_, .none, .none):
// We could not find a repo or template directory.
throw CliClientError.templateDirectoryOrRepoNotSpecified
}
}
}
private struct TemplateDirJson: Encodable {
let template: Template
init(path: String) {
self.template = .init(path: path)
}
struct Template: Encodable {
let path: String
}
}
private struct TemplateRepo: Encodable {
let template: Template
init(repo: String, version: String?) {
self.template = .init(repo: repo, version: version ?? "main")
}
struct Template: Encodable {
let repo: String
let version: String
}
}

View File

@@ -4,9 +4,6 @@ import DependenciesMacros
import Foundation import Foundation
import ShellClient import ShellClient
// TODO: Add logging options and setup in this module and remove from the
// executable `hpa` module.
public extension DependencyValues { public extension DependencyValues {
var cliClient: CliClient { var cliClient: CliClient {
get { self[CliClient.self] } get { self[CliClient.self] }
@@ -16,11 +13,49 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct CliClient: Sendable { public struct CliClient: Sendable {
public var runCommand: @Sendable (RunCommandOptions) async throws -> Void public var runCommand: @Sendable (RunCommandOptions) async throws -> Void
public var generateJSON: @Sendable (GenerateJsonOptions, LoggingOptions, JSONEncoder) async throws -> String
public func generateJSON(
_ options: GenerateJsonOptions,
logging loggingOptions: LoggingOptions,
encoder jsonEncoder: JSONEncoder = .init()
) async throws -> String {
try await generateJSON(options, loggingOptions, jsonEncoder)
}
} }
public extension CliClient { public extension CliClient {
struct GenerateJsonOptions: Equatable, Sendable {
let templateDirectory: String?
let templateRepo: String?
let version: String?
let useLocalTemplateDirectory: Bool
public init(
templateDirectory: String?,
templateRepo: String?,
version: String?,
useLocalTemplateDirectory: Bool
) {
self.templateDirectory = templateDirectory
self.templateRepo = templateRepo
self.version = version
self.useLocalTemplateDirectory = useLocalTemplateDirectory
}
}
struct LoggingOptions: Equatable, Sendable {
let commandName: String
let logLevel: Logger.Level
public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName
self.logLevel = logLevel
}
}
struct PlaybookOptions: Sendable, Equatable { struct PlaybookOptions: Sendable, Equatable {
let arguments: [String] let arguments: [String]
let configuration: Configuration? let configuration: Configuration?
@@ -109,6 +144,12 @@ extension CliClient: DependencyKey {
options.arguments options.arguments
)) ))
} }
} generateJSON: { options, loggingOptions, encoder in
let data = try await createJSONData(options, logging: loggingOptions, encoder: encoder)
guard let string = String(data: data, encoding: .utf8) else {
throw CliClientError.encodingError
}
return string
} }
} }
@@ -121,6 +162,8 @@ extension CliClient: DependencyKey {
public static func capturing(_ client: CapturingClient) -> Self { public static func capturing(_ client: CapturingClient) -> Self {
.init { options in .init { options in
await client.set(options) await client.set(options)
} generateJSON: {
try await Self().generateJSON($0, $1, $2)
} }
} }

View File

@@ -0,0 +1,28 @@
import Dependencies
import Logging
import ShellClient
public extension CliClient {
@discardableResult
func withLogger<T>(
_ options: LoggingOptions,
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await Self.withLogger(options, operation: operation)
}
@discardableResult
static func withLogger<T>(
_ options: LoggingOptions,
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger = .init(label: "\(Constants.executableName)")
$0.logger.logLevel = options.logLevel
$0.logger[metadataKey: "command"] = "\(options.commandName.blue)"
} operation: {
try await operation()
}
}
}

View File

@@ -38,18 +38,17 @@ struct BuildCommand: AsyncParsableCommand {
} }
private func _run() async throws { private func _run() async throws {
try await withSetupLogger(commandName: Self.commandName, globals: globals) { @Dependency(\.cliClient) var cliClient
@Dependency(\.cliClient) var cliClient
try await cliClient.runPlaybookCommand( try await cliClient.runPlaybookCommand(
globals.playbookOptions( globals.playbookOptions(
arguments: [ arguments: [
"--tags", "build-project", "--tags", "build-project",
"--extra-vars", "project_dir=\(self.projectDir)" "--extra-vars", "project_dir=\(projectDir)"
], ],
configuration: nil configuration: nil
) ),
) logging: globals.loggingOptions(commandName: Self.commandName)
} )
} }
} }

View File

@@ -61,126 +61,46 @@ struct CreateCommand: AsyncParsableCommand {
} }
private func _run() async throws { private func _run() async throws {
try await withSetupLogger(commandName: Self.commandName, globals: globals) { @Dependency(\.coders) var coders
@Dependency(\.coders) var coders @Dependency(\.cliClient) var cliClient
@Dependency(\.cliClient) var cliClient @Dependency(\.configurationClient) var configurationClient
@Dependency(\.configurationClient) var configurationClient
let loggingOptions = globals.loggingOptions(commandName: Self.commandName)
try await cliClient.withLogger(loggingOptions) {
@Dependency(\.logger) var logger @Dependency(\.logger) var logger
let encoder = coders.jsonEncoder() let json = try await cliClient.generateJSON(
generateJsonOptions,
let configuration = try await configurationClient.findAndLoad() logging: loggingOptions
logger.debug("Configuration: \(configuration)")
let jsonData = try parseOptions(
command: self,
configuration: configuration,
logger: logger,
encoder: encoder
) )
guard let jsonString = String(data: jsonData, encoding: .utf8) else { logger.debug("JSON string: \(json)")
throw CreateError.encodingError
}
logger.debug("JSON string: \(jsonString)")
let arguments = [ let arguments = [
"--tags", "setup-project", "--tags", "setup-project",
"--extra-vars", "project_dir=\(self.projectDir)", "--extra-vars", "project_dir=\(self.projectDir)",
"--extra-vars", "'\(jsonString)'" "--extra-vars", "'\(json)'"
] + extraArgs ] + extraArgs
try await cliClient.runPlaybookCommand( try await cliClient.runPlaybookCommand(
globals.playbookOptions( globals.playbookOptions(
arguments: arguments, arguments: arguments,
configuration: configuration configuration: nil
) ),
logging: loggingOptions
) )
// try await runPlaybook(
// commandName: Self.commandName,
// globals: self.globals,
// configuration: configuration,
// extraArgs: extraArgs,
// "--tags", "setup-project",
// "--extra-vars", "project_dir=\(self.projectDir)",
// "--extra-vars", "'\(jsonString)'"
// )
} }
} }
} }
private func parseOptions( private extension CreateCommand {
command: CreateCommand, var generateJsonOptions: CliClient.GenerateJsonOptions {
configuration: Configuration, .init(
logger: Logger, templateDirectory: templateDir,
encoder: JSONEncoder templateRepo: repo,
) throws -> Data { version: branch,
let templateDir = command.templateDir ?? configuration.template.directory useLocalTemplateDirectory: localTemplateDir
let templateRepo = command.repo ?? configuration.template.url )
let version = (command.branch ?? configuration.template.version) ?? "main"
logger.debug("""
(\(command.localTemplateDir), \(String(describing: templateDir)), \(String(describing: templateRepo)))
""")
switch (command.localTemplateDir, templateDir, templateRepo) {
case (true, .none, _):
// User supplied they wanted to use a local template directory, but we could not find
// the path set from command line or in configuration.
throw CreateError.templateDirNotFound
case let (false, _, .some(repo)):
// User did not supply they wanted to use a local template directory, and we found a repo url that was
// either set by the command line or found in the configuration.
logger.debug("Using repo.")
return try encoder.encode(TemplateRepo(repo: repo, version: version))
case let (true, .some(templateDir), _):
// User supplied they wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case let (false, .some(templateDir), _):
// User supplied they did not wanted to use a local template directory, and we found the template directory
// either set by the command line or in the configuration, and no repo was found / handled previously.
logger.debug("Using template directory.")
return try encoder.encode(TemplateDirJson(path: templateDir))
case (_, .none, .none):
// We could not find a repo or template directory.
throw CreateError.templateDirOrRepoNotSpecified
} }
} }
private struct TemplateDirJson: Encodable {
let template: Template
init(path: String) {
self.template = .init(path: path)
}
struct Template: Encodable {
let path: String
}
}
private struct TemplateRepo: Encodable {
let template: Template
init(repo: String, version: String?) {
self.template = .init(repo: repo, version: version ?? "main")
}
struct Template: Encodable {
let repo: String
let version: String
}
}
enum CreateError: Error {
case encodingError
case templateDirNotFound
case templateDirOrRepoNotSpecified
}

View File

@@ -1,4 +1,5 @@
import ArgumentParser import ArgumentParser
import CliClient
struct BasicGlobalOptions: ParsableArguments { struct BasicGlobalOptions: ParsableArguments {
@Flag( @Flag(
@@ -55,3 +56,21 @@ struct GlobalOptions: ParsableArguments {
} }
} }
extension GlobalOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
.init(
commandName: commandName,
logLevel: .init(globals: basic, quietOnlyPlaybook: quietOnlyPlaybook)
)
}
}
extension BasicGlobalOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
.init(
commandName: commandName,
logLevel: .init(globals: self, quietOnlyPlaybook: false)
)
}
}

View File

@@ -1,7 +1,5 @@
import Dependencies
import Logging import Logging
// TODO: Move some of this to the cli-client module.
extension Logger.Level { extension Logger.Level {
/// Set the log level based on the user's options supplied. /// Set the log level based on the user's options supplied.
@@ -22,34 +20,3 @@ extension Logger.Level {
self = .info self = .info
} }
} }
func withSetupLogger(
commandName: String,
globals: BasicGlobalOptions,
quietOnlyPlaybook: Bool = false,
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
operation: @escaping () async throws -> Void
) async rethrows {
try await withDependencies {
$0.logger = .init(label: "\("hpa".yellow)")
$0.logger.logLevel = .init(globals: globals, quietOnlyPlaybook: quietOnlyPlaybook)
$0.logger[metadataKey: "command"] = "\(commandName.blue)"
} operation: {
try await operation()
}
}
func withSetupLogger(
commandName: String,
globals: GlobalOptions,
dependencies setupDependencies: (inout DependencyValues) -> Void = { _ in },
operation: @escaping () async throws -> Void
) async rethrows {
try await withSetupLogger(
commandName: commandName,
globals: globals.basic,
quietOnlyPlaybook: globals.quietOnlyPlaybook,
dependencies: setupDependencies,
operation: operation
)
}

View File

@@ -50,9 +50,8 @@ struct GenerateConfigurationCommand: AsyncParsableCommand {
// FIX: // FIX:
private func _run() async throws { private func _run() async throws {
try await withSetupLogger(commandName: Self.commandName, globals: globals) { @Dependency(\.cliClient) var cliClient
@Dependency(\.cliClient) var cliClient try await cliClient.withLogger(globals.loggingOptions(commandName: Self.commandName)) {
let actualPath: String let actualPath: String
// if let path { // if let path {

View File

@@ -29,7 +29,8 @@ struct DecryptCommand: AsyncParsableCommand {
} }
try await cliClient.runVaultCommand( try await cliClient.runVaultCommand(
options.vaultOptions(arguments: args, configuration: nil) options.vaultOptions(arguments: args, configuration: nil),
logging: options.loggingOptions(commandName: Self.commandName)
) )
// try await runVault( // try await runVault(

View File

@@ -28,7 +28,8 @@ struct EncryptCommand: AsyncParsableCommand {
args.append(contentsOf: ["--output", output]) args.append(contentsOf: ["--output", output])
} }
try await cliClient.runVaultCommand( try await cliClient.runVaultCommand(
options.vaultOptions(arguments: args, configuration: nil) options.vaultOptions(arguments: args, configuration: nil),
logging: options.loggingOptions(commandName: Self.commandName)
) )
// try await runVault( // try await runVault(

View File

@@ -1,4 +1,5 @@
import ArgumentParser import ArgumentParser
import CliClient
// Holds the common options for vault commands, as they all share the // Holds the common options for vault commands, as they all share the
// same structure. // same structure.
@@ -29,3 +30,9 @@ struct VaultOptions: ParsableArguments {
} }
} }
extension VaultOptions {
func loggingOptions(commandName: String) -> CliClient.LoggingOptions {
globals.loggingOptions(commandName: commandName)
}
}

View File

@@ -10,6 +10,12 @@ import TestSupport
@Suite("CliClientTests") @Suite("CliClientTests")
struct CliClientTests: TestCase { struct CliClientTests: TestCase {
static let loggingOptions: CliClient.LoggingOptions = {
let levelString = ProcessInfo.processInfo.environment["LOGGING_LEVEL"] ?? "debug"
let logLevel = Logger.Level(rawValue: levelString) ?? .debug
return .init(commandName: "CliClientTests", logLevel: logLevel)
}()
@Test @Test
func capturingClient() async throws { func capturingClient() async throws {
let captured = CliClient.CapturingClient() let captured = CliClient.CapturingClient()
@@ -35,7 +41,10 @@ struct CliClientTests: TestCase {
@Dependency(\.cliClient) var cliClient @Dependency(\.cliClient) var cliClient
let configuration = Configuration.mock let configuration = Configuration.mock
try await cliClient.runVaultCommand(.init(arguments: [argument], quiet: false, shell: nil)) try await cliClient.runVaultCommand(
.init(arguments: [argument], quiet: false, shell: nil),
logging: Self.loggingOptions
)
let shell = await captured.shell let shell = await captured.shell
#expect(shell == .zsh(useDashC: true)) #expect(shell == .zsh(useDashC: true))
@@ -74,11 +83,14 @@ struct CliClientTests: TestCase {
try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") { try await withMockConfiguration(captured, configuration: configuration, key: "runPlaybook") {
@Dependency(\.cliClient) var cliClient @Dependency(\.cliClient) var cliClient
try await cliClient.runPlaybookCommand(.init( try await cliClient.runPlaybookCommand(
arguments: [], .init(
quiet: false, arguments: [],
shell: nil quiet: false,
)) shell: nil
),
logging: Self.loggingOptions
)
let expectedArguments = [ let expectedArguments = [
"ansible-playbook", "playbook/main.yml", "ansible-playbook", "playbook/main.yml",
@@ -156,6 +168,118 @@ struct CliClientTests: TestCase {
} }
} }
@Test(
arguments: [
GenerateJsonTestOption(
options: .init(
templateDirectory: nil,
templateRepo: nil,
version: nil,
useLocalTemplateDirectory: true
),
configuration: nil,
expectation: .failing
),
GenerateJsonTestOption(
options: .init(
templateDirectory: nil,
templateRepo: nil,
version: nil,
useLocalTemplateDirectory: false
),
configuration: nil,
expectation: .failing
),
GenerateJsonTestOption(
options: .init(
templateDirectory: "template",
templateRepo: nil,
version: nil,
useLocalTemplateDirectory: true
),
configuration: nil,
expectation: .success("""
{
"template" : {
"path" : "template"
}
}
""")
),
GenerateJsonTestOption(
options: .init(
templateDirectory: nil,
templateRepo: nil,
version: nil,
useLocalTemplateDirectory: false
),
configuration: .init(template: .init(directory: "template")),
expectation: .success("""
{
"template" : {
"path" : "template"
}
}
""")
),
GenerateJsonTestOption(
options: .init(
templateDirectory: nil,
templateRepo: "https://git.example.com/template.git",
version: nil,
useLocalTemplateDirectory: false
),
configuration: nil,
expectation: .success("""
{
"template" : {
"repo" : "https://git.example.com/template.git",
"version" : "main"
}
}
""")
),
GenerateJsonTestOption(
options: .init(
templateDirectory: nil,
templateRepo: nil,
version: nil,
useLocalTemplateDirectory: false
),
configuration: .init(template: .init(url: "https://git.example.com/template.git", version: "v0.1.0")),
expectation: .success("""
{
"template" : {
"repo" : "https://git.example.com/template.git",
"version" : "v0.1.0"
}
}
""")
)
]
)
func generateJson(input: GenerateJsonTestOption) async {
await withTestLogger(key: "generateJson") {
$0.configurationClient = .mock(input.configuration ?? .init())
$0.cliClient = .liveValue
} operation: {
@Dependency(\.cliClient) var cliClient
let json = try? await cliClient.generateJSON(
input.options,
logging: Self.loggingOptions,
encoder: jsonEncoder
)
switch input.expectation {
case let .success(expected):
#expect(json == expected)
case .failing:
#expect(json == nil)
}
}
}
func withMockConfiguration( func withMockConfiguration(
_ capturing: CliClient.CapturingClient, _ capturing: CliClient.CapturingClient,
configuration: Configuration = .mock, configuration: Configuration = .mock,
@@ -174,6 +298,17 @@ struct CliClientTests: TestCase {
} }
} }
struct GenerateJsonTestOption: Sendable {
let options: CliClient.GenerateJsonOptions
let configuration: Configuration?
let expectation: GenerateJsonExpectation
}
enum GenerateJsonExpectation: Sendable {
case failing
case success(String)
}
extension ConfigurationClient { extension ConfigurationClient {
static func mock(_ configuration: Configuration) -> Self { static func mock(_ configuration: Configuration) -> Self {
var mock = Self.testValue var mock = Self.testValue
@@ -184,3 +319,9 @@ extension ConfigurationClient {
} }
struct TestError: Error {} struct TestError: Error {}
let jsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes, .sortedKeys]
return encoder
}()