Files
2026-11-09 08:20:57 +00:00

100 lines
8.3 KiB
Swift

import XCTest
@testable import ACDryCore
final class ACDryCoreTests: XCTestCase {
func testCurrentAndLegacySchemas() throws {
let rows = try ContainerService.parse(ContainerService.demoContainers, kind: .containers)
XCTAssertEqual(rows.count, 2); XCTAssertEqual(rows[1].id, "demo-web-1"); XCTAssertEqual(rows[1].state, "running"); XCTAssertEqual(rows[1].project, "demo")
XCTAssertEqual(try ContainerService.parse("[{\"reference\":\"alpine:latest\"}]", kind: .images).first?.id, "alpine:latest")
XCTAssertEqual(try ContainerService.parse("[{\"configuration\":{\"name\":\"data\"}}]", kind: .volumes).first?.id, "data")
XCTAssertEqual(try ContainerService.parse("[{\"id\":\"default\"}]", kind: .networks).first?.id, "default")
for invalid in ["oops", "{}", "[{}]"] { XCTAssertThrowsError(try ContainerService.parse(invalid, kind: .containers)) }
}
func testComposeHintIsExplicitAndExact() throws {
let input = "[{\"configuration\":{\"id\":\"my-app-web-api-1\"}},{\"configuration\":{\"id\":\"other-web-1\"}}]"
XCTAssertEqual(try ContainerService.parse(input, kind: .containers).first?.project, "")
let rows = try ContainerService.parse(input, kind: .projects, projectHint: "my-app")
XCTAssertEqual(rows[0].project, "my-app"); XCTAssertEqual(rows[0].service, "web-api"); XCTAssertEqual(rows[1].project, "")
}
func testNativeArguments() {
XCTAssertEqual(ResourceKind.containers.listArguments, ["list", "--all", "--format", "json"])
XCTAssertEqual(ContainerService.detailArguments(kind: .containers, id: "web", mode: "Logs"), ["logs", "-n", "200", "web"])
XCTAssertEqual(ContainerService.detailArguments(kind: .networks, id: "default", mode: "Inspect"), ["network", "inspect", "default"])
XCTAssertEqual(ContainerService.actionArguments(kind: .projects, action: "prune", id: ""), ["prune"])
XCTAssertEqual(ContainerService.actionArguments(kind: .images, action: "delete", id: "alpine"), ["image", "delete", "alpine"])
}
func testDemoAndCPUUnits() async throws {
let backend = ContainerService(demo: true)
let resources = try await backend.withStats(backend.list(.containers))
let measured = resources.first(where: { $0.id == "demo-web-1" })!
XCTAssertEqual(measured.cpu, String(format: "%.2f%%", measured.cpuFraction! * 100))
XCTAssertGreaterThan(measured.cpuFraction!, 0)
do { _ = try await backend.action(kind: .containers, action: "restart", resource: resources.first); XCTFail("Demo must not claim mutation success") } catch { XCTAssertTrue(error.localizedDescription.contains("read-only")) }
}
func testProcessCaptureFailureAndTimeout() throws {
XCTAssertEqual(try ContainerService(binary: "/bin/echo").execute(["hello", "$(not-a-shell-command)"]).trimmingCharacters(in: .whitespacesAndNewlines), "hello $(not-a-shell-command)")
XCTAssertThrowsError(try ContainerService(binary: "/usr/bin/false").execute([]))
XCTAssertThrowsError(try ContainerService(binary: "/nonexistent/container").execute([]))
let start = Date(); XCTAssertThrowsError(try ContainerService(binary: "/bin/sleep").execute(["10"], timeout: 0.1)); XCTAssertLessThan(Date().timeIntervalSince(start), 4)
}
func testNetworkRatesAndCounterResets() async throws {
var rows = try await ContainerService(demo: true).withStats(ContainerService(demo: true).list(.containers))
var tracker = NetworkRateTracker()
let date = Date(timeIntervalSince1970: 0)
XCTAssertEqual(tracker.sample(rows, at: date).rx, 0)
let n = rows.firstIndex(where: { $0.id == "demo-web-1" })!
rows[n].rxBytes! += 3000; rows[n].txBytes! += 1500
let rates = tracker.sample(rows, at: date.addingTimeInterval(3))
XCTAssertEqual(rates.rx, 1000); XCTAssertEqual(rates.tx, 500)
rows[n].rxBytes = 0; rows[n].txBytes = 0
XCTAssertEqual(tracker.sample(rows, at: date.addingTimeInterval(6)).rx, 0)
_ = tracker.sample([], at: date.addingTimeInterval(9))
rows[n].rxBytes = 1_000_000
XCTAssertEqual(tracker.sample(rows, at: date.addingTimeInterval(12)).rx, 0)
}
func testRestartStopsBeforeStartingAndStopsOnFailure() async throws {
let dir = FileManager.default.temporaryDirectory.appendingPathComponent("acdry-tests-" + UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
let executable = dir.appendingPathComponent("container"), log = dir.appendingPathComponent("calls"), failure = dir.appendingPathComponent("fail-stop")
let script = "#!/bin/sh\necho \"$1\" >> \"\(log.path)\"\nif [ \"$1\" = stop ] && [ -f \"\(failure.path)\" ]; then exit 1; fi\n"
try script.write(to: executable, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: executable.path)
let backend = ContainerService(binary: executable.path)
let r = try ContainerService.parse(ContainerService.demoContainers, kind: .containers).first(where: { $0.state == "running" })!
_ = try await backend.action(kind: .containers, action: "restart", resource: r)
XCTAssertEqual(try String(contentsOf: log, encoding: .utf8), "stop\nstart\n")
try "".write(to: log, atomically: true, encoding: .utf8); try "".write(to: failure, atomically: true, encoding: .utf8)
do { _ = try await backend.action(kind: .containers, action: "restart", resource: r); XCTFail("Should fail") } catch { }
XCTAssertEqual(try String(contentsOf: log, encoding: .utf8), "stop\n")
}
func testStateVerificationAndReadOnlyDemo() async throws {
let backend = ContainerService(demo: true)
let resource = try await backend.waitForState(id: "demo-web-1", expected: "running", timeout: 1)
XCTAssertEqual(resource.state, "running")
do { _ = try await backend.waitForState(id: "demo-web-1", expected: "stopped", timeout: 0.01); XCTFail("Should time out") } catch { XCTAssertTrue(error.localizedDescription.contains("did not reach stopped")) }
do { _ = try await backend.run(["stop", "demo-web-1"]); XCTFail("Demo must be read-only") } catch { XCTAssertTrue(error.localizedDescription.contains("read-only")) }
}
func testVolumeMetadataAndMountReferences() throws {
let volumes = try ContainerService.parse("""
[{"configuration":{"name":"app-data","driver":"local","format":"ext4","source":"/volumes/app-data.img","sizeInBytes":67108864,"creationDate":"2026-11-08T12:00:00Z"}}]
""", kind: .volumes)
XCTAssertEqual(volumes[0].driver, "local"); XCTAssertEqual(volumes[0].format, "ext4"); XCTAssertNotEqual(volumes[0].capacity, "—")
let containers = try ContainerService.parse("""
[{"configuration":{"id":"app","mounts":[{"source":"/volumes/app-data.img","destination":"/data","type":{"volume":{"name":"app-data"}}},{"source":"/Users/example/project","destination":"/workspace","type":{"virtiofs":{}}},{"source":"","destination":"/tmp","type":{"tmpfs":{}}}]},"status":{"state":"stopped"}}]
""", kind: .containers)
let combined = VolumeInventory.combine(volumes: volumes, containers: containers)
XCTAssertEqual(combined.count, 2)
XCTAssertEqual(combined[0].usedBy, "app:/data"); XCTAssertFalse(combined[0].readOnlyStorage)
XCTAssertEqual(combined[1].storageKind, "Bind mount"); XCTAssertTrue(combined[1].readOnlyStorage); XCTAssertEqual(combined[1].usedBy, "app:/workspace")
XCTAssertEqual(VolumeInventory.combine(volumes: volumes, containers: containers, includeHostMounts: false).count, 1)
let legacy = try ContainerService.parse("[{\"name\":\"legacy\",\"driver\":\"local\",\"format\":\"ext4\",\"source\":\"/legacy\",\"createdAt\":\"2025-01-01\"}]", kind: .volumes)
XCTAssertEqual(legacy[0].created, "2025-01-01"); XCTAssertEqual(legacy[0].source, "/legacy")
}
func testConcurrentPipeDrainAndOutputLimit() throws {
// A subprocess that fills both pipes would deadlock a sequential reader.
let out = try ContainerService(binary: "/bin/sh").execute(["-c", "i=0; while [ $i -lt 5000 ]; do echo stdout-line; echo stderr-line >&2; i=$((i+1)); done"])
XCTAssertEqual(out.components(separatedBy: "stdout-line").count - 1, 5000)
}
}