From 717f2d36943eaacbb2ea0460275296eed2d12149 Mon Sep 17 00:00:00 2001 From: Pedro Date: Fri, 7 Feb 2025 16:11:18 +0100 Subject: [PATCH 1/4] Add --json suppor to get and list --- src/Commands/Get.swift | 7 ++-- src/Commands/List.swift | 9 ++++-- src/VM/VMDetailsPrinter.swift | 20 ++++++++---- tests/VM/VMDetailsPrinterTests.swift | 48 ++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 tests/VM/VMDetailsPrinterTests.swift diff --git a/src/Commands/Get.swift b/src/Commands/Get.swift index 32b2b423..43137024 100644 --- a/src/Commands/Get.swift +++ b/src/Commands/Get.swift @@ -9,6 +9,9 @@ struct Get: AsyncParsableCommand { @Argument(help: "Name of the virtual machine", completion: .custom(completeVMName)) var name: String + @Flag(name: .long, help: "Outputs the images as a machine-readable JSON.") + var json = false + init() { } @@ -16,6 +19,6 @@ struct Get: AsyncParsableCommand { func run() async throws { let vmController = LumeController() let vm = try vmController.get(name: name) - VMDetailsPrinter.printStatus([vm.details]) + try VMDetailsPrinter.printStatus([vm.details], json: self.json) } -} \ No newline at end of file +} diff --git a/src/Commands/List.swift b/src/Commands/List.swift index 6b1fd850..56b8e72b 100644 --- a/src/Commands/List.swift +++ b/src/Commands/List.swift @@ -7,6 +7,9 @@ struct List: AsyncParsableCommand { abstract: "List virtual machines" ) + @Flag(name: .long, help: "Outputs the images as a machine-readable JSON.") + var json = false + init() { } @@ -14,10 +17,10 @@ struct List: AsyncParsableCommand { func run() async throws { let manager = LumeController() let vms = try manager.list() - if vms.isEmpty { + if vms.isEmpty && !json { print("No virtual machines found") } else { - VMDetailsPrinter.printStatus(vms) + try VMDetailsPrinter.printStatus(vms, json: self.json) } } -} \ No newline at end of file +} diff --git a/src/VM/VMDetailsPrinter.swift b/src/VM/VMDetailsPrinter.swift index 7ec711ac..2c976a90 100644 --- a/src/VM/VMDetailsPrinter.swift +++ b/src/VM/VMDetailsPrinter.swift @@ -33,17 +33,25 @@ enum VMDetailsPrinter { /// Prints the status of all VMs in a formatted table /// - Parameter vms: Array of VM status objects to display - static func printStatus(_ vms: [VMDetails]) { - printHeader() - vms.forEach(printVM) + static func printStatus(_ vms: [VMDetails], json: Bool, print: (String) -> () = { print($0) }) throws { + if json { + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let jsonData = try jsonEncoder.encode(vms) + let jsonString = String(data: jsonData, encoding: .utf8)! + print(jsonString) + } else { + printHeader(print: print) + vms.forEach({ printVM($0, print: print)}) + } } - private static func printHeader() { + private static func printHeader(print: (String) -> () = { print($0) }) { let paddedHeaders = columns.map { $0.header.paddedToWidth($0.width) } print(paddedHeaders.joined()) } - private static func printVM(_ vm: VMDetails) { + private static func printVM(_ vm: VMDetails, print: (String) -> Void = { print($0) }) { let paddedColumns = columns.map { column in column.getValue(vm).paddedToWidth(column.width) } @@ -58,4 +66,4 @@ private extension String { func paddedToWidth(_ width: Int) -> String { padding(toLength: width, withPad: " ", startingAt: 0) } -} \ No newline at end of file +} diff --git a/tests/VM/VMDetailsPrinterTests.swift b/tests/VM/VMDetailsPrinterTests.swift new file mode 100644 index 00000000..22669173 --- /dev/null +++ b/tests/VM/VMDetailsPrinterTests.swift @@ -0,0 +1,48 @@ +import Testing +import Foundation +@testable import lume + +struct VMDetailsPrinterTests { + + @Test func printStatus_whenJSON() throws { + // Given + let vms: [VMDetails] = [VMDetails(name: "name", + os: "os", + cpuCount: 2, + memorySize: 1024, + diskSize: .init(allocated: 24, total: 30), + status: "status", + vncUrl: "vncUrl", + ipAddress: "0.0.0.0")] + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let expectedOutput = try String(data: jsonEncoder.encode(vms), encoding: .utf8)! + + // When + var printedStatus: String? + try VMDetailsPrinter.printStatus(vms, json: true, print: { printedStatus = $0 }) + + // Then + #expect(printedStatus == expectedOutput) + } + + @Test func printStatus_whenNotJSON() throws { + // Given + let vms: [VMDetails] = [VMDetails(name: "name", + os: "os", + cpuCount: 2, + memorySize: 1024, + diskSize: .init(allocated: 24, total: 30), + status: "status", + vncUrl: "vncUrl", + ipAddress: "0.0.0.0")] + + // When + var printedLines: [String] = [] + try VMDetailsPrinter.printStatus(vms, json: false, print: { printedLines.append($0) }) + + // Then + #expect(printedLines.popLast() == "name os 2 0.00G 24.0B/30.0B status 0.0.0.0 vncUrl ") + #expect(printedLines.popLast() == "name os cpu memory disk status ip vnc ") + } +} From 82456cafc555212b7cc446bcd916205fb4b1d588 Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 9 Feb 2025 10:37:34 +0100 Subject: [PATCH 2/4] Make the test expectation more resilient against tabs --- tests/VM/VMDetailsPrinterTests.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/VM/VMDetailsPrinterTests.swift b/tests/VM/VMDetailsPrinterTests.swift index 22669173..3f73e65a 100644 --- a/tests/VM/VMDetailsPrinterTests.swift +++ b/tests/VM/VMDetailsPrinterTests.swift @@ -42,7 +42,13 @@ struct VMDetailsPrinterTests { try VMDetailsPrinter.printStatus(vms, json: false, print: { printedLines.append($0) }) // Then - #expect(printedLines.popLast() == "name os 2 0.00G 24.0B/30.0B status 0.0.0.0 vncUrl ") - #expect(printedLines.popLast() == "name os cpu memory disk status ip vnc ") + #expect(printedLines.count == 2) + + + let headerParts = printedLines[0].split(whereSeparator: \.isWhitespace) + #expect(headerParts == ["name", "os", "cpu", "memory", "disk", "status", "ip", "vnc"]) + + let vmParts = printedLines[1].split(whereSeparator: \.isWhitespace) + #expect(vmParts == ["name", "os", "2", "0.00G", "24.0B/30.0B", "status", "0.0.0.0", "vncUrl"]) } } From eef1853f502c6eb8f3f26ab96d8f34f1c4a43511 Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 9 Feb 2025 10:41:50 +0100 Subject: [PATCH 3/4] Replace the flag --json with the option --format json|text --- src/Commands/Get.swift | 6 +++--- src/Commands/List.swift | 8 ++++---- src/Commands/Options/FormatOption.swift | 6 ++++++ src/VM/VMDetailsPrinter.swift | 4 ++-- tests/VM/VMDetailsPrinterTests.swift | 6 +++--- 5 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 src/Commands/Options/FormatOption.swift diff --git a/src/Commands/Get.swift b/src/Commands/Get.swift index 43137024..e3bcbbef 100644 --- a/src/Commands/Get.swift +++ b/src/Commands/Get.swift @@ -9,8 +9,8 @@ struct Get: AsyncParsableCommand { @Argument(help: "Name of the virtual machine", completion: .custom(completeVMName)) var name: String - @Flag(name: .long, help: "Outputs the images as a machine-readable JSON.") - var json = false + @Option(name: [.long, .customShort("f")], help: "Output format (json|text)") + var format: FormatOption = .text init() { } @@ -19,6 +19,6 @@ struct Get: AsyncParsableCommand { func run() async throws { let vmController = LumeController() let vm = try vmController.get(name: name) - try VMDetailsPrinter.printStatus([vm.details], json: self.json) + try VMDetailsPrinter.printStatus([vm.details], format: self.format) } } diff --git a/src/Commands/List.swift b/src/Commands/List.swift index 56b8e72b..6361f899 100644 --- a/src/Commands/List.swift +++ b/src/Commands/List.swift @@ -7,8 +7,8 @@ struct List: AsyncParsableCommand { abstract: "List virtual machines" ) - @Flag(name: .long, help: "Outputs the images as a machine-readable JSON.") - var json = false + @Option(name: [.long, .customShort("f")], help: "Output format (json|text)") + var format: FormatOption = .text init() { } @@ -17,10 +17,10 @@ struct List: AsyncParsableCommand { func run() async throws { let manager = LumeController() let vms = try manager.list() - if vms.isEmpty && !json { + if vms.isEmpty && self.format == .text { print("No virtual machines found") } else { - try VMDetailsPrinter.printStatus(vms, json: self.json) + try VMDetailsPrinter.printStatus(vms, format: self.format) } } } diff --git a/src/Commands/Options/FormatOption.swift b/src/Commands/Options/FormatOption.swift new file mode 100644 index 00000000..0d362a2e --- /dev/null +++ b/src/Commands/Options/FormatOption.swift @@ -0,0 +1,6 @@ +import ArgumentParser + +enum FormatOption: String, ExpressibleByArgument { + case json + case text +} diff --git a/src/VM/VMDetailsPrinter.swift b/src/VM/VMDetailsPrinter.swift index 2c976a90..4d62438f 100644 --- a/src/VM/VMDetailsPrinter.swift +++ b/src/VM/VMDetailsPrinter.swift @@ -33,8 +33,8 @@ enum VMDetailsPrinter { /// Prints the status of all VMs in a formatted table /// - Parameter vms: Array of VM status objects to display - static func printStatus(_ vms: [VMDetails], json: Bool, print: (String) -> () = { print($0) }) throws { - if json { + static func printStatus(_ vms: [VMDetails], format: FormatOption, print: (String) -> () = { print($0) }) throws { + if format == .json { let jsonEncoder = JSONEncoder() jsonEncoder.outputFormatting = .prettyPrinted let jsonData = try jsonEncoder.encode(vms) diff --git a/tests/VM/VMDetailsPrinterTests.swift b/tests/VM/VMDetailsPrinterTests.swift index 3f73e65a..fad026c2 100644 --- a/tests/VM/VMDetailsPrinterTests.swift +++ b/tests/VM/VMDetailsPrinterTests.swift @@ -20,7 +20,7 @@ struct VMDetailsPrinterTests { // When var printedStatus: String? - try VMDetailsPrinter.printStatus(vms, json: true, print: { printedStatus = $0 }) + try VMDetailsPrinter.printStatus(vms, format: .json, print: { printedStatus = $0 }) // Then #expect(printedStatus == expectedOutput) @@ -39,7 +39,7 @@ struct VMDetailsPrinterTests { // When var printedLines: [String] = [] - try VMDetailsPrinter.printStatus(vms, json: false, print: { printedLines.append($0) }) + try VMDetailsPrinter.printStatus(vms, format: .text, print: { printedLines.append($0) }) // Then #expect(printedLines.count == 2) @@ -49,6 +49,6 @@ struct VMDetailsPrinterTests { #expect(headerParts == ["name", "os", "cpu", "memory", "disk", "status", "ip", "vnc"]) let vmParts = printedLines[1].split(whereSeparator: \.isWhitespace) - #expect(vmParts == ["name", "os", "2", "0.00G", "24.0B/30.0B", "status", "0.0.0.0", "vncUrl"]) + #expect(vmParts == ["name", "os", "2", "0.00G", "24.0B/30.0B", "status", "0.0.0.0", "vncUrl"]) } } From 1fc45e81276f83876c44ead10ce40b6a42f22cb7 Mon Sep 17 00:00:00 2001 From: Pedro Date: Sun, 9 Feb 2025 17:18:33 +0100 Subject: [PATCH 4/4] Fix tests --- tests/VM/VMDetailsPrinterTests.swift | 98 ++++++++++++++++------------ 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/tests/VM/VMDetailsPrinterTests.swift b/tests/VM/VMDetailsPrinterTests.swift index fad026c2..95a31ff1 100644 --- a/tests/VM/VMDetailsPrinterTests.swift +++ b/tests/VM/VMDetailsPrinterTests.swift @@ -5,50 +5,64 @@ import Foundation struct VMDetailsPrinterTests { @Test func printStatus_whenJSON() throws { - // Given - let vms: [VMDetails] = [VMDetails(name: "name", - os: "os", - cpuCount: 2, - memorySize: 1024, - diskSize: .init(allocated: 24, total: 30), - status: "status", - vncUrl: "vncUrl", - ipAddress: "0.0.0.0")] - let jsonEncoder = JSONEncoder() - jsonEncoder.outputFormatting = .prettyPrinted - let expectedOutput = try String(data: jsonEncoder.encode(vms), encoding: .utf8)! - - // When - var printedStatus: String? - try VMDetailsPrinter.printStatus(vms, format: .json, print: { printedStatus = $0 }) + // Given + let vms: [VMDetails] = [VMDetails(name: "name", + os: "os", + cpuCount: 2, + memorySize: 1024, + diskSize: .init(allocated: 24, total: 30), + status: "status", + vncUrl: "vncUrl", + ipAddress: "0.0.0.0")] + let jsonEncoder = JSONEncoder() + jsonEncoder.outputFormatting = .prettyPrinted + let expectedOutput = try String(data: jsonEncoder.encode(vms), encoding: .utf8)! + + // When + var printedStatus: String? + try VMDetailsPrinter.printStatus(vms, format: .json, print: { printedStatus = $0 }) - // Then - #expect(printedStatus == expectedOutput) - } - - @Test func printStatus_whenNotJSON() throws { - // Given - let vms: [VMDetails] = [VMDetails(name: "name", - os: "os", - cpuCount: 2, - memorySize: 1024, - diskSize: .init(allocated: 24, total: 30), - status: "status", - vncUrl: "vncUrl", - ipAddress: "0.0.0.0")] + // Then + // Decode both JSONs and compare the actual data structures + let jsonDecoder = JSONDecoder() + let printedVMs = try jsonDecoder.decode([VMDetails].self, from: printedStatus!.data(using: .utf8)!) + let expectedVMs = try jsonDecoder.decode([VMDetails].self, from: expectedOutput.data(using: .utf8)!) + + #expect(printedVMs.count == expectedVMs.count) + for (printed, expected) in zip(printedVMs, expectedVMs) { + #expect(printed.name == expected.name) + #expect(printed.os == expected.os) + #expect(printed.cpuCount == expected.cpuCount) + #expect(printed.memorySize == expected.memorySize) + #expect(printed.diskSize.allocated == expected.diskSize.allocated) + #expect(printed.diskSize.total == expected.diskSize.total) + #expect(printed.status == expected.status) + #expect(printed.vncUrl == expected.vncUrl) + #expect(printed.ipAddress == expected.ipAddress) + } + } - // When - var printedLines: [String] = [] - try VMDetailsPrinter.printStatus(vms, format: .text, print: { printedLines.append($0) }) + @Test func printStatus_whenNotJSON() throws { + // Given + let vms: [VMDetails] = [VMDetails(name: "name", + os: "os", + cpuCount: 2, + memorySize: 1024, + diskSize: .init(allocated: 24, total: 30), + status: "status", + vncUrl: "vncUrl", + ipAddress: "0.0.0.0")] + + // When + var printedLines: [String] = [] + try VMDetailsPrinter.printStatus(vms, format: .text, print: { printedLines.append($0) }) - // Then - #expect(printedLines.count == 2) - - - let headerParts = printedLines[0].split(whereSeparator: \.isWhitespace) - #expect(headerParts == ["name", "os", "cpu", "memory", "disk", "status", "ip", "vnc"]) + // Then + #expect(printedLines.count == 2) + + let headerParts = printedLines[0].split(whereSeparator: \.isWhitespace) + #expect(headerParts == ["name", "os", "cpu", "memory", "disk", "status", "ip", "vnc"]) - let vmParts = printedLines[1].split(whereSeparator: \.isWhitespace) - #expect(vmParts == ["name", "os", "2", "0.00G", "24.0B/30.0B", "status", "0.0.0.0", "vncUrl"]) - } + #expect(printedLines[1].split(whereSeparator: \.isWhitespace).map(String.init) == ["name", "os", "2", "0.00G", "24.0B/30.0B", "status", "0.0.0.0", "vncUrl"]) + } }