diff --git a/Makefile b/Makefile index db1910863..74e4e987a 100644 --- a/Makefile +++ b/Makefile @@ -203,9 +203,12 @@ WARMUP_FILTER = ImageWarmup CONCURRENT_TEST_SUITES ?= \ TestCLIExportCommand/ \ + TestCLIHelp \ TestCLIMachineCommand/ \ TestCLIRmRaceCondition/ \ - TestCLIStop/ + TestCLIStatus \ + TestCLIStop/ \ + TestCLIVersion/ CONCURRENT_FILTER = $(subst $(space),|,$(strip $(CONCURRENT_TEST_SUITES))) GLOBAL_TEST_SUITES ?= \ @@ -271,9 +274,6 @@ coverage-integration-new: all $(RUN_INTEGRATION) INTEGRATION_TEST_SUITES ?= \ - TestCLIHelp \ - TestCLIStatus \ - TestCLIVersion \ TestCLINetwork \ TestCLIRunLifecycle \ TestCLIRunCapabilities \ diff --git a/Tests/CLITests/Subcommands/System/TestCLIStatus.swift b/Tests/CLITests/Subcommands/System/TestCLIStatus.swift deleted file mode 100644 index b6ac115a8..000000000 --- a/Tests/CLITests/Subcommands/System/TestCLIStatus.swift +++ /dev/null @@ -1,166 +0,0 @@ -//===----------------------------------------------------------------------===// -// 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 Foundation -import Testing - -/// Tests for `container system status` output formats and content validation. -@Suite(.serialSuites) -final class TestCLIStatus: CLITest { - struct StatusJSON: Codable { - let status: String - let appRoot: String - let installRoot: String - let logRoot: String? - let apiServerVersion: String - let apiServerCommit: String - let apiServerBuild: String - let apiServerAppName: String - } - - @Test func defaultDisplaysTable() throws { - let (data, out, _, status) = try run(arguments: ["system", "status"]) // default is table - - // If apiserver is not running, skip this test - guard status == 0 else { - return - } - - #expect(!out.isEmpty) - - // Validate table structure - let lines = out.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - #expect(lines.count >= 2) // header + at least one data row - - // Check for header row - #expect(lines[0].contains("FIELD") && lines[0].contains("VALUE")) - - // Check for key fields in output - let fullOutput = lines.joined(separator: "\n") - #expect(fullOutput.contains("status")) - #expect(fullOutput.contains("running")) - #expect(fullOutput.contains("appRoot")) - #expect(fullOutput.contains("installRoot")) - #expect(fullOutput.contains("apiserver.version")) - #expect(fullOutput.contains("apiserver.commit")) - - _ = data // silence unused warning if assertions short-circuit - } - - @Test func jsonFormat() throws { - let (data, out, _, status) = try run(arguments: ["system", "status", "--format", "json"]) - - // If apiserver is not running, validate error JSON - if status != 0 { - #expect(!out.isEmpty) - let decoded = try JSONDecoder().decode(StatusJSON.self, from: data) - #expect(decoded.status == "not running" || decoded.status == "unregistered") - return - } - - #expect(!out.isEmpty) - - let decoded = try JSONDecoder().decode(StatusJSON.self, from: data) - #expect(decoded.status == "running") - #expect(!decoded.appRoot.isEmpty) - #expect(!decoded.installRoot.isEmpty) - #expect(!decoded.apiServerVersion.isEmpty) - #expect(!decoded.apiServerCommit.isEmpty) - #expect(!decoded.apiServerBuild.isEmpty) - #expect(!decoded.apiServerAppName.isEmpty) - } - - @Test func explicitTableFormat() throws { - let (_, out, _, status) = try run(arguments: ["system", "status", "--format", "table"]) - - // If apiserver is not running, skip validation - guard status == 0 else { - return - } - - #expect(!out.isEmpty) - - let lines = out.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - #expect(lines.count >= 2) - #expect(lines[0].contains("FIELD") && lines[0].contains("VALUE")) - - let fullOutput = lines.joined(separator: "\n") - #expect(fullOutput.contains("status")) - #expect(fullOutput.contains("running")) - } - - @Test func statusFieldsMatch() throws { - // Validate that JSON and table outputs contain the same information - let (jsonData, _, _, jsonStatus) = try run(arguments: ["system", "status", "--format", "json"]) - let (_, tableOut, _, tableStatus) = try run(arguments: ["system", "status", "--format", "table"]) - - #expect(jsonStatus == tableStatus) - - // If apiserver is not running, just verify consistency - guard jsonStatus == 0 else { - return - } - - let decoded = try JSONDecoder().decode(StatusJSON.self, from: jsonData) - - // Verify table output contains key values from JSON - #expect(tableOut.contains(decoded.status)) - #expect(tableOut.contains(decoded.appRoot)) - #expect(tableOut.contains(decoded.installRoot)) - #expect(tableOut.contains(decoded.apiServerVersion)) - #expect(tableOut.contains(decoded.apiServerCommit)) - #expect(tableOut.contains(decoded.apiServerBuild)) - #expect(tableOut.contains(decoded.apiServerAppName)) - } - - @Test func jsonOutputValidStructure() throws { - let (data, _, _, status) = try run(arguments: ["system", "status", "--format", "json"]) - - // Should always produce valid JSON regardless of status - #expect(throws: Never.self) { - _ = try JSONDecoder().decode(StatusJSON.self, from: data) - } - - let decoded = try JSONDecoder().decode(StatusJSON.self, from: data) - - if status == 0 { - // When running, all fields should be populated - #expect(decoded.status == "running") - #expect(!decoded.appRoot.isEmpty) - #expect(!decoded.installRoot.isEmpty) - #expect(!decoded.apiServerVersion.isEmpty) - } else { - // When not running, status should indicate the issue - #expect(decoded.status == "not running" || decoded.status == "unregistered") - } - } - - @Test func prefixOption() throws { - // Test with explicit prefix (should work the same as default) - let (_, out, _, status) = try run(arguments: ["system", "status", "--prefix", "com.apple.container."]) - - guard status == 0 else { - return - } - - #expect(!out.isEmpty) - let lines = out.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - #expect(lines.count >= 2) - } -} diff --git a/Tests/CLITests/Subcommands/System/TestCLIVersion.swift b/Tests/CLITests/Subcommands/System/TestCLIVersion.swift deleted file mode 100644 index 4fd72e31f..000000000 --- a/Tests/CLITests/Subcommands/System/TestCLIVersion.swift +++ /dev/null @@ -1,113 +0,0 @@ -//===----------------------------------------------------------------------===// -// Copyright © 2025-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 Foundation -import Testing -import Yams - -/// Tests for `container system version` output formats and build type detection. -@Suite(.serialSuites) -final class TestCLIVersion: CLITest { - struct VersionInfo: Codable { - let version: String - let buildType: String - let commit: String - let appName: String - } - - struct VersionOutput: Codable { - let version: String - let buildType: String - let commit: String - let appName: String - let server: VersionInfo? - } - - private func expectedBuildType() -> String { - #if DEBUG - return "debug" - #else - return "release" - #endif - } - - @Test func defaultDisplaysTable() throws { - let (data, out, err, status) = try run(arguments: ["system", "version"]) // default is table - #expect(status == 0, "system version should succeed, stderr: \(err)") - #expect(!out.isEmpty) - - // Validate table structure - let lines = out.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - #expect(lines.count >= 2) // header + at least CLI row - #expect(lines[0].contains("COMPONENT") && lines[0].contains("VERSION") && lines[0].contains("BUILD") && lines[0].contains("COMMIT")) - #expect(lines[1].hasPrefix("container")) - - // Build should reflect the binary we are running (debug/release) - let expected = expectedBuildType() - #expect(lines.joined(separator: "\n").contains(" \(expected) ")) - _ = data // silence unused warning if assertions short-circuit - } - - @Test func jsonFormat() throws { - let (data, out, err, status) = try run(arguments: ["system", "version", "--format", "json"]) - #expect(status == 0, "system version --format json should succeed, stderr: \(err)") - #expect(!out.isEmpty) - - let decoded = try JSONDecoder().decode([VersionOutput].self, from: data) - #expect(decoded[0].appName == "container") - #expect(!decoded[0].version.isEmpty) - #expect(!decoded[0].commit.isEmpty) - - let expected = expectedBuildType() - #expect(decoded[0].buildType == expected) - } - - @Test func yamlFormat() throws { - let (data, out, err, status) = try run(arguments: ["system", "version", "--format", "yaml"]) - #expect(status == 0, "system version --format yaml should succeed, stderr: \(err)") - #expect(!out.isEmpty) - - let decoded = try YAMLDecoder().decode([VersionOutput].self, from: data) - #expect(decoded[0].appName == "container") - #expect(!decoded[0].version.isEmpty) - #expect(!decoded[0].commit.isEmpty) - - let expected = expectedBuildType() - #expect(decoded[0].buildType == expected) - } - - @Test func explicitTableFormat() throws { - let (_, out, err, status) = try run(arguments: ["system", "version", "--format", "table"]) - #expect(status == 0, "system version --format table should succeed, stderr: \(err)") - #expect(!out.isEmpty) - - let lines = out.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: .newlines) - #expect(lines.count >= 2) - #expect(lines[0].contains("COMPONENT") && lines[0].contains("VERSION") && lines[0].contains("BUILD") && lines[0].contains("COMMIT")) - } - - @Test func buildTypeMatchesBinary() throws { - // Validate build type via JSON to avoid parsing table text loosely - let (data, _, err, status) = try run(arguments: ["system", "version", "--format", "json"]) - #expect(status == 0, "version --format json should succeed, stderr: \(err)") - let decoded = try JSONDecoder().decode([VersionOutput].self, from: data) - - let expected = expectedBuildType() - #expect(decoded[0].buildType == expected, "Expected build type \(expected) but got \(decoded[0].buildType)") - } -} diff --git a/Tests/CLITests/TestCLIHelp.swift b/Tests/CLITests/TestCLIHelp.swift deleted file mode 100644 index de8daed32..000000000 --- a/Tests/CLITests/TestCLIHelp.swift +++ /dev/null @@ -1,40 +0,0 @@ -//===----------------------------------------------------------------------===// -// 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 Foundation -import Testing - -@Suite(.serialSuites) -class TestCLIHelp: CLITest { - @Test func testHelp() throws { - let (_, output, error, status) = try run(arguments: ["help"]) - #expect(status == 0, "help should succeed, stderr: \(error)") - - #expect( - output.contains("OVERVIEW: A container platform for macOS"), - "output should contain overview section" - ) - } - @Test func testDebugHelp() throws { - let (_, output, error, status) = try run(arguments: ["--debug", "help"]) - #expect(status == 0, "help should succeed, stderr: \(error)") - - #expect( - output.contains("OVERVIEW: A container platform for macOS"), - "output should contain overview section" - ) - } -} diff --git a/Tests/IntegrationTests/System/TestCLIHelp.swift b/Tests/IntegrationTests/System/TestCLIHelp.swift new file mode 100644 index 000000000..c91f6bc61 --- /dev/null +++ b/Tests/IntegrationTests/System/TestCLIHelp.swift @@ -0,0 +1,42 @@ +//===----------------------------------------------------------------------===// +// 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 Testing + +@Suite +struct TestCLIHelp { + @Test func testHelp() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["help"]) + #expect(result.status == 0, "help should succeed, stderr: \(result.error)") + #expect( + result.output.contains("OVERVIEW: A container platform for macOS"), + "output should contain overview section" + ) + } + } + + @Test func testDebugHelp() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["--debug", "help"]) + #expect(result.status == 0, "help should succeed, stderr: \(result.error)") + #expect( + result.output.contains("OVERVIEW: A container platform for macOS"), + "output should contain overview section" + ) + } + } +} diff --git a/Tests/IntegrationTests/System/TestCLIStatus.swift b/Tests/IntegrationTests/System/TestCLIStatus.swift new file mode 100644 index 000000000..36041810b --- /dev/null +++ b/Tests/IntegrationTests/System/TestCLIStatus.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// 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 Foundation +import Testing + +/// Tests for `container system status` output formats and content validation. +@Suite +struct TestCLIStatus { + struct StatusJSON: Codable { + let status: String + let appRoot: String + let installRoot: String + let logRoot: String? + let apiServerVersion: String + let apiServerCommit: String + let apiServerBuild: String + let apiServerAppName: String + } + + @Test func defaultDisplaysTable() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "status"]) + guard result.status == 0 else { return } + + #expect(!result.output.isEmpty) + + let lines = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + #expect(lines.count >= 2) + #expect(lines[0].contains("FIELD") && lines[0].contains("VALUE")) + + let fullOutput = lines.joined(separator: "\n") + #expect(fullOutput.contains("status")) + #expect(fullOutput.contains("running")) + #expect(fullOutput.contains("appRoot")) + #expect(fullOutput.contains("installRoot")) + #expect(fullOutput.contains("apiserver.version")) + #expect(fullOutput.contains("apiserver.commit")) + } + } + + @Test func jsonFormat() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "status", "--format", "json"]) + + if result.status != 0 { + #expect(!result.output.isEmpty) + let decoded = try JSONDecoder().decode(StatusJSON.self, from: result.outputData) + #expect(decoded.status == "not running" || decoded.status == "unregistered") + return + } + + #expect(!result.output.isEmpty) + let decoded = try JSONDecoder().decode(StatusJSON.self, from: result.outputData) + #expect(decoded.status == "running") + #expect(!decoded.appRoot.isEmpty) + #expect(!decoded.installRoot.isEmpty) + #expect(!decoded.apiServerVersion.isEmpty) + #expect(!decoded.apiServerCommit.isEmpty) + #expect(!decoded.apiServerBuild.isEmpty) + #expect(!decoded.apiServerAppName.isEmpty) + } + } + + @Test func explicitTableFormat() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "status", "--format", "table"]) + guard result.status == 0 else { return } + + #expect(!result.output.isEmpty) + + let lines = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + #expect(lines.count >= 2) + #expect(lines[0].contains("FIELD") && lines[0].contains("VALUE")) + #expect(lines.joined(separator: "\n").contains("running")) + } + } + + @Test func statusFieldsMatch() async throws { + try await ContainerFixture.with { f in + let jsonResult = try f.run(["system", "status", "--format", "json"]) + let tableResult = try f.run(["system", "status", "--format", "table"]) + #expect(jsonResult.status == tableResult.status) + + guard jsonResult.status == 0 else { return } + + let decoded = try JSONDecoder().decode(StatusJSON.self, from: jsonResult.outputData) + #expect(tableResult.output.contains(decoded.status)) + #expect(tableResult.output.contains(decoded.appRoot)) + #expect(tableResult.output.contains(decoded.installRoot)) + #expect(tableResult.output.contains(decoded.apiServerVersion)) + #expect(tableResult.output.contains(decoded.apiServerCommit)) + #expect(tableResult.output.contains(decoded.apiServerBuild)) + #expect(tableResult.output.contains(decoded.apiServerAppName)) + } + } + + @Test func jsonOutputValidStructure() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "status", "--format", "json"]) + let decoded = try JSONDecoder().decode(StatusJSON.self, from: result.outputData) + if result.status == 0 { + #expect(decoded.status == "running") + #expect(!decoded.appRoot.isEmpty) + #expect(!decoded.installRoot.isEmpty) + #expect(!decoded.apiServerVersion.isEmpty) + } else { + #expect(decoded.status == "not running" || decoded.status == "unregistered") + } + } + } + + @Test func prefixOption() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "status", "--prefix", "com.apple.container."]) + guard result.status == 0 else { return } + + #expect(!result.output.isEmpty) + let lines = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + #expect(lines.count >= 2) + } + } +} diff --git a/Tests/IntegrationTests/System/TestCLIVersion.swift b/Tests/IntegrationTests/System/TestCLIVersion.swift new file mode 100644 index 000000000..4c0d7bda9 --- /dev/null +++ b/Tests/IntegrationTests/System/TestCLIVersion.swift @@ -0,0 +1,121 @@ +//===----------------------------------------------------------------------===// +// 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 Foundation +import Testing +import Yams + +/// Tests for `container system version` output formats and build type detection. +@Suite +struct TestCLIVersion { + struct VersionInfo: Codable { + let version: String + let buildType: String + let commit: String + let appName: String + } + + struct VersionOutput: Codable { + let version: String + let buildType: String + let commit: String + let appName: String + let server: VersionInfo? + } + + private func expectedBuildType() -> String { + #if DEBUG + return "debug" + #else + return "release" + #endif + } + + @Test func defaultDisplaysTable() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "version"]) + #expect(result.status == 0, "system version should succeed, stderr: \(result.error)") + #expect(!result.output.isEmpty) + + let lines = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + #expect(lines.count >= 2) + #expect( + lines[0].contains("COMPONENT") && lines[0].contains("VERSION") + && lines[0].contains("BUILD") && lines[0].contains("COMMIT")) + #expect(lines[1].hasPrefix("container")) + + let expected = expectedBuildType() + #expect(lines.joined(separator: "\n").contains(" \(expected) ")) + } + } + + @Test func jsonFormat() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "version", "--format", "json"]) + #expect(result.status == 0, "system version --format json should succeed, stderr: \(result.error)") + #expect(!result.output.isEmpty) + + let decoded = try JSONDecoder().decode([VersionOutput].self, from: result.outputData) + #expect(decoded[0].appName == "container") + #expect(!decoded[0].version.isEmpty) + #expect(!decoded[0].commit.isEmpty) + #expect(decoded[0].buildType == expectedBuildType()) + } + } + + @Test func yamlFormat() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "version", "--format", "yaml"]) + #expect(result.status == 0, "system version --format yaml should succeed, stderr: \(result.error)") + #expect(!result.output.isEmpty) + + let decoded = try YAMLDecoder().decode([VersionOutput].self, from: result.outputData) + #expect(decoded[0].appName == "container") + #expect(!decoded[0].version.isEmpty) + #expect(!decoded[0].commit.isEmpty) + #expect(decoded[0].buildType == expectedBuildType()) + } + } + + @Test func explicitTableFormat() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "version", "--format", "table"]) + #expect(result.status == 0, "system version --format table should succeed, stderr: \(result.error)") + #expect(!result.output.isEmpty) + + let lines = result.output.trimmingCharacters(in: .whitespacesAndNewlines) + .components(separatedBy: .newlines) + #expect(lines.count >= 2) + #expect( + lines[0].contains("COMPONENT") && lines[0].contains("VERSION") + && lines[0].contains("BUILD") && lines[0].contains("COMMIT")) + } + } + + @Test func buildTypeMatchesBinary() async throws { + try await ContainerFixture.with { f in + let result = try f.run(["system", "version", "--format", "json"]) + #expect(result.status == 0, "version --format json should succeed, stderr: \(result.error)") + + let decoded = try JSONDecoder().decode([VersionOutput].self, from: result.outputData) + let expected = expectedBuildType() + #expect( + decoded[0].buildType == expected, + "Expected build type \(expected) but got \(decoded[0].buildType)") + } + } +}