Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions Sources/tart/Commands/Get.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ArgumentParser
import Foundation

fileprivate struct VMInfo: Encodable {
struct GetVMInfo: Encodable {
let OS: OS
let CPU: Int
let Memory: UInt64
Expand All @@ -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 {
Expand All @@ -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))
}
}
}
73 changes: 63 additions & 10 deletions Sources/tart/Commands/List.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,63 @@
import ArgumentParser
import Dispatch
import Foundation
import SwiftUI

fileprivate struct VMInfo: Encodable {
struct ListVMInfo: Encodable {
let Source: String
let Name: String
let Disk: Int
let Size: Int
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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 })
}

Expand Down
47 changes: 47 additions & 0 deletions Tests/TartTests/GetTests.swift
Original file line number Diff line number Diff line change
@@ -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)))
}
}
44 changes: 44 additions & 0 deletions Tests/TartTests/ListTests.swift
Original file line number Diff line number Diff line change
@@ -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)))
}
}