Display byte units in text list/get output

This commit is contained in:
Nanook 2026-06-11 00:51:40 +00:00
parent 0a01a4430c
commit cd4b681ab6
No known key found for this signature in database
GPG Key ID: 4F1D9A261C2F05B5
4 changed files with 238 additions and 13 deletions

View File

@ -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)
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))
}
}
}

View File

@ -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 {
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 })
}

View File

@ -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)))
}
}

View File

@ -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)))
}
}