mirror of
https://github.com/apple/container.git
synced 2026-07-12 12:37:02 +00:00
0c95007763
- For result sets with known sizes, it's more efficient to supply the size as a capacity hint.
1397 lines
48 KiB
Swift
1397 lines
48 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 ContainerizationError
|
|
import ContainerizationExtras
|
|
import Foundation
|
|
import SystemPackage
|
|
import Testing
|
|
|
|
@testable import ContainerAPIClient
|
|
@testable import ContainerPersistence
|
|
|
|
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() async 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 await ConfigurationLoader.load(configurationFiles: [FilePath(tempFile.path(percentEncoded: false))])
|
|
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() async 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 await ConfigurationLoader.load(configurationFiles: [FilePath(tempFile.path(percentEncoded: false))])
|
|
let result = try Parser.resources(
|
|
cpus: nil, memory: nil,
|
|
defaultCPUs: config.container.cpus,
|
|
defaultMemory: config.container.memory
|
|
)
|
|
#expect(result.cpus == 8)
|
|
}
|
|
|
|
@Test func testResourcesMemoryFromProperty() async 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 await ConfigurationLoader.load(configurationFiles: [FilePath(tempFile.path(percentEncoded: false))])
|
|
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() async 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 await ConfigurationLoader.load(configurationFiles: [FilePath(tempFile.path(percentEncoded: false))])
|
|
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() async 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 await ConfigurationLoader.load(configurationFiles: [FilePath(tempFile.path(percentEncoded: false))])
|
|
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())
|
|
}
|
|
|
|
// MARK: - DNS Flag Validation Tests
|
|
|
|
@Test
|
|
func testManagementFlagsRejectsNoDNSWithDNS() throws {
|
|
#expect(throws: (any Error).self) {
|
|
_ = try Flags.Management.parse(["--dns", "1.1.1.1", "--no-dns"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testManagementFlagsRejectsNoDNSWithDNSDomain() throws {
|
|
#expect(throws: (any Error).self) {
|
|
_ = try Flags.Management.parse(["--dns-domain", "example.com", "--no-dns"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testManagementFlagsRejectsNoDNSWithDNSSearch() throws {
|
|
#expect(throws: (any Error).self) {
|
|
_ = try Flags.Management.parse(["--dns-search", "example.com", "--no-dns"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testManagementFlagsRejectsNoDNSWithDNSOption() throws {
|
|
#expect(throws: (any Error).self) {
|
|
_ = try Flags.Management.parse(["--dns-option", "debug", "--no-dns"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func testManagementFlagsAcceptsDNSAlone() throws {
|
|
_ = try Flags.Management.parse(["--dns", "1.1.1.1"])
|
|
}
|
|
|
|
@Test
|
|
func testManagementFlagsAcceptsNoDNSAlone() throws {
|
|
_ = try Flags.Management.parse(["--no-dns"])
|
|
}
|
|
|
|
// MARK: - Collection capacity hints
|
|
|
|
@Test("labels with large input preserves all entries")
|
|
func testLabelsLargeInput() throws {
|
|
let labels = (0..<100).map { "key\($0)=value\($0)" }
|
|
let result = try Parser.labels(labels)
|
|
#expect(result.count == 100)
|
|
#expect(result["key42"] == "value42")
|
|
#expect(result["key99"] == "value99")
|
|
}
|
|
|
|
@Test("resolve with large input preserves all entries")
|
|
func testParseKeyValuePairsLargeInput() {
|
|
let pairs = (0..<100).map { "key\($0)=value\($0)" }
|
|
let result = Utility.parseKeyValuePairs(pairs)
|
|
#expect(result.count == 100)
|
|
#expect(result["key0"] == "value0")
|
|
#expect(result["key99"] == "value99")
|
|
}
|
|
|
|
@Test("tmpfsMounts with large input")
|
|
func testTmpfsMountsLargeInput() throws {
|
|
let mounts = (0..<20).map { "/mnt/tmpfs\($0)" }
|
|
let result = try Parser.tmpfsMounts(mounts)
|
|
#expect(result.count == 20)
|
|
}
|
|
|
|
@Test("volumes with large input")
|
|
func testVolumesLargeInput() throws {
|
|
let volumes = (0..<20).map { "vol\($0):/mnt/vol\($0)" }
|
|
let result = try Parser.volumes(volumes)
|
|
#expect(result.count == 20)
|
|
}
|
|
|
|
@Test("capabilities with large input")
|
|
func testCapabilitiesLargeInput() throws {
|
|
let result = try Parser.capabilities(capAdd: ["ALL", "SYS_ADMIN", "NET_RAW", "CHOWN"], capDrop: ["SETUID", "KILL"])
|
|
#expect(result.capAdd.count == 4)
|
|
#expect(result.capDrop.count == 2)
|
|
#expect(result.capAdd.first == "ALL")
|
|
}
|
|
|
|
@Test("rlimits with large input")
|
|
func testRlimitsLargeInput() throws {
|
|
let result = try Parser.rlimits(["nofile=1024:2048", "nproc=100:200", "memlock=65536:65536"])
|
|
#expect(result.count == 3)
|
|
#expect(result[0].limit == "RLIMIT_NOFILE")
|
|
}
|
|
|
|
@Test("allEnv with large env lists")
|
|
func testAllEnvLargeInput() throws {
|
|
let imageEnvs = (0..<50).map { "IMAGE_VAR\($0)=value\($0)" }
|
|
let envs = (0..<50).map { "USER_VAR\($0)=value\($0)" }
|
|
let result = try Parser.allEnv(imageEnvs: imageEnvs, envFiles: [], envs: envs)
|
|
#expect(result.count == 100)
|
|
}
|
|
}
|