//===----------------------------------------------------------------------===// // Copyright © 2026 Apple Inc. and the container project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //===----------------------------------------------------------------------===// import ContainerResource import Foundation import Testing @testable import ContainerCommands // MARK: - Test ListDisplayable conformer private struct TestItem: ListDisplayable, Codable { let id: String let name: String static var tableHeader: [String] { ["ID", "NAME"] } var tableRow: [String] { [id, name] } var quietValue: String { id } } // MARK: - TableOutput tests struct TableOutputTests { @Test func emptyRowsProducesEmptyString() { #expect(TableOutput(rows: []).format() == "") } @Test func headerOnlyRow() { #expect(TableOutput(rows: [["ID", "NAME"]]).format() == "ID NAME") } @Test func columnsPaddedToMaxWidth() { let output = TableOutput(rows: [ ["ID", "NAME"], ["1", "short"], ["2", "a longer name"], ]).format() let lines = output.split(separator: "\n") #expect(lines.count == 3) #expect(lines[0].hasPrefix("ID ")) #expect(lines[1].hasPrefix("1 ")) #expect(lines[2].hasPrefix("2 ")) } @Test func customSpacing() { let output = TableOutput(rows: [["A", "B"], ["1", "2"]], spacing: 4).format() #expect(output.contains("A B")) } @Test func lastColumnNotPadded() { let lines = TableOutput(rows: [["A", "B"], ["1", "2"]]).format().split(separator: "\n") for line in lines { #expect(!line.hasSuffix(" ")) } } @Test func singleColumnNoPadding() { let output = TableOutput(rows: [["DOMAIN"], ["example.com"], ["test.local"]]).format() #expect(output == "DOMAIN\nexample.com\ntest.local") } @Test func outputLineCountMatchesInputRowCount() { let rows = [["H1", "H2"], ["a", "b"], ["c", "d"], ["e", "f"]] let lines = TableOutput(rows: rows).format().split(separator: "\n") #expect(lines.count == rows.count) } } // MARK: - renderTable tests struct RenderTableTests { @Test func rendersHeaderAndRows() { let items = [TestItem(id: "abc", name: "first"), TestItem(id: "def", name: "second")] let output = Output.renderTable(items) #expect(output.contains("ID")) #expect(output.contains("NAME")) #expect(output.contains("abc")) #expect(output.contains("second")) } @Test func emptyListRendersHeaderOnly() { let output = Output.renderTable([TestItem]()) #expect(output.contains("ID")) #expect(output.contains("NAME")) #expect(!output.contains("\n")) } @Test func columnCountMatchesHeader() { let items = [TestItem(id: "1", name: "test")] let lines = Output.renderTable(items).split(separator: "\n") let headerColumnCount = lines[0].split(separator: " ", omittingEmptySubsequences: true).count let rowColumnCount = lines[1].split(separator: " ", omittingEmptySubsequences: true).count #expect(headerColumnCount == rowColumnCount) } } // MARK: - renderList tests struct RenderListTests { @Test func tableMode() { let items = [TestItem(id: "abc", name: "first")] let output = Output.renderList(items, quiet: false) #expect(output.contains("ID")) #expect(output.contains("abc")) #expect(output.contains("first")) } @Test func quietMode() { let items = [TestItem(id: "abc", name: "first"), TestItem(id: "def", name: "second")] let output = Output.renderList(items, quiet: true) #expect(output == "abc\ndef") } @Test func quietModeEmptyList() { let output = Output.renderList([TestItem](), quiet: true) #expect(output == "") } } // MARK: - renderJSON tests struct RenderJSONTests { @Test func compactProducesValidJSON() throws { let items = [TestItem(id: "a", name: "b")] let json = try Output.renderJSON(items) let decoded = try JSONDecoder().decode([TestItem].self, from: json.data(using: .utf8)!) #expect(decoded.count == 1) #expect(decoded[0].id == "a") #expect(decoded[0].name == "b") } @Test func compactIsSingleLine() throws { let items = [TestItem(id: "a", name: "b"), TestItem(id: "c", name: "d")] let json = try Output.renderJSON(items) #expect(!json.contains("\n")) } @Test func prettyIsMultiLine() throws { let items = [TestItem(id: "a", name: "b")] let json = try Output.renderJSON(items, options: .pretty) #expect(json.contains("\n")) } @Test func prettyHasSortedKeys() throws { let json = try Output.renderJSON(["z": 1, "a": 2], options: .pretty) let aIndex = json.range(of: "\"a\"")!.lowerBound let zIndex = json.range(of: "\"z\"")!.lowerBound #expect(aIndex < zIndex) } @Test func customDateStrategy() throws { struct Dated: Codable { let date: Date } let item = Dated(date: Date(timeIntervalSince1970: 0)) let options = JSONOptions( outputFormatting: [.prettyPrinted, .sortedKeys], dateEncodingStrategy: .iso8601 ) let json = try Output.renderJSON(item, options: options) #expect(json.contains("1970-01-01")) } @Test func arrayEncodingMatchesOldJoinApproach() throws { // Verify renderJSON(array) is structurally identical to the old // jsonArray() approach (encode each element, join with commas). let items = [TestItem(id: "x", name: "y"), TestItem(id: "a", name: "b")] let wholeArray = try Output.renderJSON(items) let perElement = try items.map { try Output.renderJSON($0) } let joined = "[\(perElement.joined(separator: ","))]" let decoded1 = try JSONDecoder().decode([TestItem].self, from: wholeArray.data(using: .utf8)!) let decoded2 = try JSONDecoder().decode([TestItem].self, from: joined.data(using: .utf8)!) #expect(decoded1.count == decoded2.count) #expect(decoded1[0].id == decoded2[0].id) #expect(decoded1[1].id == decoded2[1].id) } } // MARK: - renderTOML tests struct RenderTOMLTests { @Test func topLevelArrayProducesNonEmptyTOML() throws { let items = [TestItem(id: "a", name: "first"), TestItem(id: "c", name: "second")] let toml = try Output.renderTOML(items) #expect(!toml.isEmpty) #expect(toml.contains("first")) #expect(toml.contains("second")) } @Test func emptyArrayProducesNonEmptyTOML() throws { let toml = try Output.renderTOML([TestItem]()) #expect(!toml.isEmpty) } @Test func singleValueEncodesAsTopLevelTable() throws { let toml = try Output.renderTOML(TestItem(id: "x", name: "y")) #expect(toml.contains("id")) #expect(toml.contains("x")) } } // MARK: - JSONOptions tests struct JSONOptionsTests { @Test func compactPresetHasSortedKeys() { let opts = JSONOptions.compact #expect(opts.outputFormatting == [.sortedKeys]) #expect(!opts.outputFormatting.contains(.prettyPrinted)) } @Test func prettyPresetHasBothFlags() { let opts = JSONOptions.pretty #expect(opts.outputFormatting.contains(.prettyPrinted)) #expect(opts.outputFormatting.contains(.sortedKeys)) } } // MARK: - ManagedContainer conformance tests struct ManagedContainerDisplayTests { @Test func tableHeaderHasNineColumns() { #expect(ManagedContainer.tableHeader.count == 9) #expect(ManagedContainer.tableHeader[0] == "ID") #expect(ManagedContainer.tableHeader[4] == "STATE") #expect(ManagedContainer.tableHeader[8] == "STARTED") } } // MARK: - NetworkResource ListDisplayable conformance tests struct NetworkResourceDisplayTests { @Test func tableHeaderHasTwoColumns() { #expect(NetworkResource.tableHeader.count == 2) #expect(NetworkResource.tableHeader == ["NETWORK", "SUBNET"]) } } // MARK: - ListFormat tests struct ListFormatTests { @Test func hasAllOutputFormatCases() { #expect(ListFormat.allCases.count == 4) #expect(ListFormat.json.rawValue == "json") #expect(ListFormat.table.rawValue == "table") #expect(ListFormat.yaml.rawValue == "yaml") #expect(ListFormat.toml.rawValue == "toml") } }