mirror of https://github.com/cirruslabs/tart.git
Display byte units in text list/get output
This commit is contained in:
parent
0a01a4430c
commit
cd4b681ab6
|
|
@ -1,7 +1,7 @@
|
||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
fileprivate struct VMInfo: Encodable {
|
struct GetVMInfo: Encodable {
|
||||||
let OS: OS
|
let OS: OS
|
||||||
let CPU: Int
|
let CPU: Int
|
||||||
let Memory: UInt64
|
let Memory: UInt64
|
||||||
|
|
@ -11,6 +11,71 @@ fileprivate struct VMInfo: Encodable {
|
||||||
let Display: String
|
let Display: String
|
||||||
let Running: Bool
|
let Running: Bool
|
||||||
let State: String
|
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 {
|
struct Get: AsyncParsableCommand {
|
||||||
|
|
@ -27,7 +92,23 @@ struct Get: AsyncParsableCommand {
|
||||||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
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))
|
print(format.renderSingle(info))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import Dispatch
|
import Dispatch
|
||||||
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
fileprivate struct VMInfo: Encodable {
|
struct ListVMInfo: Encodable {
|
||||||
let Source: String
|
let Source: String
|
||||||
let Name: String
|
let Name: String
|
||||||
let Disk: Int
|
let Disk: Int
|
||||||
|
|
@ -10,6 +11,53 @@ fileprivate struct VMInfo: Encodable {
|
||||||
let Accessed: String
|
let Accessed: String
|
||||||
let Running: Bool
|
let Running: Bool
|
||||||
let State: String
|
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 {
|
struct List: AsyncParsableCommand {
|
||||||
|
|
@ -35,15 +83,15 @@ struct List: AsyncParsableCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
var infos: [VMInfo] = []
|
var infos: [ListVMInfo] = []
|
||||||
|
|
||||||
if source == nil || source == "local" {
|
if source == nil || source == "local" {
|
||||||
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
||||||
try VMInfo(
|
try ListVMInfo(
|
||||||
Source: "local",
|
Source: "local",
|
||||||
Name: name,
|
Name: name,
|
||||||
Disk: vmDir.sizeGB(),
|
diskBytes: vmDir.sizeBytes(),
|
||||||
Size: vmDir.allocatedSizeGB(),
|
sizeBytes: vmDir.allocatedSizeBytes(),
|
||||||
Accessed: formatAccessDate(try vmDir.accessDate()),
|
Accessed: formatAccessDate(try vmDir.accessDate()),
|
||||||
Running: vmDir.running(),
|
Running: vmDir.running(),
|
||||||
State: vmDir.state().rawValue
|
State: vmDir.state().rawValue
|
||||||
|
|
@ -53,11 +101,11 @@ struct List: AsyncParsableCommand {
|
||||||
|
|
||||||
if source == nil || source == "oci" {
|
if source == nil || source == "oci" {
|
||||||
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
||||||
try VMInfo(
|
try ListVMInfo(
|
||||||
Source: "OCI",
|
Source: "OCI",
|
||||||
Name: name,
|
Name: name,
|
||||||
Disk: vmDir.sizeGB(),
|
diskBytes: vmDir.sizeBytes(),
|
||||||
Size: vmDir.allocatedSizeGB(),
|
sizeBytes: vmDir.allocatedSizeBytes(),
|
||||||
Accessed: formatAccessDate(try vmDir.accessDate()),
|
Accessed: formatAccessDate(try vmDir.accessDate()),
|
||||||
Running: vmDir.running(),
|
Running: vmDir.running(),
|
||||||
State: vmDir.state().rawValue
|
State: vmDir.state().rawValue
|
||||||
|
|
@ -70,11 +118,16 @@ struct List: AsyncParsableCommand {
|
||||||
print(info.Name)
|
print(info.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
switch format {
|
||||||
|
case .text:
|
||||||
|
print(format.renderList(infos.map { $0.textInfo }))
|
||||||
|
case .json:
|
||||||
print(format.renderList(infos))
|
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 })
|
infos.sorted(by: { left, right in left.Name < right.Name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue