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