diff --git a/Sources/tart/Commands/Get.swift b/Sources/tart/Commands/Get.swift index 7ffada69..b53011f0 100644 --- a/Sources/tart/Commands/Get.swift +++ b/Sources/tart/Commands/Get.swift @@ -1,7 +1,7 @@ import ArgumentParser import Foundation -fileprivate struct VMInfo: Encodable { +struct GetVMInfo: Encodable { let OS: OS let CPU: Int let Memory: UInt64 @@ -11,6 +11,71 @@ fileprivate struct VMInfo: Encodable { let Display: String let Running: Bool let State: String + + private let diskBytes: Int + private let sizeBytes: Int + + enum CodingKeys: String, CodingKey { + case OS + case CPU + case Memory + case Disk + case DiskFormat + case Size + case Display + case Running + case State + } + + init( + OS: OS, + CPU: Int, + Memory: UInt64, + diskBytes: Int, + DiskFormat: String, + sizeBytes: Int, + Display: String, + Running: Bool, + State: String + ) { + self.OS = OS + self.CPU = CPU + self.Memory = Memory + self.Disk = diskBytes / 1000 / 1000 / 1000 + self.DiskFormat = DiskFormat + self.Size = String(format: "%.3f", Float(sizeBytes) / 1000 / 1000 / 1000) + self.Display = Display + self.Running = Running + self.State = State + self.diskBytes = diskBytes + self.sizeBytes = sizeBytes + } + + var textInfo: GetVMTextInfo { + GetVMTextInfo( + OS: OS, + CPU: CPU, + Memory: Memory, + Disk: ByteCountFormatter.string(fromByteCount: Int64(diskBytes), countStyle: .file), + DiskFormat: DiskFormat, + Size: ByteCountFormatter.string(fromByteCount: Int64(sizeBytes), countStyle: .file), + Display: Display, + Running: Running, + State: State + ) + } +} + +struct GetVMTextInfo: Encodable { + let OS: OS + let CPU: Int + let Memory: UInt64 + let Disk: String + let DiskFormat: String + let Size: String + let Display: String + let Running: Bool + let State: String } struct Get: AsyncParsableCommand { @@ -27,7 +92,23 @@ struct Get: AsyncParsableCommand { let vmConfig = try VMConfig(fromURL: vmDir.configURL) let memorySizeInMb = vmConfig.memorySize / 1024 / 1024 - let info = VMInfo(OS: vmConfig.os, CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), DiskFormat: vmConfig.diskFormat.rawValue, Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000), Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state().rawValue) - print(format.renderSingle(info)) + let info = GetVMInfo( + OS: vmConfig.os, + CPU: vmConfig.cpuCount, + Memory: memorySizeInMb, + diskBytes: try vmDir.sizeBytes(), + DiskFormat: vmConfig.diskFormat.rawValue, + sizeBytes: try vmDir.allocatedSizeBytes(), + Display: vmConfig.display.description, + Running: try vmDir.running(), + State: try vmDir.state().rawValue + ) + + switch format { + case .text: + print(format.renderSingle(info.textInfo)) + case .json: + print(format.renderSingle(info)) + } } } diff --git a/Sources/tart/Commands/List.swift b/Sources/tart/Commands/List.swift index 6261157f..e726285a 100644 --- a/Sources/tart/Commands/List.swift +++ b/Sources/tart/Commands/List.swift @@ -1,8 +1,9 @@ import ArgumentParser import Dispatch +import Foundation import SwiftUI -fileprivate struct VMInfo: Encodable { +struct ListVMInfo: Encodable { let Source: String let Name: String let Disk: Int @@ -10,6 +11,53 @@ fileprivate struct VMInfo: Encodable { let Accessed: String let Running: Bool let State: String + + private let diskBytes: Int + private let sizeBytes: Int + + enum CodingKeys: String, CodingKey { + case Source + case Name + case Disk + case Size + case Accessed + case Running + case State + } + + init(Source: String, Name: String, diskBytes: Int, sizeBytes: Int, Accessed: String, Running: Bool, State: String) { + self.Source = Source + self.Name = Name + self.Disk = diskBytes / 1000 / 1000 / 1000 + self.Size = sizeBytes / 1000 / 1000 / 1000 + self.Accessed = Accessed + self.Running = Running + self.State = State + self.diskBytes = diskBytes + self.sizeBytes = sizeBytes + } + + var textInfo: ListVMTextInfo { + ListVMTextInfo( + Source: Source, + Name: Name, + Disk: ByteCountFormatter.string(fromByteCount: Int64(diskBytes), countStyle: .file), + Size: ByteCountFormatter.string(fromByteCount: Int64(sizeBytes), countStyle: .file), + Accessed: Accessed, + Running: Running, + State: State + ) + } +} + +struct ListVMTextInfo: Encodable { + let Source: String + let Name: String + let Disk: String + let Size: String + let Accessed: String + let Running: Bool + let State: String } struct List: AsyncParsableCommand { @@ -35,15 +83,15 @@ struct List: AsyncParsableCommand { } func run() async throws { - var infos: [VMInfo] = [] + var infos: [ListVMInfo] = [] if source == nil || source == "local" { infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in - try VMInfo( + try ListVMInfo( Source: "local", Name: name, - Disk: vmDir.sizeGB(), - Size: vmDir.allocatedSizeGB(), + diskBytes: vmDir.sizeBytes(), + sizeBytes: vmDir.allocatedSizeBytes(), Accessed: formatAccessDate(try vmDir.accessDate()), Running: vmDir.running(), State: vmDir.state().rawValue @@ -53,11 +101,11 @@ struct List: AsyncParsableCommand { if source == nil || source == "oci" { infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in - try VMInfo( + try ListVMInfo( Source: "OCI", Name: name, - Disk: vmDir.sizeGB(), - Size: vmDir.allocatedSizeGB(), + diskBytes: vmDir.sizeBytes(), + sizeBytes: vmDir.allocatedSizeBytes(), Accessed: formatAccessDate(try vmDir.accessDate()), Running: vmDir.running(), State: vmDir.state().rawValue @@ -70,11 +118,16 @@ struct List: AsyncParsableCommand { print(info.Name) } } else { - print(format.renderList(infos)) + switch format { + case .text: + print(format.renderList(infos.map { $0.textInfo })) + case .json: + print(format.renderList(infos)) + } } } - private func sortedInfos(_ infos: [VMInfo]) -> [VMInfo] { + private func sortedInfos(_ infos: [ListVMInfo]) -> [ListVMInfo] { infos.sorted(by: { left, right in left.Name < right.Name }) } diff --git a/Tests/TartTests/GetTests.swift b/Tests/TartTests/GetTests.swift new file mode 100644 index 00000000..5b650eee --- /dev/null +++ b/Tests/TartTests/GetTests.swift @@ -0,0 +1,47 @@ +import XCTest +import Foundation +@testable import tart + +final class GetTests: XCTestCase { + func testGetJSONKeepsDiskAsIntegerGBAndSizeAsThreeDecimalGBString() throws { + let info = GetVMInfo( + OS: .linux, + CPU: 4, + Memory: 8192, + diskBytes: 51_400_000_000, + DiskFormat: "raw", + sizeBytes: 17_234_000_000, + Display: "1024x768", + Running: false, + State: "stopped" + ) + + let json = Format.json.renderSingle(info) + let data = try XCTUnwrap(json.data(using: .utf8)) + let vmInfo = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + + XCTAssertEqual(vmInfo["Disk"] as? Int, 51) + XCTAssertEqual(vmInfo["Size"] as? String, "17.234") + } + + func testGetTextUsesHumanReadableDiskAndSize() throws { + let info = GetVMInfo( + OS: .linux, + CPU: 4, + Memory: 8192, + diskBytes: 51_400_000_000, + DiskFormat: "raw", + sizeBytes: 17_200_000_000, + Display: "1024x768", + Running: false, + State: "stopped" + ) + + let text = Format.text.renderSingle(info.textInfo) + + XCTAssertTrue(text.contains("Disk")) + XCTAssertTrue(text.contains("Size")) + XCTAssertTrue(text.contains(ByteCountFormatter.string(fromByteCount: 51_400_000_000, countStyle: .file))) + XCTAssertTrue(text.contains(ByteCountFormatter.string(fromByteCount: 17_200_000_000, countStyle: .file))) + } +} diff --git a/Tests/TartTests/ListTests.swift b/Tests/TartTests/ListTests.swift new file mode 100644 index 00000000..b87004db --- /dev/null +++ b/Tests/TartTests/ListTests.swift @@ -0,0 +1,44 @@ +import XCTest +import Foundation +@testable import tart + +final class ListTests: XCTestCase { + func testListJSONKeepsDiskAndSizeAsIntegerGB() throws { + let info = ListVMInfo( + Source: "local", + Name: "test", + diskBytes: 51_400_000_000, + sizeBytes: 17_200_000_000, + Accessed: "2026-06-11T00:00:00Z", + Running: false, + State: "stopped" + ) + + let json = Format.json.renderList([info]) + let data = try XCTUnwrap(json.data(using: .utf8)) + let decoded = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [[String: Any]]) + let vmInfo = try XCTUnwrap(decoded.first) + + XCTAssertEqual(vmInfo["Disk"] as? Int, 51) + XCTAssertEqual(vmInfo["Size"] as? Int, 17) + } + + func testListTextUsesHumanReadableDiskAndSize() throws { + let info = ListVMInfo( + Source: "local", + Name: "test", + diskBytes: 51_400_000_000, + sizeBytes: 17_200_000_000, + Accessed: "1 second ago", + Running: false, + State: "stopped" + ) + + let text = Format.text.renderList([info.textInfo]) + + XCTAssertTrue(text.contains("Disk")) + XCTAssertTrue(text.contains("Size")) + XCTAssertTrue(text.contains(ByteCountFormatter.string(fromByteCount: 51_400_000_000, countStyle: .file))) + XCTAssertTrue(text.contains(ByteCountFormatter.string(fromByteCount: 17_200_000_000, countStyle: .file))) + } +}