From ad1c0c84c952c7d7a4aa37477dae4f308d2a7906 Mon Sep 17 00:00:00 2001 From: Chris George Date: Fri, 15 May 2026 14:29:29 -0700 Subject: [PATCH] test(ContainerResource): unit coverage for RestartPolicy + ContainerCreateOptions Codable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestCLIRunRestart already covers end-to-end behavior against a live daemon. These add a complementary unit-level layer over the wire shape of 'RestartPolicy' and the forward-compatibility guarantees of 'ContainerCreateOptions': - encodesAsBareString — '.always' -> "always" (catches accidental enum-with-associated-value encoding regressions). - decodesEveryCase — round-trip safety for { no, onFailure, always }. - rejectsUnknownString — Compose-spec mode 'unless-stopped' is intentionally NOT in this PR's enum. Test pins that decision: any downstream tool that emits the broader Docker set fails loudly rather than silently degrading. - decodesLegacyOptionsWithoutRestartPolicy — the wire-compat invariant. An 'options.json' blob written by a daemon that predates 'restartPolicy' must still decode, defaulting to '.no'. This locks in the 'decodeIfPresent ?? .no' contract in ContainerCreateOptions.init(from:) — removing it would silently break every existing container on disk. - decodesLegacyOptionsWithAutoRemove — same invariant with a non-zero pre-existing field present, ensuring the default doesn't get triggered by unrelated keys. - roundTripPreservesRestartPolicy — basic options-level round-trip. - encodedJSONIncludesRestartPolicyField — confirms the field name matches the CodingKeys declaration (catches typo regressions in the key enum). - defaultStaticIsNoRestart — pins '.default' to '.no'. No production code touched. Verification: swift build -> Build complete! (69.11s), exit 0. swift test --filter RestartPolicyTests -> 8/8 tests passed in 0.001s. Refs #1258. --- .../RestartPolicyTests.swift | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Tests/ContainerResourceTests/RestartPolicyTests.swift diff --git a/Tests/ContainerResourceTests/RestartPolicyTests.swift b/Tests/ContainerResourceTests/RestartPolicyTests.swift new file mode 100644 index 000000000..6c8916191 --- /dev/null +++ b/Tests/ContainerResourceTests/RestartPolicyTests.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// 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 + +@testable import ContainerResource + +/// Unit-level coverage for the wire shape of ``RestartPolicy`` and the +/// forward-compatibility guarantees of ``ContainerCreateOptions``. +/// +/// `TestCLIRunRestart` already covers end-to-end behavior against a live +/// daemon. These tests cover the contract one layer down — JSON shape and +/// legacy-blob compatibility — so a future contributor cannot regress the +/// `decodeIfPresent` default without a unit-level red flag. +struct RestartPolicyTests { + // MARK: - RestartPolicy round-trip + + @Test + func encodesAsBareString() throws { + let data = try JSONEncoder().encode(RestartPolicy.always) + let s = String(decoding: data, as: UTF8.self) + #expect(s == "\"always\"") + } + + @Test + func decodesEveryCase() throws { + let cases: [RestartPolicy] = [.no, .onFailure, .always] + for policy in cases { + let data = try JSONEncoder().encode(policy) + let decoded = try JSONDecoder().decode(RestartPolicy.self, from: data) + #expect(decoded == policy) + } + } + + @Test + func rejectsUnknownString() { + // Compose-spec includes `unless-stopped`; this PR deliberately does + // not. The decoder must reject it so downstream tooling that emits + // a fuller mode set fails loudly instead of silently degrading. + let bogus = Data("\"unless-stopped\"".utf8) + #expect(throws: DecodingError.self) { + try JSONDecoder().decode(RestartPolicy.self, from: bogus) + } + } + + // MARK: - ContainerCreateOptions forward-compat + + /// `options.json` blobs written by daemon versions that predate + /// `restartPolicy` MUST still decode — defaulting to `.no`. This is the + /// wire-compatibility invariant the PR description promises and the + /// reason ``ContainerCreateOptions/init(from:)`` uses + /// `decodeIfPresent`. Removing that default would silently break every + /// existing container's `options.json` on disk. + @Test + func decodesLegacyOptionsWithoutRestartPolicy() throws { + let legacy = Data(#"{"autoRemove":false}"#.utf8) + let options = try JSONDecoder().decode(ContainerCreateOptions.self, from: legacy) + #expect(options.autoRemove == false) + #expect(options.restartPolicy == .no) + } + + @Test + func decodesLegacyOptionsWithAutoRemove() throws { + let legacy = Data(#"{"autoRemove":true}"#.utf8) + let options = try JSONDecoder().decode(ContainerCreateOptions.self, from: legacy) + #expect(options.autoRemove == true) + #expect(options.restartPolicy == .no) + } + + @Test + func roundTripPreservesRestartPolicy() throws { + let original = ContainerCreateOptions(autoRemove: false, restartPolicy: .onFailure) + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(ContainerCreateOptions.self, from: data) + #expect(decoded.autoRemove == original.autoRemove) + #expect(decoded.restartPolicy == original.restartPolicy) + } + + @Test + func encodedJSONIncludesRestartPolicyField() throws { + let options = ContainerCreateOptions(autoRemove: false, restartPolicy: .always) + let data = try JSONEncoder().encode(options) + let json = String(decoding: data, as: UTF8.self) + #expect(json.contains("\"restartPolicy\":\"always\"")) + } + + @Test + func defaultStaticIsNoRestart() { + #expect(ContainerCreateOptions.default.restartPolicy == .no) + #expect(ContainerCreateOptions.default.autoRemove == false) + } +}