mirror of
https://github.com/apple/container.git
synced 2026-10-02 12:27:52 +00:00
- Discussion topic #1336. - This change migrates away from using `UserDefaults`, instead providing a TOML configuration mechanism for user configurable settings. All existing system property settings keys are supported in the new configuration file. However, users will have to migrate any settings they have configured in the `UserDefaults` into TOML for these settings to take effect. - Breaking changes: * `container system property get` is removed in favor of users directly utilizing `container system property list --format toml | jq<>`. * `container system property set` is removed since the TOML configuration is effectively immutable during the lifetime of the `container` daemon. Uses can edit the TOML they have in their home directory, however no changes will take effect until the daemon is restarted via `container system stop && container system start` * `container system property list --format table` is removed as generating tabular format is non-trivial and the new TOML format is intended to be human readable
1299 lines
44 KiB
Swift
1299 lines
44 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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 ContainerPersistence
|
|
import ContainerizationError
|
|
import ContainerizationExtras
|
|
import Foundation
|
|
import Testing
|
|
|
|
@testable import ContainerAPIClient
|
|
|
|
struct ParserTest {
|
|
@Test
|
|
func testPublishPortParserTcp() throws {
|
|
let result = try Parser.publishPorts(["127.0.0.1:8080:8000/tcp"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("127.0.0.1")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(8000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortParserUdp() throws {
|
|
let result = try Parser.publishPorts(["192.168.32.36:8000:8080/UDP"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("192.168.32.36")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8000))
|
|
#expect(result[0].containerPort == UInt16(8080))
|
|
#expect(result[0].proto == .udp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRange() throws {
|
|
let result = try Parser.publishPorts(["127.0.0.1:8080-8179:9000-9099/tcp"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("127.0.0.1")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(9000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 100)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeSingle() throws {
|
|
let result = try Parser.publishPorts(["127.0.0.1:8080-8080:9000-9000/tcp"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("127.0.0.1")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(9000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortNoHostAddress() throws {
|
|
let result = try Parser.publishPorts(["8080:8000/tcp"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("0.0.0.0")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(8000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortNoProtocol() throws {
|
|
let result = try Parser.publishPorts(["8080:8000"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("0.0.0.0")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(8000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortParserIPv6() throws {
|
|
let result = try Parser.publishPorts(["[fe80::36f3:5e50:ed71:1bb]:8080:8000/tcp"])
|
|
#expect(result.count == 1)
|
|
let expectedAddress = try IPAddress("fe80::36f3:5e50:ed71:1bb")
|
|
#expect(result[0].hostAddress == expectedAddress)
|
|
#expect(result[0].hostPort == UInt16(8080))
|
|
#expect(result[0].containerPort == UInt16(8000))
|
|
#expect(result[0].proto == .tcp)
|
|
#expect(result[0].count == 1)
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortInvalidProtocol() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8080:8000/sctp"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish protocol")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortInvalidValue() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts([""])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish value")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortMissingPort() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["1234"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish value")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishInvalidIPv4Address() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["1234:8080:8000"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish IPv4 address")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishInvalidIPv6Address() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts([
|
|
"[1234:5678]:8080:8000",
|
|
"[2001::db8::1]:8080:8080",
|
|
"[2001:db8:85a3::8a2e:370g:7334]:8080:8080",
|
|
"[2001:db8:85a3::][8a2e::7334]:8080:8080",
|
|
])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish IPv6 address")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortInvalidHostPort() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["65536:1234"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortInvalidContainerPort() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["1234:65536"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeMismatch() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8000:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("counts are not equal")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidHostPortStart() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["65536-65537:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeZeroHostPortStart() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["0-1:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidHostPortEnd() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["65535-65536:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidHostPortRange() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001-8002:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeNegativeHostPortRange() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8001-8000:9000-9001"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish host port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidContainerPortStart() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001:65536-65537"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeZeroContainerPortStart() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001:0-1"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidContainerPortEnd() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001:65535-65536"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeInvalidContainerPortRange() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001:9000-9001-9002"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testPublishPortRangeNegativeContainerPortRange() throws {
|
|
#expect {
|
|
_ = try Parser.publishPorts(["8000-8001:9001-9000"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid publish container port")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testRelativePaths() throws {
|
|
// Test bind mount with relative path "."
|
|
do {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.mount("type=bind,src=.,dst=/foo", relativeTo: tempDir)
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
#expect(fs.source == tempDir.standardizedFileURL.path)
|
|
#expect(fs.destination == "/foo")
|
|
#expect(!fs.isVolume)
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
|
|
// Test volume with relative path "./"
|
|
do {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-rel-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.volume("./:/foo", relativeTo: tempDir)
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
let expectedPath = tempDir.standardizedFileURL.path
|
|
// Normalize trailing slashes for comparison
|
|
#expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
|
#expect(fs.destination == "/foo")
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
|
|
// Test volume with nested relative path "./subdir"
|
|
do {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-rel-nested-\(UUID().uuidString)")
|
|
let nestedDir = tempDir.appendingPathComponent("subdir")
|
|
try FileManager.default.createDirectory(at: nestedDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.volume("./subdir:/foo", relativeTo: tempDir)
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
let expectedPath = nestedDir.standardizedFileURL.path
|
|
// Normalize trailing slashes for comparison
|
|
#expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
|
#expect(fs.destination == "/foo")
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
|
|
// Test volume with bare "." as source (current directory)
|
|
do {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-dot-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.volume(".:/docs:ro", relativeTo: tempDir)
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
let expectedPath = tempDir.standardizedFileURL.path
|
|
#expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
|
#expect(fs.destination == "/docs")
|
|
#expect(fs.options.contains("ro"))
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
|
|
// Test volume with ".." as source (parent directory)
|
|
do {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-volume-dotdot-\(UUID().uuidString)")
|
|
let childDir = tempDir.appendingPathComponent("child")
|
|
try FileManager.default.createDirectory(at: childDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.volume("..:/data", relativeTo: childDir)
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
let expectedPath = tempDir.standardizedFileURL.path
|
|
#expect(fs.source.trimmingCharacters(in: CharacterSet(charactersIn: "/")) == expectedPath.trimmingCharacters(in: CharacterSet(charactersIn: "/")))
|
|
#expect(fs.destination == "/data")
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testMountBindAbsolutePath() throws {
|
|
let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent("test-bind-abs-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempDir)
|
|
}
|
|
|
|
let result = try Parser.mount("type=bind,src=\(tempDir.path),dst=/foo")
|
|
|
|
switch result {
|
|
case .filesystem(let fs):
|
|
#expect(fs.source == tempDir.path)
|
|
#expect(fs.destination == "/foo")
|
|
#expect(!fs.isVolume)
|
|
case .volume:
|
|
#expect(Bool(false), "Expected filesystem mount, got volume")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testMountVolumeValidName() throws {
|
|
let result = try Parser.mount("type=volume,src=myvolume,dst=/data")
|
|
|
|
switch result {
|
|
case .filesystem:
|
|
#expect(Bool(false), "Expected volume mount, got filesystem")
|
|
case .volume(let vol):
|
|
#expect(vol.name == "myvolume")
|
|
#expect(vol.destination == "/data")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testMountVolumeInvalidName() throws {
|
|
#expect {
|
|
_ = try Parser.mount("type=volume,src=.,dst=/data")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid volume name")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testMountBindNonExistentPath() throws {
|
|
#expect {
|
|
_ = try Parser.mount("type=bind,src=/nonexistent/path,dst=/foo")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("path") && error.description.contains("does not exist")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testMountBindFileInsteadOfDirectory() throws {
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-file-\(UUID().uuidString)")
|
|
try "test content".write(to: tempFile, atomically: true, encoding: .utf8)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: tempFile)
|
|
}
|
|
|
|
#expect {
|
|
_ = try Parser.mount("type=bind,src=\(tempFile.path),dst=/foo")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("path") && error.description.contains("is not a directory")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testIsValidDomainNameOk() throws {
|
|
let names = [
|
|
"a",
|
|
"a.b",
|
|
"foo.bar",
|
|
"F-O.B-R",
|
|
[
|
|
String(repeating: "0", count: 63),
|
|
String(repeating: "1", count: 63),
|
|
String(repeating: "2", count: 63),
|
|
String(repeating: "3", count: 63),
|
|
].joined(separator: "."),
|
|
]
|
|
for name in names {
|
|
#expect(Parser.isValidDomainName(name))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testIsValidDomainNameBad() throws {
|
|
let names = [
|
|
".foo",
|
|
"foo.",
|
|
".foo.bar",
|
|
"foo.bar.",
|
|
"-foo.bar",
|
|
"foo.bar-",
|
|
[
|
|
String(repeating: "0", count: 63),
|
|
String(repeating: "1", count: 63),
|
|
String(repeating: "2", count: 63),
|
|
String(repeating: "3", count: 62),
|
|
"4",
|
|
].joined(separator: "."),
|
|
]
|
|
for name in names {
|
|
#expect(!Parser.isValidDomainName(name))
|
|
}
|
|
}
|
|
|
|
// MARK: - Environment Variable Tests
|
|
|
|
@Test
|
|
func testEnvExplicitValue() throws {
|
|
let result = Parser.env(envList: ["FOO=bar", "BAZ=qux"])
|
|
#expect(result == ["FOO=bar", "BAZ=qux"])
|
|
}
|
|
|
|
@Test
|
|
func testEnvImplicitInheritance() throws {
|
|
guard let homeValue = ProcessInfo.processInfo.environment["PATH"] else {
|
|
Issue.record("PATH environment variable not set")
|
|
return
|
|
}
|
|
|
|
let result = Parser.env(envList: ["PATH"])
|
|
#expect(result == ["PATH=\(homeValue)"])
|
|
}
|
|
|
|
@Test
|
|
func testEnvImplicitUndefinedVariable() throws {
|
|
// A variable that doesn't exist should be silently skipped
|
|
let result = Parser.env(envList: ["THIS_VAR_DEFINITELY_DOES_NOT_EXIST_12345"])
|
|
#expect(result.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func testEnvMixedExplicitAndImplicit() throws {
|
|
guard let homeValue = ProcessInfo.processInfo.environment["HOME"] else {
|
|
Issue.record("HOME environment variable not set")
|
|
return
|
|
}
|
|
|
|
let result = Parser.env(envList: ["FOO=bar", "HOME", "BAZ=qux"])
|
|
#expect(result == ["FOO=bar", "HOME=\(homeValue)", "BAZ=qux"])
|
|
}
|
|
|
|
@Test
|
|
func testEnvEmptyValue() throws {
|
|
// Explicit empty value should be preserved
|
|
let result = Parser.env(envList: ["EMPTY="])
|
|
#expect(result == ["EMPTY="])
|
|
}
|
|
|
|
@Test
|
|
func testAllEnvUserOverridesImage() throws {
|
|
let result = try Parser.allEnv(
|
|
imageEnvs: ["FOO=fromimage", "BAR=kept"],
|
|
envFiles: [],
|
|
envs: ["FOO=fromuser"]
|
|
)
|
|
#expect(Set(result) == Set(["FOO=fromuser", "BAR=kept"]))
|
|
}
|
|
|
|
@Test
|
|
func testAllEnvFileOverridesImage() throws {
|
|
let tmpFile = try tmpFileWithContent("FOO=fromfile\n")
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let result = try Parser.allEnv(
|
|
imageEnvs: ["FOO=fromimage", "BAR=kept"],
|
|
envFiles: [tmpFile.path],
|
|
envs: []
|
|
)
|
|
#expect(Set(result) == Set(["FOO=fromfile", "BAR=kept"]))
|
|
}
|
|
|
|
@Test
|
|
func testAllEnvUserOverridesFileOverridesImage() throws {
|
|
let tmpFile = try tmpFileWithContent("FOO=fromfile\nBAZ=fromfile\n")
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let result = try Parser.allEnv(
|
|
imageEnvs: ["FOO=fromimage", "BAR=fromimage"],
|
|
envFiles: [tmpFile.path],
|
|
envs: ["FOO=fromuser"]
|
|
)
|
|
#expect(Set(result) == Set(["FOO=fromuser", "BAR=fromimage", "BAZ=fromfile"]))
|
|
}
|
|
|
|
private func tmpFileWithContent(_ content: String) throws -> URL {
|
|
let tempDir = FileManager.default.temporaryDirectory
|
|
let tempFile = tempDir.appendingPathComponent("envfile-test-\(UUID().uuidString)")
|
|
try content.write(to: tempFile, atomically: true, encoding: .utf8)
|
|
return tempFile
|
|
}
|
|
|
|
// NOTE: A lot of these env-file tests are recreations of the docker cli's unit tests for their
|
|
// env-file support.
|
|
|
|
@Test
|
|
func testParseEnvFileGoodFile() throws {
|
|
var content = """
|
|
foo=bar
|
|
baz=quux
|
|
# comment
|
|
|
|
_foobar=foobaz
|
|
with.dots=working
|
|
and_underscore=working too
|
|
"""
|
|
content += "\n \t "
|
|
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let lines = try Parser.envFile(path: tmpFile.path)
|
|
|
|
let expectedLines = [
|
|
"foo=bar",
|
|
"baz=quux",
|
|
"_foobar=foobaz",
|
|
"with.dots=working",
|
|
"and_underscore=working too",
|
|
]
|
|
|
|
#expect(lines == expectedLines)
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileMultipleEqualsSigns() throws {
|
|
let content = """
|
|
URL=https://foo.bar?baz=woo
|
|
"""
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let lines = try Parser.envFile(path: tmpFile.path)
|
|
|
|
let expectedLines = [
|
|
"URL=https://foo.bar?baz=woo"
|
|
]
|
|
|
|
#expect(lines == expectedLines)
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileEmptyFile() throws {
|
|
let tmpFile = try tmpFileWithContent("")
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let lines = try Parser.envFile(path: tmpFile.path)
|
|
#expect(lines.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileNonExistentFile() throws {
|
|
#expect {
|
|
_ = try Parser.envFile(path: "/nonexistent/foo_bar_baz")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError,
|
|
let cause = error.cause
|
|
else {
|
|
return false
|
|
}
|
|
return String(describing: cause).contains("No such file or directory")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileBadlyFormattedFile() throws {
|
|
let content = """
|
|
foo=bar
|
|
f =quux
|
|
"""
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
#expect {
|
|
_ = try Parser.envFile(path: tmpFile.path)
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("contains whitespaces")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileRandomFile() throws {
|
|
let content = """
|
|
first line
|
|
another invalid line
|
|
"""
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
#expect {
|
|
_ = try Parser.envFile(path: tmpFile.path)
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("first line") && error.description.contains("contains whitespaces")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvVariableDefinitionsFile() throws {
|
|
let content = """
|
|
# comment=
|
|
UNDEFINED_VAR
|
|
HOME
|
|
"""
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
let variables = try Parser.envFile(path: tmpFile.path)
|
|
|
|
// HOME should be imported from environment
|
|
guard let homeValue = ProcessInfo.processInfo.environment["HOME"] else {
|
|
Issue.record("HOME environment variable not set")
|
|
return
|
|
}
|
|
|
|
#expect(variables.count == 1)
|
|
#expect(variables[0] == "HOME=\(homeValue)")
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvVariableWithNoNameFile() throws {
|
|
let content = """
|
|
# comment=
|
|
=blank variable names are an error case
|
|
"""
|
|
let tmpFile = try tmpFileWithContent(content)
|
|
defer { try? FileManager.default.removeItem(at: tmpFile) }
|
|
|
|
#expect {
|
|
_ = try Parser.envFile(path: tmpFile.path)
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("no variable name")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseEnvFileFromNamedPipe() throws {
|
|
let pipePath = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("envfile-pipe-\(UUID().uuidString)")
|
|
|
|
// Create a named pipe (FIFO)
|
|
let result = mkfifo(pipePath.path, 0o600)
|
|
guard result == 0 else {
|
|
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EPERM)
|
|
}
|
|
defer { try? FileManager.default.removeItem(at: pipePath) }
|
|
|
|
let group = DispatchGroup()
|
|
|
|
group.enter()
|
|
DispatchQueue.global().async {
|
|
do {
|
|
let handle = try FileHandle(forWritingTo: pipePath)
|
|
try handle.write(contentsOf: "SECRET_KEY=value123\n".data(using: .utf8)!)
|
|
try handle.close()
|
|
} catch {
|
|
Issue.record(error)
|
|
}
|
|
group.leave()
|
|
}
|
|
|
|
// Read from pipe (blocks until writer connects)
|
|
let lines = try Parser.envFile(path: pipePath.path)
|
|
|
|
// Wait for write to complete
|
|
group.wait()
|
|
|
|
#expect(lines == ["SECRET_KEY=value123"])
|
|
}
|
|
|
|
// MARK: Network Parser Tests
|
|
|
|
@Test
|
|
func testParseNetworkSimpleName() throws {
|
|
let result = try Parser.network("default")
|
|
#expect(result.name == "default")
|
|
#expect(result.macAddress == nil)
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkWithMACAddress() throws {
|
|
let result = try Parser.network("backend,mac=02:42:ac:11:00:02")
|
|
#expect(result.name == "backend")
|
|
#expect(result.macAddress == "02:42:ac:11:00:02")
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkWithMACAddressHyphenSeparator() throws {
|
|
let result = try Parser.network("backend,mac=02-42-ac-11-00-02")
|
|
#expect(result.name == "backend")
|
|
#expect(result.macAddress == "02-42-ac-11-00-02")
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkEmptyString() throws {
|
|
#expect {
|
|
_ = try Parser.network("")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("network specification cannot be empty")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkEmptyName() throws {
|
|
#expect {
|
|
_ = try Parser.network(",mac=02:42:ac:11:00:02")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("network name cannot be empty")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkEmptyMACAddress() throws {
|
|
#expect {
|
|
_ = try Parser.network("backend,mac=")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("mac address value cannot be empty")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkUnknownProperty() throws {
|
|
#expect {
|
|
_ = try Parser.network("backend,unknown=value")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("unknown network property") && error.description.contains("unknown")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testParseNetworkInvalidPropertyFormat() throws {
|
|
#expect {
|
|
_ = try Parser.network("backend,invalidproperty")
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid property format")
|
|
}
|
|
}
|
|
|
|
// MARK: - Relative Path Passthrough Tests
|
|
|
|
@Test
|
|
func testProcessEntrypointRelativePathPassthrough() throws {
|
|
let processFlags = try Flags.Process.parse(["--cwd", "/bin"])
|
|
let managementFlags = try Flags.Management.parse(["--entrypoint", "./uname"])
|
|
|
|
let result = try Parser.process(
|
|
arguments: [],
|
|
processFlags: processFlags,
|
|
managementFlags: managementFlags,
|
|
config: nil
|
|
)
|
|
|
|
#expect(result.executable == "./uname")
|
|
#expect(result.workingDirectory == "/bin")
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserSoftAndHard() throws {
|
|
let result = try Parser.rlimits(["nofile=1024:2048"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_NOFILE")
|
|
#expect(result[0].soft == 1024)
|
|
#expect(result[0].hard == 2048)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserSingleValue() throws {
|
|
let result = try Parser.rlimits(["nproc=512"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_NPROC")
|
|
#expect(result[0].soft == 512)
|
|
#expect(result[0].hard == 512)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserUnlimited() throws {
|
|
let result = try Parser.rlimits(["memlock=unlimited"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_MEMLOCK")
|
|
#expect(result[0].soft == UInt64.max)
|
|
#expect(result[0].hard == UInt64.max)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserUnlimitedHardOnly() throws {
|
|
let result = try Parser.rlimits(["stack=8192:unlimited"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_STACK")
|
|
#expect(result[0].soft == 8192)
|
|
#expect(result[0].hard == UInt64.max)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserMinusOneAsUnlimited() throws {
|
|
let result = try Parser.rlimits(["core=-1"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_CORE")
|
|
#expect(result[0].soft == UInt64.max)
|
|
#expect(result[0].hard == UInt64.max)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserMultipleUlimits() throws {
|
|
let result = try Parser.rlimits(["nofile=1024:2048", "nproc=256", "cpu=60:120"])
|
|
#expect(result.count == 3)
|
|
#expect(result[0].limit == "RLIMIT_NOFILE")
|
|
#expect(result[1].limit == "RLIMIT_NPROC")
|
|
#expect(result[2].limit == "RLIMIT_CPU")
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserAllSupportedTypes() throws {
|
|
let types = ["core", "cpu", "data", "fsize", "memlock", "nofile", "nproc", "rss", "stack"]
|
|
let expectedRlimits = [
|
|
"RLIMIT_CORE", "RLIMIT_CPU", "RLIMIT_DATA", "RLIMIT_FSIZE",
|
|
"RLIMIT_MEMLOCK", "RLIMIT_NOFILE", "RLIMIT_NPROC", "RLIMIT_RSS", "RLIMIT_STACK",
|
|
]
|
|
|
|
for (i, type) in types.enumerated() {
|
|
let result = try Parser.rlimits(["\(type)=100"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == expectedRlimits[i])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserCaseInsensitive() throws {
|
|
let result = try Parser.rlimits(["NOFILE=1024", "Nproc=512"])
|
|
#expect(result.count == 2)
|
|
#expect(result[0].limit == "RLIMIT_NOFILE")
|
|
#expect(result[1].limit == "RLIMIT_NPROC")
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserInvalidFormat() throws {
|
|
#expect {
|
|
_ = try Parser.rlimits(["nofile"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid ulimit format")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserUnsupportedType() throws {
|
|
#expect {
|
|
_ = try Parser.rlimits(["foo=100"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("unsupported ulimit type")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserSoftExceedsHard() throws {
|
|
#expect {
|
|
_ = try Parser.rlimits(["nofile=2048:1024"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("soft limit") && error.description.contains("cannot exceed hard limit")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserDuplicateType() throws {
|
|
#expect {
|
|
_ = try Parser.rlimits(["nofile=1024", "nofile=2048"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("duplicate ulimit type")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserInvalidValue() throws {
|
|
#expect {
|
|
_ = try Parser.rlimits(["nofile=abc"])
|
|
} throws: { error in
|
|
guard let error = error as? ContainerizationError else {
|
|
return false
|
|
}
|
|
return error.description.contains("invalid ulimit value")
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserEmptyArray() throws {
|
|
let result = try Parser.rlimits([])
|
|
#expect(result.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserZeroValue() throws {
|
|
let result = try Parser.rlimits(["core=0"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_CORE")
|
|
#expect(result[0].soft == 0)
|
|
#expect(result[0].hard == 0)
|
|
}
|
|
|
|
@Test
|
|
func testUlimitParserLargeValues() throws {
|
|
let result = try Parser.rlimits(["nproc=\(UInt64.max - 1):\(UInt64.max)"])
|
|
#expect(result.count == 1)
|
|
#expect(result[0].limit == "RLIMIT_NPROC")
|
|
#expect(result[0].soft == UInt64.max - 1)
|
|
#expect(result[0].hard == UInt64.max)
|
|
}
|
|
|
|
// MARK: - Capabilities Parser Tests
|
|
|
|
@Test
|
|
func testCapabilitiesParserEmpty() throws {
|
|
let result = try Parser.capabilities(capAdd: [], capDrop: [])
|
|
#expect(result.capAdd.isEmpty)
|
|
#expect(result.capDrop.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserAddSingle() throws {
|
|
let result = try Parser.capabilities(capAdd: ["CAP_NET_RAW"], capDrop: [])
|
|
#expect(result.capAdd == ["CAP_NET_RAW"])
|
|
#expect(result.capDrop.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserDropSingle() throws {
|
|
let result = try Parser.capabilities(capAdd: [], capDrop: ["CAP_MKNOD"])
|
|
#expect(result.capAdd.isEmpty)
|
|
#expect(result.capDrop == ["CAP_MKNOD"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserWithoutPrefix() throws {
|
|
let result = try Parser.capabilities(capAdd: ["NET_RAW"], capDrop: ["MKNOD"])
|
|
#expect(result.capAdd == ["CAP_NET_RAW"])
|
|
#expect(result.capDrop == ["CAP_MKNOD"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserCaseInsensitive() throws {
|
|
let result = try Parser.capabilities(capAdd: ["net_raw"], capDrop: ["mknod"])
|
|
#expect(result.capAdd == ["CAP_NET_RAW"])
|
|
#expect(result.capDrop == ["CAP_MKNOD"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserLowercaseWithPrefix() throws {
|
|
let result = try Parser.capabilities(capAdd: ["cap_net_raw"], capDrop: [])
|
|
#expect(result.capAdd == ["CAP_NET_RAW"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserALL() throws {
|
|
let result = try Parser.capabilities(capAdd: ["ALL"], capDrop: ["ALL"])
|
|
#expect(result.capAdd == ["ALL"])
|
|
#expect(result.capDrop == ["ALL"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserDropALLWithAdd() throws {
|
|
let result = try Parser.capabilities(capAdd: ["CAP_NET_RAW", "CAP_MKNOD"], capDrop: ["ALL"])
|
|
#expect(result.capAdd == ["CAP_NET_RAW", "CAP_MKNOD"])
|
|
#expect(result.capDrop == ["ALL"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserAddALLWithDrop() throws {
|
|
let result = try Parser.capabilities(capAdd: ["ALL"], capDrop: ["CAP_NET_ADMIN"])
|
|
#expect(result.capAdd == ["ALL"])
|
|
#expect(result.capDrop == ["CAP_NET_ADMIN"])
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserMultiple() throws {
|
|
let result = try Parser.capabilities(
|
|
capAdd: ["CAP_NET_RAW", "CAP_SYS_ADMIN"],
|
|
capDrop: ["CAP_MKNOD", "CAP_CHOWN"]
|
|
)
|
|
#expect(result.capAdd.count == 2)
|
|
#expect(result.capAdd.contains("CAP_NET_RAW"))
|
|
#expect(result.capAdd.contains("CAP_SYS_ADMIN"))
|
|
#expect(result.capDrop.count == 2)
|
|
#expect(result.capDrop.contains("CAP_MKNOD"))
|
|
#expect(result.capDrop.contains("CAP_CHOWN"))
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserInvalidAdd() throws {
|
|
#expect {
|
|
_ = try Parser.capabilities(capAdd: ["CHWOWZERS"], capDrop: [])
|
|
} throws: { _ in
|
|
true
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testCapabilitiesParserInvalidDrop() throws {
|
|
#expect {
|
|
_ = try Parser.capabilities(capAdd: [], capDrop: ["CHWOWZERS"])
|
|
} throws: { _ in
|
|
true
|
|
}
|
|
}
|
|
|
|
// MARK: - Parser.resources
|
|
|
|
@Test func testResourcesCustomDefaults() throws {
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: 2, defaultMemory: try MemorySize("2048MB")
|
|
)
|
|
#expect(result.cpus == 2)
|
|
#expect(result.memoryInBytes == 2048.mib())
|
|
}
|
|
|
|
@Test func testResourcesFlagOverridesDefaults() throws {
|
|
let result = try Parser.resources(cpus: 1, memory: "256m", defaultCPUs: 8, defaultMemory: MemorySize("2g"))
|
|
#expect(result.cpus == 1)
|
|
#expect(result.memoryInBytes == 256.mib())
|
|
}
|
|
|
|
@Test func testResourcesBuildPropertyLookup() throws {
|
|
let content = """
|
|
[build]
|
|
cpus = 8
|
|
memory = "4g"
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-build-lookup.toml")
|
|
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
|
defer { try? FileManager.default.removeItem(at: tempFile) }
|
|
|
|
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: config.build.cpus,
|
|
defaultMemory: config.build.memory
|
|
)
|
|
#expect(result.cpus == 8)
|
|
#expect(result.memoryInBytes == 4096.mib())
|
|
}
|
|
|
|
@Test func testResourcesCPUsFromProperty() throws {
|
|
let content = """
|
|
[container]
|
|
cpus = 8
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-cpus-property.toml")
|
|
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
|
defer { try? FileManager.default.removeItem(at: tempFile) }
|
|
|
|
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: config.container.cpus,
|
|
defaultMemory: config.container.memory
|
|
)
|
|
#expect(result.cpus == 8)
|
|
}
|
|
|
|
@Test func testResourcesMemoryFromProperty() throws {
|
|
let content = """
|
|
[container]
|
|
memory = "2g"
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-memory-property.toml")
|
|
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
|
defer { try? FileManager.default.removeItem(at: tempFile) }
|
|
|
|
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: config.container.cpus,
|
|
defaultMemory: config.container.memory
|
|
)
|
|
#expect(result.memoryInBytes == 2048.mib())
|
|
}
|
|
|
|
@Test func testResourcesFlagOverridesProperty() throws {
|
|
let content = """
|
|
[container]
|
|
cpus = 8
|
|
memory = "2g"
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-flag-overrides.toml")
|
|
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
|
defer { try? FileManager.default.removeItem(at: tempFile) }
|
|
|
|
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
|
let result = try Parser.resources(
|
|
cpus: 1, memory: "256m",
|
|
defaultCPUs: config.container.cpus,
|
|
defaultMemory: config.container.memory
|
|
)
|
|
#expect(result.cpus == 1)
|
|
#expect(result.memoryInBytes == 256.mib())
|
|
}
|
|
|
|
@Test func testResourcesPropertyKeysAreIsolated() throws {
|
|
let content = """
|
|
[container]
|
|
cpus = 16
|
|
memory = "8g"
|
|
"""
|
|
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent("test-keys-isolated.toml")
|
|
FileManager.default.createFile(atPath: tempFile.path(), contents: Data(content.utf8))
|
|
defer { try? FileManager.default.removeItem(at: tempFile) }
|
|
|
|
let config: ContainerSystemConfig = try SystemRuntimeOptions.loadConfig(configFile: tempFile)
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: config.build.cpus,
|
|
defaultMemory: config.build.memory
|
|
)
|
|
#expect(result.cpus == 2)
|
|
#expect(result.memoryInBytes == 2048.mib())
|
|
}
|
|
}
|