chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
private final class Unprotected: @unchecked Sendable {
|
||||
var value: Int
|
||||
init(_ value: Int) { self.value = value }
|
||||
}
|
||||
|
||||
final class AsyncLockTests {
|
||||
@Test
|
||||
func testBasicModification() async throws {
|
||||
let lock = AsyncLock()
|
||||
let counter = Unprotected(0)
|
||||
|
||||
let result = await lock.withLock { _ in
|
||||
counter.value += 1
|
||||
return counter.value
|
||||
}
|
||||
|
||||
#expect(result == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testSequentialReturnValues() async throws {
|
||||
let lock = AsyncLock()
|
||||
|
||||
let first = await lock.withLock { _ in 1 }
|
||||
let second = await lock.withLock { _ in first + 1 }
|
||||
let third = await lock.withLock { _ in second + 1 }
|
||||
|
||||
#expect(third == 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testMultipleModifications() async throws {
|
||||
let lock = AsyncLock()
|
||||
let counter = Unprotected(0)
|
||||
|
||||
await lock.withLock { _ in
|
||||
counter.value += 5
|
||||
}
|
||||
|
||||
let result = await lock.withLock { value in
|
||||
counter.value += 10
|
||||
return counter.value
|
||||
}
|
||||
|
||||
#expect(result == 15)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testMutualExclusion() async throws {
|
||||
let lock = AsyncLock()
|
||||
let counter = Unprotected(0)
|
||||
let iterations = 100
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for _ in 0..<iterations {
|
||||
group.addTask {
|
||||
await lock.withLock { _ in
|
||||
let current = counter.value
|
||||
try? await Task.sleep(for: .milliseconds(10))
|
||||
counter.value = current + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#expect(counter.value == iterations)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testThrowingClosure() async throws {
|
||||
let lock = AsyncLock()
|
||||
let counter = Unprotected(0)
|
||||
|
||||
await #expect(throws: POSIXError.self) {
|
||||
try await lock.withLock { _ in
|
||||
counter.value = 1
|
||||
throw POSIXError(.ENOENT)
|
||||
}
|
||||
}
|
||||
|
||||
// Value should still be modified even though closure threw
|
||||
#expect(counter.value == 1)
|
||||
|
||||
// Lock should still be usable even though the previous closure threw
|
||||
await lock.withLock { _ in
|
||||
counter.value = 2
|
||||
}
|
||||
|
||||
#expect(counter.value == 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
final class AsyncMutexTests {
|
||||
@Test
|
||||
func testBasicModification() async throws {
|
||||
let mutex = AsyncMutex(0)
|
||||
|
||||
let result = await mutex.withLock { value in
|
||||
value += 1
|
||||
return value
|
||||
}
|
||||
|
||||
#expect(result == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testMultipleModifications() async throws {
|
||||
let mutex = AsyncMutex(0)
|
||||
|
||||
await mutex.withLock { value in
|
||||
value += 5
|
||||
}
|
||||
|
||||
let result = await mutex.withLock { value in
|
||||
value += 10
|
||||
return value
|
||||
}
|
||||
|
||||
#expect(result == 15)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testConcurrentAccess() async throws {
|
||||
let mutex = AsyncMutex(0)
|
||||
let iterations = 100
|
||||
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
for _ in 0..<iterations {
|
||||
group.addTask {
|
||||
await mutex.withLock { value in
|
||||
value += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let finalValue = await mutex.withLock { value in
|
||||
value
|
||||
}
|
||||
|
||||
#expect(finalValue == iterations)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testAsyncOperationsUnderLock() async throws {
|
||||
let mutex = AsyncMutex([Int]())
|
||||
|
||||
await mutex.withLock { value in
|
||||
try? await Task.sleep(for: .milliseconds(10))
|
||||
value.append(1)
|
||||
}
|
||||
|
||||
await mutex.withLock { value in
|
||||
try? await Task.sleep(for: .milliseconds(10))
|
||||
value.append(2)
|
||||
}
|
||||
|
||||
let result = await mutex.withLock { value in
|
||||
value
|
||||
}
|
||||
|
||||
#expect(result == [1, 2])
|
||||
}
|
||||
|
||||
@Test
|
||||
func testThrowingClosure() async throws {
|
||||
let mutex = AsyncMutex(0)
|
||||
|
||||
await #expect(throws: POSIXError.self) {
|
||||
try await mutex.withLock { value in
|
||||
value += 1
|
||||
throw POSIXError(.ENOENT)
|
||||
}
|
||||
}
|
||||
|
||||
// Value should still be modified even though closure threw
|
||||
let result = await mutex.withLock { value in
|
||||
value
|
||||
}
|
||||
|
||||
#expect(result == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testComplexDataStructure() async throws {
|
||||
struct Counter: Sendable {
|
||||
var count: Int
|
||||
var label: String
|
||||
}
|
||||
|
||||
let mutex = AsyncMutex(Counter(count: 0, label: "test"))
|
||||
|
||||
await mutex.withLock { value in
|
||||
value.count += 10
|
||||
value.label = "modified"
|
||||
}
|
||||
|
||||
await mutex.withLock { value in
|
||||
#expect(value.count == 10)
|
||||
#expect(value.label == "modified")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
struct ProxyUtilsTests {
|
||||
|
||||
@Test("HTTP proxy resolution")
|
||||
func testHttpProxy() {
|
||||
let env = ["http_proxy": "http://proxy.local:8080"]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "example.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://proxy.local:8080")
|
||||
}
|
||||
|
||||
@Test("HTTP proxy miss")
|
||||
func testHttpProxyMiss() {
|
||||
let env = ["http_proxy": "http://proxy.local:8080"]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "https", host: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == nil)
|
||||
}
|
||||
|
||||
@Test("HTTPS proxy resolution")
|
||||
func testHttpsProxy() {
|
||||
let env = ["https_proxy": "https://secureproxy.local:8443"]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "https", host: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == "https://secureproxy.local:8443")
|
||||
}
|
||||
|
||||
@Test("HTTPS proxy miss")
|
||||
func testHttpsProxyMiss() {
|
||||
let env = ["https_proxy": "https://secureproxy.local:8443"]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "example.com", env: env)
|
||||
#expect(proxy?.absoluteString == nil)
|
||||
}
|
||||
|
||||
@Test("NO_PROXY exact match")
|
||||
func testNoProxyExactMatch() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"NO_PROXY": "example.com",
|
||||
]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "example.com", env: env)
|
||||
#expect(proxy == nil)
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTP_PROXY is respected")
|
||||
func testUppercaseHttpProxy() {
|
||||
let env = ["HTTP_PROXY": "http://upper.local:8081"]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "upper.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://upper.local:8081")
|
||||
}
|
||||
|
||||
@Test("Lowercase no_proxy is respected")
|
||||
func testLowercaseNoProxy() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"no_proxy": "lower.com",
|
||||
]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "lower.com", env: env)
|
||||
#expect(proxy == nil)
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTP_PROXY overrides lowercase http_proxy")
|
||||
func testUppercaseOverridesLowercaseHttp() {
|
||||
let env = [
|
||||
"http_proxy": "http://lower.local:8080",
|
||||
"HTTP_PROXY": "http://upper.local:8081",
|
||||
]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "example.com", env: env)
|
||||
#expect(proxy?.absoluteString == "http://upper.local:8081")
|
||||
}
|
||||
|
||||
@Test("Uppercase HTTPS_PROXY overrides lowercase https_proxy")
|
||||
func testUppercaseOverridesLowercaseHttps() {
|
||||
let env = [
|
||||
"https_proxy": "https://lower.local:8443",
|
||||
"HTTPS_PROXY": "https://upper.local:8444",
|
||||
]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "https", host: "secure.com", env: env)
|
||||
#expect(proxy?.absoluteString == "https://upper.local:8444")
|
||||
}
|
||||
|
||||
@Test("Uppercase NO_PROXY overrides lowercase no_proxy")
|
||||
func testUppercaseOverridesLowercaseNoProxy() {
|
||||
let env = [
|
||||
"http_proxy": "http://proxy.local:8080",
|
||||
"no_proxy": "foo.com",
|
||||
"NO_PROXY": "bar.com",
|
||||
]
|
||||
let proxyFoo = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "foo.com", env: env)
|
||||
let proxyBar = ProxyUtils.proxyFromEnvironment(scheme: "https", host: "bar.com", env: env)
|
||||
|
||||
#expect(proxyFoo != nil)
|
||||
#expect(proxyBar == nil)
|
||||
}
|
||||
|
||||
@Test("NO_PROXY with wildcard")
|
||||
func testComplexNoProxy() {
|
||||
let env = [
|
||||
"HTTP_PROXY": "http://proxy.example.com",
|
||||
"NO_PROXY": "localhost,127.0.0.1,*.bar.com",
|
||||
]
|
||||
let proxy = ProxyUtils.proxyFromEnvironment(scheme: "http", host: "foo.bar.com", env: env)
|
||||
#expect(proxy == nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
struct TestCIDR {
|
||||
|
||||
// MARK: - Normalization Tests
|
||||
|
||||
struct Normalization {
|
||||
let input: String
|
||||
let expectedNetwork: String
|
||||
let expectedLength: UInt8
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
Normalization(
|
||||
input: "192.168.1.100/24",
|
||||
expectedNetwork: "192.168.1.0",
|
||||
expectedLength: 24
|
||||
),
|
||||
Normalization(
|
||||
input: "10.1.2.3/16",
|
||||
expectedNetwork: "10.1.0.0",
|
||||
expectedLength: 16
|
||||
),
|
||||
Normalization(
|
||||
input: "2001:db8::1234/64",
|
||||
expectedNetwork: "2001:db8::",
|
||||
expectedLength: 64
|
||||
),
|
||||
Normalization(
|
||||
input: "172.16.0.1/12",
|
||||
expectedNetwork: "172.16.0.0",
|
||||
expectedLength: 12
|
||||
),
|
||||
])
|
||||
func testNormalization(testCase: Normalization) throws {
|
||||
let cidr = try CIDR(testCase.input)
|
||||
#expect(cidr.lower.description == testCase.expectedNetwork)
|
||||
#expect(cidr.prefix.length == testCase.expectedLength)
|
||||
}
|
||||
|
||||
struct ParsePreservation {
|
||||
let input: String
|
||||
let expectedIP: String
|
||||
let expectedLength: UInt8
|
||||
}
|
||||
|
||||
// MARK: - Bounds Tests
|
||||
|
||||
struct Bounds {
|
||||
let cidr: String
|
||||
let lower: String
|
||||
let upper: String
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
Bounds(
|
||||
cidr: "192.168.1.0/24",
|
||||
lower: "192.168.1.0",
|
||||
upper: "192.168.1.255"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "10.0.0.0/8",
|
||||
lower: "10.0.0.0",
|
||||
upper: "10.255.255.255"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "2001:db8::/64",
|
||||
lower: "2001:db8::",
|
||||
upper: "2001:db8::ffff:ffff:ffff:ffff"
|
||||
),
|
||||
Bounds(
|
||||
cidr: "192.168.1.0/32",
|
||||
lower: "192.168.1.0",
|
||||
upper: "192.168.1.0"
|
||||
),
|
||||
])
|
||||
func testBounds(testCase: Bounds) throws {
|
||||
let block = try CIDR(testCase.cidr)
|
||||
#expect(block.lower.description == testCase.lower)
|
||||
#expect(block.upper.description == testCase.upper)
|
||||
}
|
||||
|
||||
// MARK: - Containment Tests
|
||||
|
||||
struct IPContainment {
|
||||
let cidr: String
|
||||
let ip: String
|
||||
let shouldContain: Bool
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
IPContainment(
|
||||
cidr: "192.168.1.0/24",
|
||||
ip: "192.168.1.100",
|
||||
shouldContain: true
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "192.168.1.0/24",
|
||||
ip: "192.168.2.1",
|
||||
shouldContain: false
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "10.0.0.0/8",
|
||||
ip: "10.255.255.255",
|
||||
shouldContain: true
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "10.0.0.0/8",
|
||||
ip: "11.0.0.1",
|
||||
shouldContain: false
|
||||
),
|
||||
IPContainment(
|
||||
cidr: "2001:db8::/32",
|
||||
ip: "2001:db8::1",
|
||||
shouldContain: true
|
||||
),
|
||||
])
|
||||
func testContainsIP(testCase: IPContainment) throws {
|
||||
let block = try CIDR(testCase.cidr)
|
||||
let address = try IPAddress(testCase.ip)
|
||||
#expect(block.contains(address) == testCase.shouldContain)
|
||||
}
|
||||
|
||||
@Test func testDoesNotContainDifferentIPv6Zone() throws {
|
||||
let cidr = try CIDR("fe80::1/64")
|
||||
let ip = try IPv6Address("fe80::2%eth1")
|
||||
#expect(!cidr.contains(.v6(ip)))
|
||||
}
|
||||
|
||||
// MARK: - Range Constructor
|
||||
|
||||
@Test func testRangeConstructorFindsSmallestBlock() throws {
|
||||
let lower = try IPAddress("192.168.1.0")
|
||||
let upper = try IPAddress("192.168.1.255")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 24)
|
||||
#expect(cidr.address.description == "192.168.1.0")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorSingleIPv4() throws {
|
||||
let ip = try IPAddress("192.168.1.100")
|
||||
let cidr = try CIDR(lower: ip, upper: ip)
|
||||
#expect(cidr.prefix.length == 32)
|
||||
#expect(cidr.address.description == "192.168.1.100")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorIPv6() throws {
|
||||
let lower = try IPAddress("2001:db8::")
|
||||
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 64)
|
||||
#expect(cidr.address.description == "2001:db8::")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorSingleIPv6() throws {
|
||||
let ip = try IPAddress("2001:db8::1")
|
||||
let cidr = try CIDR(lower: ip, upper: ip)
|
||||
#expect(cidr.prefix.length == 128)
|
||||
#expect(cidr.address.description == "2001:db8::1")
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorRejectsMixedVersions() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let v4 = try IPAddress("192.168.1.0")
|
||||
let v6 = try IPAddress("2001:db8::1")
|
||||
_ = try CIDR(lower: v4, upper: v6)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorRejectsDifferentZones() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let lower = IPAddress.v6(try IPv6Address("fe80::1%eth0"))
|
||||
let upper = IPAddress.v6(try IPv6Address("fe80::2%eth1"))
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Validation Tests
|
||||
|
||||
struct InvalidInput {
|
||||
let input: String
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
InvalidInput(input: "192.168.1.0/33"), // IPv4 prefix too large
|
||||
InvalidInput(input: "2001:db8::/129"), // IPv6 prefix too large
|
||||
InvalidInput(input: "192.168.1.0"), // Missing prefix
|
||||
InvalidInput(input: "192.168.1.0/"), // Empty prefix
|
||||
InvalidInput(input: "192.168.1.0/abc"), // Invalid prefix
|
||||
])
|
||||
func testRejectsInvalidInput(testCase: InvalidInput) throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
try CIDR(testCase.input)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testRejectsInvalidRangeOrder() throws {
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
let lower = try IPAddress("192.168.1.255")
|
||||
let upper = try IPAddress("192.168.1.0")
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Range Constructor Validation Tests
|
||||
|
||||
@Test func testRangeConstructorValidatesContainment() throws {
|
||||
// This should work: 192.168.1.64 to 192.168.1.127 -> /26
|
||||
let lower1 = try IPAddress("192.168.1.64")
|
||||
let upper1 = try IPAddress("192.168.1.127")
|
||||
let cidr1 = try CIDR(lower: lower1, upper: upper1)
|
||||
#expect(cidr1.prefix.length == 26)
|
||||
#expect(cidr1.contains(lower1))
|
||||
#expect(cidr1.contains(upper1))
|
||||
}
|
||||
|
||||
@Test func testRangeConstructorValidatesIPv6Containment() throws {
|
||||
// Test IPv6 range containment validation
|
||||
let lower = try IPAddress("2001:db8::1000")
|
||||
let upper = try IPAddress("2001:db8::1fff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.contains(lower))
|
||||
#expect(cidr.contains(upper))
|
||||
}
|
||||
|
||||
// MARK: - Version-Specific Range Constructors
|
||||
|
||||
@Test func testV4RangeConstructor() throws {
|
||||
let lower = try IPAddress("192.168.1.0")
|
||||
let upper = try IPAddress("192.168.1.255")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 24)
|
||||
#expect(cidr.address.description == "192.168.1.0")
|
||||
}
|
||||
|
||||
@Test func testV4RangeSingleAddress() throws {
|
||||
let addr = try IPAddress("192.168.1.100")
|
||||
let cidr = try CIDR(lower: addr, upper: addr)
|
||||
#expect(cidr.prefix.length == 32)
|
||||
#expect(cidr.address.description == "192.168.1.100")
|
||||
}
|
||||
|
||||
@Test func testV6RangeConstructor() throws {
|
||||
let lower = try IPAddress("2001:db8::")
|
||||
let upper = try IPAddress("2001:db8::ffff:ffff:ffff:ffff")
|
||||
let cidr = try CIDR(lower: lower, upper: upper)
|
||||
#expect(cidr.prefix.length == 64)
|
||||
#expect(cidr.address.description == "2001:db8::")
|
||||
}
|
||||
|
||||
@Test func testV6RangeSingleAddress() throws {
|
||||
let addr = try IPAddress("2001:db8::1")
|
||||
let cidr = try CIDR(lower: addr, upper: addr)
|
||||
#expect(cidr.prefix.length == 128)
|
||||
#expect(cidr.address.description == "2001:db8::1")
|
||||
}
|
||||
|
||||
@Test func testV4RangeRejectsInvalidOrder() throws {
|
||||
let lower = try IPAddress("192.168.1.255")
|
||||
let upper = try IPAddress("192.168.1.0")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testV6RangeRejectsInvalidOrder() throws {
|
||||
let lower = try IPAddress("2001:db8::ffff")
|
||||
let upper = try IPAddress("2001:db8::1")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testV6RangeRejectsDifferentZones() throws {
|
||||
let lower = try IPAddress("fe80::1%eth0")
|
||||
let upper = try IPAddress("fe80::2%eth1")
|
||||
#expect(throws: CIDR.Error.self) {
|
||||
_ = try CIDR(lower: lower, upper: upper)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description Tests
|
||||
|
||||
@Test func testDescriptionFormat() throws {
|
||||
let cidr = try CIDR("10.0.0.0/8")
|
||||
#expect(cidr.description == "10.0.0.0/8")
|
||||
}
|
||||
|
||||
@Test func testPreservesAddress() throws {
|
||||
let cidr = try CIDR("192.168.1.100/24")
|
||||
#expect(cidr.description == "192.168.1.100/24")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"CIDRv4 Codable encodes to string representation",
|
||||
arguments: [
|
||||
"192.168.1.0/24",
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
]
|
||||
)
|
||||
func testCIDRv4CodableEncode(cidr: String) throws {
|
||||
let original = try CIDRv4(cidr)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
let jsonString = String(data: encoded, encoding: .utf8)!
|
||||
#expect(jsonString.contains(original.address.description))
|
||||
#expect(jsonString.contains("\(original.prefix.length)"))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"CIDRv4 Codable decodes from string representation",
|
||||
arguments: [
|
||||
"192.168.1.0/24",
|
||||
"10.0.0.0/8",
|
||||
"172.16.0.0/12",
|
||||
]
|
||||
)
|
||||
func testCIDRv4CodableDecode(cidr: String) throws {
|
||||
let json = Data("\"\(cidr)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(CIDRv4.self, from: json)
|
||||
let expected = try CIDRv4(cidr)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"CIDRv6 Codable encodes to string representation",
|
||||
arguments: [
|
||||
("2001:db8::/32", "2001:db8::", 32),
|
||||
("fe80::/10", "fe80::", 10),
|
||||
("::1/128", "::1", 128),
|
||||
]
|
||||
)
|
||||
func testCIDRv6CodableEncode(cidr: String, expectedAddr: String, expectedPrefix: UInt8) throws {
|
||||
let original = try CIDRv6(cidr)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
let jsonString = String(data: encoded, encoding: .utf8)!
|
||||
#expect(jsonString.contains(expectedAddr))
|
||||
#expect(jsonString.contains("\(expectedPrefix)"))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"CIDRv6 Codable decodes from string representation",
|
||||
arguments: [
|
||||
"2001:db8::/32",
|
||||
"fe80::/10",
|
||||
"::1/128",
|
||||
]
|
||||
)
|
||||
func testCIDRv6CodableDecode(cidr: String) throws {
|
||||
let json = Data("\"\(cidr)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(CIDRv6.self, from: json)
|
||||
let expected = try CIDRv6(cidr)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("Unified IPAddress Tests")
|
||||
struct IPAddressTests {
|
||||
|
||||
@Test(
|
||||
"Parse IPv4 addresses",
|
||||
arguments: [
|
||||
"192.168.1.1",
|
||||
"127.0.0.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testParseIPv4(input: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isV4, "Should be IPv4: \(input)")
|
||||
#expect(!ip.isV6, "Should not be IPv6: \(input)")
|
||||
#expect(ip.ipv4 != nil, "Should have IPv4 value: \(input)")
|
||||
#expect(ip.ipv6 == nil, "Should not have IPv6 value: \(input)")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Parse IPv6 addresses",
|
||||
arguments: [
|
||||
"2001:db8::1",
|
||||
"::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
"2001:db8:0:0:0:0:0:1",
|
||||
]
|
||||
)
|
||||
func testParseIPv6(input: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isV6, "Should be IPv6: \(input)")
|
||||
#expect(!ip.isV4, "Should not be IPv4: \(input)")
|
||||
#expect(ip.ipv6 != nil, "Should have IPv6 value: \(input)")
|
||||
#expect(ip.ipv4 == nil, "Should not have IPv4 value: \(input)")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Loopback detection",
|
||||
arguments: [
|
||||
// Loopback addresses
|
||||
("127.0.0.1", true, "IPv4 loopback"),
|
||||
("127.0.0.255", true, "IPv4 loopback variant"),
|
||||
("127.255.255.255", true, "Any 127.x.x.x"),
|
||||
("::1", true, "IPv6 loopback"),
|
||||
// Non-loopback addresses
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
("0.0.0.0", false, "IPv4 unspecified"),
|
||||
("::", false, "IPv6 unspecified"),
|
||||
]
|
||||
)
|
||||
func testLoopback(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isLoopback == expected, "\(description): \(input) should\(expected ? "" : " not") be loopback")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Multicast detection",
|
||||
arguments: [
|
||||
// Multicast addresses
|
||||
("224.0.0.1", true, "IPv4 multicast start"),
|
||||
("239.255.255.255", true, "IPv4 multicast end (224.0.0.0/4)"),
|
||||
("ff02::1", true, "IPv6 link-local multicast"),
|
||||
("ff00::1", true, "IPv6 multicast"),
|
||||
// Non-multicast addresses
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
("223.255.255.255", false, "Just before multicast range"),
|
||||
]
|
||||
)
|
||||
func testMulticast(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isMulticast == expected, "\(description): \(input) should\(expected ? "" : " not") be multicast")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Unspecified detection",
|
||||
arguments: [
|
||||
// Unspecified addresses
|
||||
("0.0.0.0", true, "IPv4 unspecified"),
|
||||
("::", true, "IPv6 unspecified"),
|
||||
// Specified addresses
|
||||
("0.0.0.1", false, "Not unspecified IPv4"),
|
||||
("192.168.1.1", false, "Private IPv4"),
|
||||
("::1", false, "IPv6 loopback"),
|
||||
("2001:db8::1", false, "IPv6 documentation"),
|
||||
]
|
||||
)
|
||||
func testUnspecified(input: String, expected: Bool, description: String) throws {
|
||||
let ip = try IPAddress(input)
|
||||
#expect(ip.isUnspecified == expected, "\(description): \(input) should\(expected ? "" : " not") be unspecified")
|
||||
}
|
||||
|
||||
@Test("Comparable - IPv4 ordering")
|
||||
func testIPv4Ordering() throws {
|
||||
let ip1 = try IPv4Address("192.168.1.1")
|
||||
let ip2 = try IPv4Address("192.168.1.2")
|
||||
let ip3 = try IPv4Address("192.168.2.1")
|
||||
|
||||
#expect(ip1 < ip2)
|
||||
#expect(ip2 < ip3)
|
||||
#expect(ip1 < ip3)
|
||||
#expect(!(ip2 < ip1))
|
||||
}
|
||||
|
||||
@Test("Comparable - IPv6 ordering")
|
||||
func testIPv6Ordering() throws {
|
||||
let ip1 = try IPv6Address("2001:db8::1")
|
||||
let ip2 = try IPv6Address("2001:db8::2")
|
||||
let ip3 = try IPv6Address("2001:db9::1")
|
||||
|
||||
#expect(ip1 < ip2)
|
||||
#expect(ip2 < ip3)
|
||||
#expect(ip1 < ip3)
|
||||
#expect(!(ip2 < ip1))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Equality",
|
||||
arguments: [
|
||||
("192.168.1.1", "192.168.1.1", true, "Same IPv4"),
|
||||
("192.168.1.1", "192.168.1.2", false, "Different IPv4"),
|
||||
("2001:db8::1", "2001:0db8:0000:0000:0000:0000:0000:0001", true, "Same IPv6, different format"),
|
||||
("2001:db8::1", "2001:db8::2", false, "Different IPv6"),
|
||||
]
|
||||
)
|
||||
func testEquality(addr1: String, addr2: String, shouldBeEqual: Bool, description: String) throws {
|
||||
let ip1 = try IPAddress(addr1)
|
||||
let ip2 = try IPAddress(addr2)
|
||||
|
||||
if shouldBeEqual {
|
||||
#expect(ip1 == ip2, "\(description): \(addr1) should equal \(addr2)")
|
||||
} else {
|
||||
#expect(ip1 != ip2, "\(description): \(addr1) should not equal \(addr2)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Hashable")
|
||||
func testHashable() throws {
|
||||
var dict: [IPAddress: String] = [:]
|
||||
|
||||
let ip1 = try IPAddress("192.168.1.1")
|
||||
let ip2 = try IPAddress("2001:db8::1")
|
||||
|
||||
dict[ip1] = "IPv4"
|
||||
dict[ip2] = "IPv6"
|
||||
|
||||
#expect(dict[ip1] == "IPv4")
|
||||
#expect(dict[ip2] == "IPv6")
|
||||
#expect(dict.count == 2)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableEncodeIPv4(address: String) throws {
|
||||
let original = try IPAddress(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableDecodeIPv4(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPAddress.self, from: json)
|
||||
let expected = try IPAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
("::1", "::1"),
|
||||
("2001:db8::1", "2001:db8::1"),
|
||||
("::", "::"),
|
||||
("fe80::1", "fe80::1"),
|
||||
]
|
||||
)
|
||||
func testCodableEncodeIPv6(input: String, expected: String) throws {
|
||||
let original = try IPAddress(input)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(expected)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"::1",
|
||||
"2001:db8::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
]
|
||||
)
|
||||
func testCodableDecodeIPv6(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPAddress.self, from: json)
|
||||
let expected = try IPAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("IPv4Address Tests")
|
||||
struct IPv4AddressTests {
|
||||
|
||||
// MARK: - Initializer Tests
|
||||
|
||||
@Suite("Initializers")
|
||||
struct InitializerTests {
|
||||
|
||||
@Test(
|
||||
"UInt32 initializer",
|
||||
arguments: [
|
||||
(0x7F00_0001, "127.0.0.1"), // localhost
|
||||
(0x0000_0000, "0.0.0.0"), // zero address
|
||||
(0xFFFF_FFFF, "255.255.255.255"), // max address
|
||||
(0xC0A8_0101, "192.168.1.1"), // private network
|
||||
(0x0808_0808, "8.8.8.8"), // Google DNS
|
||||
]
|
||||
)
|
||||
func testUInt32Initializer(inputValue: UInt32, description: String) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.value == inputValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - valid addresses",
|
||||
arguments: [
|
||||
("127.0.0.1", 0x7F00_0001), // localhost
|
||||
("0.0.0.0", 0x0000_0000), // zero address
|
||||
("255.255.255.255", 0xFFFF_FFFF), // broadcast
|
||||
("10.0.0.1", 0x0A00_0001), // private network 10.x
|
||||
("192.168.1.1", 0xC0A8_0101), // private network 192.168.x
|
||||
("172.16.0.1", 0xAC10_0001), // private network 172.16.x
|
||||
("1.2.3.4", 0x0102_0304), // single digits
|
||||
("192.168.100.254", 0xC0A8_64FE), // mixed digits
|
||||
]
|
||||
)
|
||||
func testStringInitializerValid(addressString: String, expectedValue: UInt32) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - invalid addresses",
|
||||
arguments: [
|
||||
"", // empty string
|
||||
"1.2.3", // too short
|
||||
"1.2.3.4.5", // too many octets
|
||||
"192.168.1.256", // octet out of range
|
||||
"192.168.001.1", // leading zeros
|
||||
"01.2.3.4", // leading zero first octet
|
||||
" 192.168.1.1", // leading whitespace
|
||||
"192.168.1.1 ", // trailing whitespace
|
||||
"192. 168.1.1", // internal whitespace
|
||||
"192.168.1.a", // invalid character
|
||||
"192.168.1.-1", // negative number
|
||||
"192..1.1", // missing octet
|
||||
".168.1.1", // missing first octet
|
||||
"192.168.1.", // missing last octet
|
||||
"192.168.1.1.extra", // too long
|
||||
]
|
||||
)
|
||||
func testStringInitializerInvalid(invalidAddress: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"byte array initializer - valid addresses",
|
||||
arguments: [
|
||||
([UInt8(127), 0, 0, 1], UInt32(0x7F00_0001)), // localhost
|
||||
([UInt8(0), 0, 0, 0], UInt32(0x0000_0000)), // zero address
|
||||
([UInt8(255), 255, 255, 255], UInt32(0xFFFF_FFFF)), // broadcast
|
||||
([UInt8(192), 168, 1, 1], UInt32(0xC0A8_0101)), // private network
|
||||
([UInt8(0x12), 0x34, 0x56, 0x78], UInt32(0x1234_5678)), // byte order test
|
||||
]
|
||||
)
|
||||
func testByteArrayInitializerValid(bytes: [UInt8], expectedValue: UInt32) throws {
|
||||
let address = try IPv4Address(bytes)
|
||||
#expect(address.value == expectedValue)
|
||||
// The byte array initializer must round-trip with the bytes property.
|
||||
#expect(address.bytes == bytes)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"byte array initializer - invalid lengths",
|
||||
arguments: [
|
||||
[], // empty
|
||||
[UInt8(1)], // too short
|
||||
[UInt8(1), 2, 3], // too short
|
||||
[UInt8(1), 2, 3, 4, 5], // too long
|
||||
]
|
||||
)
|
||||
func testByteArrayInitializerInvalid(bytes: [UInt8]) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Suite("Properties")
|
||||
struct PropertyTests {
|
||||
|
||||
@Test(
|
||||
"bytes property",
|
||||
arguments: [
|
||||
(UInt32(0x7F00_0001), [UInt8(127), UInt8(0), UInt8(0), UInt8(1)]), // localhost
|
||||
(UInt32(0x0000_0000), [UInt8(0), UInt8(0), UInt8(0), UInt8(0)]), // zero
|
||||
(UInt32(0xFFFF_FFFF), [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]), // broadcast
|
||||
(UInt32(0xC0A8_0101), [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]), // private network
|
||||
(UInt32(0x1234_5678), [UInt8(0x12), UInt8(0x34), UInt8(0x56), UInt8(0x78)]), // byte order test
|
||||
]
|
||||
)
|
||||
func testBytesProperty(inputValue: UInt32, expectedBytes: [UInt8]) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.bytes == expectedBytes)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"description property",
|
||||
arguments: [
|
||||
(0x7F00_0001, "127.0.0.1"), // localhost
|
||||
(0x0000_0000, "0.0.0.0"), // zero
|
||||
(0xFFFF_FFFF, "255.255.255.255"), // broadcast
|
||||
(0xC0A8_0101, "192.168.1.1"), // private network
|
||||
(0x0102_0304, "1.2.3.4"), // single digits
|
||||
]
|
||||
)
|
||||
func testDescriptionProperty(inputValue: UInt32, expectedDescription: String) {
|
||||
let address = IPv4Address(inputValue)
|
||||
#expect(address.description == expectedDescription)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"round-trip string conversion",
|
||||
arguments: [
|
||||
"0.0.0.0",
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"255.255.255.255",
|
||||
"1.2.3.4",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
]
|
||||
)
|
||||
func testRoundTripStringConversion(addressString: String) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.description == addressString)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Conformance Tests
|
||||
|
||||
@Suite("Protocol Conformances")
|
||||
struct ProtocolConformanceTests {
|
||||
|
||||
@Test("Equatable conformance")
|
||||
func testEquatableConformance() {
|
||||
let addr1 = IPv4Address(0x7F00_0001)
|
||||
let addr2 = IPv4Address(0x7F00_0001)
|
||||
let addr3 = IPv4Address(0xC0A8_0101)
|
||||
|
||||
#expect(addr1 == addr2)
|
||||
#expect(addr1 != addr3)
|
||||
#expect(addr2 != addr3)
|
||||
}
|
||||
|
||||
@Test("Hashable conformance")
|
||||
func testHashableConformance() {
|
||||
let addr1 = IPv4Address(0x7F00_0001)
|
||||
let addr2 = IPv4Address(0x7F00_0001)
|
||||
let addr3 = IPv4Address(0xC0A8_0101)
|
||||
|
||||
// Equal objects should have equal hash values
|
||||
#expect(addr1.hashValue == addr2.hashValue)
|
||||
|
||||
// Different objects should ideally have different hash values
|
||||
// (though this is not guaranteed, it's very likely for these values)
|
||||
#expect(addr1.hashValue != addr3.hashValue)
|
||||
|
||||
// Test that addresses can be used in Sets and Dictionaries
|
||||
let addressSet: Set<IPv4Address> = [addr1, addr2, addr3]
|
||||
#expect(addressSet.count == 2) // addr1 and addr2 are equal
|
||||
|
||||
let addressDict = [addr1: "localhost", addr3: "private"]
|
||||
#expect(addressDict[addr2] == "localhost") // addr2 equals addr1
|
||||
}
|
||||
|
||||
@Test("CustomStringConvertible conformance")
|
||||
func testCustomStringConvertibleConformance() {
|
||||
let address = IPv4Address(0x7F00_0001)
|
||||
let stringRepresentation = String(describing: address)
|
||||
#expect(stringRepresentation == "127.0.0.1")
|
||||
}
|
||||
|
||||
@Test("Sendable conformance")
|
||||
func testSendableConformance() {
|
||||
// This test verifies that IPv4Address can be safely passed across concurrency boundaries
|
||||
let address = IPv4Address(0x7F00_0001)
|
||||
|
||||
Task {
|
||||
let taskAddress = address
|
||||
#expect(taskAddress.value == 0x7F00_0001)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableEncode(address: String) throws {
|
||||
let original = try IPv4Address(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"127.0.0.1",
|
||||
"192.168.1.1",
|
||||
"0.0.0.0",
|
||||
"255.255.255.255",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPv4Address.self, from: json)
|
||||
let expected = try IPv4Address(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases and Error Conditions
|
||||
|
||||
@Suite("Edge Cases")
|
||||
struct EdgeCaseTests {
|
||||
|
||||
@Test(
|
||||
"boundary values",
|
||||
arguments: [
|
||||
("0.0.0.0", 0x0000_0000), // minimum
|
||||
("255.255.255.255", 0xFFFF_FFFF), // maximum
|
||||
("255.0.0.0", 0xFF00_0000), // max first octet
|
||||
("0.255.0.0", 0x00FF_0000), // max second octet
|
||||
("0.0.255.0", 0x0000_FF00), // max third octet
|
||||
("0.0.0.255", 0x0000_00FF), // max fourth octet
|
||||
]
|
||||
)
|
||||
func testBoundaryValues(addressString: String, expectedValue: UInt32) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"special addresses",
|
||||
arguments: [
|
||||
"127.0.0.1", // loopback
|
||||
"255.255.255.255", // broadcast
|
||||
"0.0.0.0", // network address
|
||||
"8.8.8.8", // Google DNS
|
||||
"1.1.1.1", // Cloudflare DNS
|
||||
]
|
||||
)
|
||||
func testSpecialAddresses(addressString: String) throws {
|
||||
let address = try IPv4Address(addressString)
|
||||
#expect(address.description == addressString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"leading zero validation - invalid",
|
||||
arguments: [
|
||||
"01.0.0.0",
|
||||
"0.01.0.0",
|
||||
"0.0.01.0",
|
||||
"0.0.0.01",
|
||||
"192.168.001.1",
|
||||
"010.0.0.1",
|
||||
"00.0.0.1",
|
||||
]
|
||||
)
|
||||
func testLeadingZeroValidationInvalid(invalidAddress: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(invalidAddress)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("leading zero validation - valid single zeros")
|
||||
func testLeadingZeroValidationValid() {
|
||||
// Single "0" should be valid
|
||||
#expect(throws: Never.self) {
|
||||
try IPv4Address("0.0.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"string length validation - too short",
|
||||
arguments: [
|
||||
"", "1", "1.2", "1.2.3", "1.2.3.",
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooShort(shortString: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(shortString)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"string length validation - too long",
|
||||
arguments: [
|
||||
"255.255.255.255.1",
|
||||
"1234.168.1.1",
|
||||
"192.1234.1.1",
|
||||
"192.168.1234.1",
|
||||
"192.168.1.1234",
|
||||
]
|
||||
)
|
||||
func testStringLengthValidationTooLong(longString: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try IPv4Address(longString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Suite("Performance")
|
||||
struct PerformanceTests {
|
||||
|
||||
@Test("parsing performance")
|
||||
func testParsingPerformance() throws {
|
||||
let testAddresses = [
|
||||
"192.168.1.1",
|
||||
"10.0.0.1",
|
||||
"172.16.0.1",
|
||||
"127.0.0.1",
|
||||
"8.8.8.8",
|
||||
"1.1.1.1",
|
||||
"255.255.255.255",
|
||||
"0.0.0.0",
|
||||
]
|
||||
|
||||
// Warm up
|
||||
for _ in 0..<100 {
|
||||
for address in testAddresses {
|
||||
_ = try IPv4Address(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Measure performance
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
for address in testAddresses {
|
||||
_ = try IPv4Address(address)
|
||||
}
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations * testAddresses.count)
|
||||
|
||||
// Should be very fast - less than 1ms per parse on average
|
||||
#expect(averageTime < 0.001, "Parsing should be fast: \(averageTime)s per address")
|
||||
}
|
||||
|
||||
@Test("bytes property performance")
|
||||
func testBytesPropertyPerformance() {
|
||||
let address = IPv4Address(0xC0A8_0101)
|
||||
|
||||
let iterations = 100000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.bytes
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be very fast - less than 0.1ms per call on average
|
||||
#expect(averageTime < 0.0001, "Bytes property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
|
||||
@Test("description property performance")
|
||||
func testDescriptionPropertyPerformance() {
|
||||
let address = IPv4Address(0xC0A8_0101)
|
||||
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.description
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be reasonably fast - less than 1ms per call on average
|
||||
#expect(averageTime < 0.001, "Description property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
@Suite("Integration")
|
||||
struct IntegrationTests {
|
||||
|
||||
@Test(
|
||||
"comprehensive round-trip test",
|
||||
arguments: [
|
||||
(0x0000_0000, "0.0.0.0"),
|
||||
(0x7F00_0001, "127.0.0.1"),
|
||||
(0xC0A8_0101, "192.168.1.1"),
|
||||
(0x0A00_0001, "10.0.0.1"),
|
||||
(0xAC10_0001, "172.16.0.1"),
|
||||
(0xFFFF_FFFF, "255.255.255.255"),
|
||||
(0x0808_0808, "8.8.8.8"),
|
||||
(0x0101_0101, "1.1.1.1"),
|
||||
(0x1234_5678, "18.52.86.120"),
|
||||
(0xDEAD_BEEF, "222.173.190.239"),
|
||||
]
|
||||
)
|
||||
func testComprehensiveRoundTrip(expectedValue: UInt32, expectedString: String) throws {
|
||||
// Test UInt32 -> String
|
||||
let addressFromUInt32 = IPv4Address(expectedValue)
|
||||
#expect(addressFromUInt32.description == expectedString)
|
||||
|
||||
// Test String -> UInt32
|
||||
let addressFromString = try IPv4Address(expectedString)
|
||||
#expect(addressFromString.value == expectedValue)
|
||||
|
||||
// Test equality
|
||||
#expect(addressFromUInt32 == addressFromString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"error message consistency",
|
||||
arguments: [
|
||||
"",
|
||||
"256.1.1.1",
|
||||
"1.2.3",
|
||||
"1.2.3.4.5",
|
||||
"192.168.001.1",
|
||||
" 192.168.1.1",
|
||||
"192.168.1.1 ",
|
||||
"192.168.1.a",
|
||||
]
|
||||
)
|
||||
func testErrorMessageConsistency(invalidInput: String) {
|
||||
do {
|
||||
_ = try IPv4Address(invalidInput)
|
||||
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
|
||||
} catch let error as AddressError {
|
||||
#expect(error == AddressError.unableToParse)
|
||||
} catch {
|
||||
#expect(Bool(false), "Should have thrown AddressError, got: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("IPv6Address Parsing Tests")
|
||||
struct IPv6AddressParseTests {
|
||||
|
||||
// MARK: - Valid Hexadecimal Group Tests
|
||||
|
||||
@Test(
|
||||
"Parsing valid hexadecimal groups",
|
||||
arguments: [
|
||||
("0", 0x0),
|
||||
("1", 0x1),
|
||||
("F", 0xF),
|
||||
("FF", 0xFF),
|
||||
("FFF", 0xFFF),
|
||||
("FFFF", 0xFFFF),
|
||||
("1234", 0x1234),
|
||||
("abcd", 0xABCD),
|
||||
("ABCD", 0xABCD),
|
||||
("0000", 0x0000),
|
||||
]
|
||||
)
|
||||
func testParseValidHexadecimalGroups(input: String, expectedValue: UInt16) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(nextIndex == input.endIndex, "Parser should consume entire input")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Parsing hexadecimal groups with trailing characters",
|
||||
arguments: [
|
||||
("FF:1234", 0xFF, ":1234"),
|
||||
("1234G", 0x1234, "G"),
|
||||
("AB::CD", 0xAB, "::CD"),
|
||||
("0Z", 0x0, "Z"),
|
||||
]
|
||||
)
|
||||
func testParseHexadecimalGroupWithTrailingCharacters(
|
||||
input: String,
|
||||
expectedValue: UInt16,
|
||||
expectedRemainder: String
|
||||
) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(
|
||||
remainder == expectedRemainder,
|
||||
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Error Handling Tests
|
||||
|
||||
@Test(
|
||||
"Parsing invalid hexadecimal groups should throw",
|
||||
arguments: [
|
||||
"", // Empty string - no hex digits found
|
||||
"G", // Invalid hex character - no hex digits found
|
||||
"GGGG", // All invalid hex characters - no hex digits found
|
||||
]
|
||||
)
|
||||
func testParseInvalidHexadecimalGroup(invalidInput: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
let utf8 = invalidInput.utf8
|
||||
_ = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Parsing hexadecimal groups with overflow behavior",
|
||||
arguments: [
|
||||
("12345", 0x1234, "5"), // 5 digits -> takes first 4
|
||||
("10000", 0x1000, "0"), // 5 digits -> takes first 4
|
||||
("FFFFF", 0xFFFF, "F"), // 5 digits -> takes first 4
|
||||
("123456789", 0x1234, "56789"), // Many digits -> takes first 4
|
||||
]
|
||||
)
|
||||
func testParseHexadecimalGroupOverflow(
|
||||
input: String,
|
||||
expectedValue: UInt16,
|
||||
expectedRemainder: String
|
||||
) throws {
|
||||
let utf8 = input.utf8
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
|
||||
#expect(
|
||||
parsedValue == expectedValue,
|
||||
"For input '\(input)': expected \(expectedValue) but got \(parsedValue)"
|
||||
)
|
||||
#expect(
|
||||
remainder == expectedRemainder,
|
||||
"For input '\(input)': expected remainder '\(expectedRemainder)' but got '\(remainder)'"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("Parsing from middle of string")
|
||||
func testParseHexadecimalGroupFromMiddle() throws {
|
||||
let input = "prefix1234suffix"
|
||||
let utf8 = input.utf8
|
||||
let startIndex = utf8.index(utf8.startIndex, offsetBy: 6) // Start at "1234"
|
||||
|
||||
let (parsedValue, nextIndex) = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: startIndex
|
||||
)
|
||||
|
||||
#expect(parsedValue == 0x1234)
|
||||
|
||||
let remainder = String(input[String.Index(nextIndex, within: input)!...])
|
||||
#expect(remainder == "suffix")
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Test("Performance with maximum length hex groups")
|
||||
func testParsePerformance() throws {
|
||||
let testInput = "FFFF"
|
||||
|
||||
// Measure performance of parsing operation
|
||||
let startTime = Date().timeIntervalSinceReferenceDate
|
||||
let count = 10000
|
||||
for _ in 0..<count {
|
||||
let utf8 = testInput.utf8
|
||||
_ = try IPv6Address.parseHexadecimal(
|
||||
from: utf8,
|
||||
startingAt: utf8.startIndex
|
||||
)
|
||||
}
|
||||
|
||||
let timeElapsed = Date().timeIntervalSinceReferenceDate - startTime
|
||||
|
||||
// Expect parsing to be reasonably fast (less than 1ms per operation on average)
|
||||
print("Parsed \(count) IPv6 addresses in \(timeElapsed)s")
|
||||
#expect(timeElapsed < 0.1, "Parsing should be performant: \(timeElapsed)s for 10000 operations")
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.3 Text Representation of Address Prefixes Tests
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Valid address prefix representations")
|
||||
func testRFC4291Section23ValidPrefixRepresentations() throws {
|
||||
// Examples of valid 60-bit prefix 20010DB80000CD3 (hexadecimal)
|
||||
let validPrefixCases = [
|
||||
"2001:0DB8:0000:CD30:0000:0000:0000:0000/60",
|
||||
"2001:0DB8::CD30:0:0:0:0/60",
|
||||
"2001:0DB8:0:CD30::/60",
|
||||
]
|
||||
|
||||
let parsed = try validPrefixCases.map { testCase in
|
||||
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
|
||||
return try IPv6Address.parse(addressPart).bytes
|
||||
}
|
||||
|
||||
#expect(
|
||||
parsed.allSatisfy { $0 == parsed.first },
|
||||
"All valid prefix representations should parse to identical byte arrays"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Invalid address prefix representations")
|
||||
func testRFC4291Section23InvalidPrefixRepresentations() throws {
|
||||
// RFC 4291 Section 2.3 examples of invalid representations
|
||||
let invalidPrefixCases = [
|
||||
"2001:0DB8:0:CD3/60", // ex 1: may drop leading zeros, but not trailing zeros"
|
||||
"2001:0DB8::CD30/60", // ex 2: expands to wrong address
|
||||
"2001:0DB8::CD3/60", // ex 3: expands to wrong address
|
||||
]
|
||||
|
||||
let validAddress = "2001:0DB8:0000:CD30:0000:0000:0000:0000"
|
||||
let parsedValidAddress: [UInt8] = [32, 1, 13, 184, 0, 0, 205, 48, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
let parsedLibValidAddress = try IPv6Address.parse(validAddress).bytes
|
||||
#expect(parsedLibValidAddress == parsedValidAddress)
|
||||
|
||||
for testCase in invalidPrefixCases {
|
||||
let addressPart = String(testCase.prefix(while: { $0 != "/" }))
|
||||
|
||||
do {
|
||||
let actualBytes = try IPv6Address.parse(addressPart).bytes
|
||||
#expect(actualBytes != parsedValidAddress, "\(testCase) should not match valid prefix")
|
||||
} catch {
|
||||
// If parsing fails, that's also acceptable for invalid representations
|
||||
#expect(error is AddressError, "\(testCase) should throw IPAddressError if it fails to parse")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.3 - Leading zero rules in prefixes",
|
||||
arguments: [
|
||||
("2001:DB8:0:0CD3::", "2001:DB8:0:CD3::", "Can drop leading zeros 0CD3 -> CD3"),
|
||||
("2001:0DB8::", "2001:DB8::", "Can drop leading zeros 0DB8 -> DB8"),
|
||||
("0001:0002:0003::", "1:2:3::", "Can drop leading zeros in multiple groups"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section23LeadingZeroRules(form1: String, form2: String, description: String) throws {
|
||||
let bytes1 = try IPv6Address.parse(form1).bytes
|
||||
let bytes2 = try IPv6Address.parse(form2).bytes
|
||||
#expect(bytes1 == bytes2, "\(description)")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.3 - cannot drop trailing zeros",
|
||||
arguments: [
|
||||
("2001:DB8:0:CD30::", "2001:DB8:0:CD3::", "Cannot drop trailing zeros in CD30 -> CD3"),
|
||||
("2001:DB80::", "2001:DB8::", "Cannot drop trailing zero DB80 -> DB8"),
|
||||
("ABCD:EF00::", "ABCD:EF::", "Cannot drop trailing zeros EF00 -> EF"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section23CannotDropTrailingZeros(full: String, truncated: String, description: String) throws {
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
let truncatedBytes = try IPv6Address.parse(truncated).bytes
|
||||
#expect(fullBytes != truncatedBytes, "\(description)")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.3 - Node address and subnet prefix combination")
|
||||
func testRFC4291Section23NodeAddressSubnetCombination() throws {
|
||||
// RFC example: node address 2001:0DB8:0:CD30:123:4567:89AB:CDEF
|
||||
// and its subnet number 2001:0DB8:0:CD30::/60
|
||||
// can be abbreviated as 2001:0DB8:0:CD30:123:4567:89AB:CDEF/60
|
||||
|
||||
let nodeAddress = "2001:0DB8:0:CD30:123:4567:89AB:CDEF"
|
||||
let subnetPrefix = "2001:0DB8:0:CD30::"
|
||||
|
||||
// Both should parse successfully
|
||||
#expect(throws: Never.self, "Node address should parse successfully") {
|
||||
_ = try IPv6Address.parse(nodeAddress)
|
||||
}
|
||||
|
||||
#expect(throws: Never.self, "Subnet prefix should parse successfully") {
|
||||
_ = try IPv6Address.parse(subnetPrefix)
|
||||
}
|
||||
|
||||
// Verify that the subnet prefix is indeed a prefix of the node address
|
||||
let nodeBytes = try IPv6Address.parse(nodeAddress).bytes
|
||||
let subnetBytes = try IPv6Address.parse(subnetPrefix).bytes
|
||||
|
||||
// Validate that node address has the subnet as a prefix (60-bit prefix = 7.5 bytes)
|
||||
let prefixLength = 60
|
||||
let hasValidPrefix = nodeBytes.hasPrefix(subnetBytes, upToBits: prefixLength)
|
||||
|
||||
#expect(
|
||||
hasValidPrefix,
|
||||
"Node address should have subnet as a \(prefixLength)-bit prefix"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.2 Comprehensive Text Representation Tests
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Preferred form with all groups",
|
||||
arguments: [
|
||||
"2001:0db8:0000:0042:0000:8a2e:0370:7334",
|
||||
"ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
|
||||
"0000:0000:0000:0000:0000:0000:0000:0000",
|
||||
"FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22PreferredForm(testCase: String) throws {
|
||||
#expect(throws: Never.self, "Should parse preferred form: \(testCase)") {
|
||||
_ = try IPv6Address.parse(testCase)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Leading zero omission in all positions",
|
||||
arguments: [
|
||||
("2001:0db8:0000:0042:0000:8a2e:0370:7334", "2001:db8:0:42:0:8a2e:370:7334"),
|
||||
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8"),
|
||||
("0000:0001:0002:0003:0004:0005:0006:0007", "0:1:2:3:4:5:6:7"),
|
||||
("1000:0100:0010:0001:1000:0100:0010:0001", "1000:100:10:1:1000:100:10:1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22LeadingZeroOmission(full: String, compressed: String) throws {
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
#expect(fullBytes == compressedBytes, "'\(full)' should equal '\(compressed)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression at beginning",
|
||||
arguments: [
|
||||
("::", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
("::1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
|
||||
("::8a2e:370:7334", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x8a, 0x2e, 0x03, 0x70, 0x73, 0x34]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionAtBeginning(compressed: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(compressed)
|
||||
#expect(parsed.bytes == expected, "'\(compressed)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression in middle",
|
||||
arguments: [
|
||||
("2001:db8::8a2e:370:7334", "2001:db8:0:0:0:8a2e:370:7334"),
|
||||
("2001:db8::1", "2001:db8:0:0:0:0:0:1"),
|
||||
("fe80::1", "fe80:0:0:0:0:0:0:1"),
|
||||
("2001:0db8:0:0::1", "2001:db8:0:0:0:0:0:1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionInMiddle(compressed: String, full: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression at end",
|
||||
arguments: [
|
||||
("2001:db8::", "2001:db8:0:0:0:0:0:0"),
|
||||
("2001:db8:0:0:1::", "2001:db8:0:0:1:0:0:0"),
|
||||
("fe80::", "fe80:0:0:0:0:0:0:0"),
|
||||
("1::", "1:0:0:0:0:0:0:0"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionAtEnd(compressed: String, full: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let fullBytes = try IPv6Address.parse(full).bytes
|
||||
#expect(compressedBytes == fullBytes, "'\(compressed)' should equal '\(full)'")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Multiple :: should fail",
|
||||
arguments: [
|
||||
"2001::db8::1",
|
||||
"::1::2",
|
||||
"fe80::1::2::3",
|
||||
"::1::",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22MultipleDoubleColonsShouldFail(invalid: String) {
|
||||
#expect(throws: AddressError.self, "Multiple '::' should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Case insensitivity",
|
||||
arguments: [
|
||||
("2001:db8::1", "2001:DB8::1", "2001:Db8::1"),
|
||||
("dead:beef::cafe", "DEAD:BEEF::CAFE", "DeAd:BeEf::CaFe"),
|
||||
("fe80::1", "FE80::1", "Fe80::1"),
|
||||
("abcd:ef01:2345:6789::1", "ABCD:EF01:2345:6789::1", "AbCd:Ef01:2345:6789::1"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22CaseInsensitivity(lower: String, upper: String, mixed: String) throws {
|
||||
let lowerBytes = try IPv6Address.parse(lower).bytes
|
||||
let upperBytes = try IPv6Address.parse(upper).bytes
|
||||
let mixedBytes = try IPv6Address.parse(mixed).bytes
|
||||
|
||||
#expect(lowerBytes == upperBytes, "Case should not matter: '\(lower)' vs '\(upper)'")
|
||||
#expect(lowerBytes == mixedBytes, "Case should not matter: '\(lower)' vs '\(mixed)'")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Special addresses")
|
||||
func testRFC4291Section22SpecialAddresses() throws {
|
||||
// Unspecified address
|
||||
let unspecified = try IPv6Address.parse("::")
|
||||
#expect(unspecified.bytes.allSatisfy { $0 == 0 }, ":: should be all zeros")
|
||||
|
||||
// Loopback address
|
||||
let loopback = try IPv6Address.parse("::1")
|
||||
let expectedLoopback: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
|
||||
#expect(loopback.bytes == expectedLoopback, "::1 should be loopback address")
|
||||
|
||||
// IPv4-compatible (deprecated but valid syntax)
|
||||
let ipv4Compat = try IPv6Address.parse("::c000:0201")
|
||||
let expectedCompat: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc0, 0x00, 0x02, 0x01]
|
||||
#expect(ipv4Compat.bytes == expectedCompat, "::c000:0201 should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Invalid formats should fail",
|
||||
arguments: [
|
||||
"2001:db8", // Too few groups without ::
|
||||
"2001:db8:1:2:3:4:5:6:7", // Too many groups
|
||||
"2001:db8:1:2:3:4:5:6:7:8:9", // Too many groups
|
||||
"2001:db8:::1", // Triple colon
|
||||
"2001:db8::1::2", // Multiple ::
|
||||
"gggg::1", // Invalid hex character
|
||||
"2001:db8:xyz::1", // Invalid hex character
|
||||
"::ffff:", // Trailing colon
|
||||
":2001:db8::1", // Leading single colon
|
||||
"2001:db8::1:", // Trailing single colon
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22InvalidFormatsShouldFail(invalid: String) {
|
||||
#expect(throws: AddressError.self, "Invalid format should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Maximum values")
|
||||
func testRFC4291Section22MaximumValues() throws {
|
||||
// All FFs - maximum value
|
||||
let maxAddress = try IPv6Address.parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
#expect(maxAddress.bytes.allSatisfy { $0 == 0xFF }, "All groups should be 0xFFFF")
|
||||
|
||||
// Mix of max and min values
|
||||
let mixedMax = try IPv6Address.parse("ffff:0:ffff:0:ffff:0:ffff:0")
|
||||
let expectedMixed: [UInt8] = [0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0, 0xff, 0xff, 0, 0]
|
||||
#expect(mixedMax.bytes == expectedMixed, "Should alternate between max and zero")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Single zero groups",
|
||||
arguments: [
|
||||
("2001:db8:0:1:2:3:4:5", "2001:db8:0:1:2:3:4:5"), // Single 0, no compression needed
|
||||
("2001:db8:0:0:1:2:3:4", "2001:db8::1:2:3:4"), // Two zeros can be compressed
|
||||
("0:0:0:0:0:0:0:1", "::1"), // All zeros except last
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22SingleZeroGroups(withZero: String, withCompression: String) throws {
|
||||
let zeroBytes = try IPv6Address.parse(withZero).bytes
|
||||
let compressedBytes = try IPv6Address.parse(withCompression).bytes
|
||||
#expect(zeroBytes == compressedBytes, "'\(withZero)' should equal '\(withCompression)'")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Boundary conditions")
|
||||
func testRFC4291Section22BoundaryConditions() throws {
|
||||
// Single non-zero value in each position
|
||||
for position in 0..<8 {
|
||||
var groups = [String](repeating: "0", count: 8)
|
||||
groups[position] = "1"
|
||||
let address = groups.joined(separator: ":")
|
||||
|
||||
#expect(throws: Never.self, "Single non-zero at position \(position) should parse") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the bytes are correct
|
||||
let firstPosition = try IPv6Address.parse("1:0:0:0:0:0:0:0")
|
||||
#expect(firstPosition.bytes[0] == 0 && firstPosition.bytes[1] == 1, "First group should be 0x0001")
|
||||
|
||||
let lastPosition = try IPv6Address.parse("0:0:0:0:0:0:0:1")
|
||||
#expect(lastPosition.bytes[14] == 0 && lastPosition.bytes[15] == 1, "Last group should be 0x0001")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Hex digit limits",
|
||||
arguments: [
|
||||
("1:2:3:4:5:6:7:8", "1-digit groups"),
|
||||
("12:34:56:78:9a:bc:de:f0", "2-digit groups"),
|
||||
("123:456:789:abc:def:123:456:789", "3-digit groups"),
|
||||
("1234:5678:9abc:def0:1234:5678:9abc:def0", "4-digit groups"),
|
||||
("1:12:123:1234:1:12:123:1234", "Mixed digit counts"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22HexDigitLimits(address: String, description: String) throws {
|
||||
#expect(throws: Never.self, "Should parse \(description): \(address)") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - Equivalence of different representations")
|
||||
func testRFC4291Section22EquivalenceOfRepresentations() throws {
|
||||
let equivalentGroups: [[String]] = [
|
||||
// Same address, different representations
|
||||
[
|
||||
"2001:0db8:0000:0000:0000:0000:0000:0001",
|
||||
"2001:db8:0:0:0:0:0:1",
|
||||
"2001:db8::1",
|
||||
"2001:0DB8::1",
|
||||
"2001:0DB8:0000:0000:0000:0000:0000:0001",
|
||||
],
|
||||
[
|
||||
"fe80:0000:0000:0000:0000:0000:0000:0001",
|
||||
"fe80::1",
|
||||
"FE80::1",
|
||||
"fe80:0:0:0:0:0:0:1",
|
||||
],
|
||||
[
|
||||
"0000:0000:0000:0000:0000:0000:0000:0000",
|
||||
"::",
|
||||
"0:0:0:0:0:0:0:0",
|
||||
],
|
||||
]
|
||||
|
||||
for group in equivalentGroups {
|
||||
let bytesArray = try group.map { try IPv6Address.parse($0).bytes }
|
||||
let firstBytes = bytesArray[0]
|
||||
|
||||
for (index, bytes) in bytesArray.enumerated() {
|
||||
#expect(bytes == firstBytes, "All forms should be equivalent: \(group[index])")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Zero compression selection (longest run)",
|
||||
arguments: [
|
||||
("2001:db8:0:0:1:0:0:1", "Two runs of 2 zeros each"),
|
||||
("2001:0:0:0:db8:0:0:1", "Run of 3 and run of 2"),
|
||||
("2001:db8:0:0:0:0:1:1", "Run of 4 zeros in middle"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22ZeroCompressionLongestRun(address: String, description: String) throws {
|
||||
#expect(throws: Never.self, "Should parse \(description): \(address)") {
|
||||
_ = try IPv6Address.parse(address)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - Edge case with :: at different positions",
|
||||
arguments: [
|
||||
("::1", "0:0:0:0:0:0:0:1"),
|
||||
("1::", "1:0:0:0:0:0:0:0"),
|
||||
("1::1", "1:0:0:0:0:0:0:1"),
|
||||
("1:2::1", "1:2:0:0:0:0:0:1"),
|
||||
("1::2:3", "1:0:0:0:0:0:2:3"),
|
||||
("1:2:3::4:5:6", "1:2:3:0:0:4:5:6"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22DoubleColonAtDifferentPositions(compressed: String, expanded: String) throws {
|
||||
let compressedBytes = try IPv6Address.parse(compressed).bytes
|
||||
let expandedBytes = try IPv6Address.parse(expanded).bytes
|
||||
#expect(compressedBytes == expandedBytes, "'\(compressed)' should equal '\(expanded)'")
|
||||
}
|
||||
|
||||
// MARK: - RFC 4291 Section 2.2 IPv4 Mixed Notation Tests
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation basic formats",
|
||||
arguments: [
|
||||
// RFC 4291 examples
|
||||
("0:0:0:0:0:0:13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
|
||||
("::13.1.68.3", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 13, 1, 68, 3]),
|
||||
|
||||
// IPv4-mapped IPv6 address (::ffff:x.x.x.x)
|
||||
("::ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
|
||||
("0:0:0:0:0:ffff:129.144.52.38", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 129, 144, 52, 38]),
|
||||
|
||||
// IPv4-compatible IPv6 address (deprecated but valid syntax)
|
||||
("::192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
("::0.0.0.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]),
|
||||
|
||||
// Various valid positions
|
||||
("2001:db8::192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
|
||||
("fe80::192.168.1.1", [0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotation(address: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes == expected, "IPv4 mixed notation '\(address)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation with full IPv6 prefix",
|
||||
arguments: [
|
||||
("0:0:0:0:0:0:192.168.1.1", [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 1, 1]),
|
||||
("2001:db8:0:0:0:0:192.0.2.1", [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 1]),
|
||||
("64:ff9b::192.0.2.33", [0, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0, 192, 0, 2, 33]),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationFullPrefix(address: String, expected: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes == expected, "Full prefix with IPv4 '\(address)' should parse correctly")
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation edge cases")
|
||||
func testRFC4291Section22IPv4MixedNotationEdgeCases() throws {
|
||||
// Maximum values
|
||||
let maxIPv4 = try IPv6Address.parse("::255.255.255.255")
|
||||
let expectedMax: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255]
|
||||
#expect(maxIPv4.bytes == expectedMax, "Maximum IPv4 values should work")
|
||||
|
||||
// Minimum values
|
||||
let minIPv4 = try IPv6Address.parse("::0.0.0.0")
|
||||
let expectedMin: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
#expect(minIPv4.bytes == expectedMin, "Minimum IPv4 values should work")
|
||||
|
||||
// With non-zero prefix
|
||||
let withPrefix = try IPv6Address.parse("2001:db8:85a3::8a2e:255.255.255.255")
|
||||
let expectedPrefix: [UInt8] = [0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0, 0, 0, 0, 0x8a, 0x2e, 255, 255, 255, 255]
|
||||
#expect(withPrefix.bytes == expectedPrefix, "IPv4 with hex prefix should work")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation invalid formats",
|
||||
arguments: [
|
||||
"::192.168.1", // Incomplete IPv4
|
||||
"::192.168.1.1.1", // Too many IPv4 octets
|
||||
"::256.1.1.1", // IPv4 octet out of range
|
||||
"::192.168.001.1", // Leading zeros in IPv4
|
||||
"::192.168.-1.1", // Negative in IPv4
|
||||
"::192.168.1.a", // Non-numeric in IPv4
|
||||
"2001:db8:1:2:3:4:5:192.168.1.1", // Too many hex groups before IPv4
|
||||
"::192.168.1.1:1234", // Extra hex after IPv4
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationInvalid(invalid: String) {
|
||||
#expect(throws: AddressError.self, "Invalid IPv4 mixed notation should fail: \(invalid)") {
|
||||
_ = try IPv6Address.parse(invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test("RFC 4291 Section 2.2 - IPv4 mixed notation equivalence")
|
||||
func testRFC4291Section22IPv4MixedNotationEquivalence() throws {
|
||||
// These should all represent the same address
|
||||
let equivalentForms = [
|
||||
"0:0:0:0:0:0:192.0.2.1",
|
||||
"::192.0.2.1",
|
||||
"::c000:201", // Same as 192.0.2.1 in hex
|
||||
]
|
||||
|
||||
let bytesArray = try equivalentForms.map { try IPv6Address.parse($0).bytes }
|
||||
let firstBytes = bytesArray[0]
|
||||
|
||||
for (index, bytes) in bytesArray.enumerated() {
|
||||
#expect(bytes == firstBytes, "All forms should be equivalent: \(equivalentForms[index])")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4-mapped IPv6 addresses",
|
||||
arguments: [
|
||||
("127.0.0.1", "::ffff:127.0.0.1"),
|
||||
("192.168.1.1", "::ffff:192.168.1.1"),
|
||||
("8.8.8.8", "::ffff:8.8.8.8"),
|
||||
("0.0.0.0", "::ffff:0.0.0.0"),
|
||||
("255.255.255.255", "::ffff:255.255.255.255"),
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MappedAddresses(ipv4: String, ipv6: String) throws {
|
||||
let parsed = try IPv6Address.parse(ipv6)
|
||||
|
||||
// First 10 bytes should be 0
|
||||
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 }, "First 10 bytes should be zero")
|
||||
|
||||
// Next 2 bytes should be 0xff
|
||||
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff, "Bytes 10-11 should be 0xffff")
|
||||
|
||||
// Last 4 bytes should match IPv4 address
|
||||
let ipv4Parsed = try IPv4Address.parse(ipv4)
|
||||
let ipv4Bytes = [
|
||||
UInt8((ipv4Parsed >> 24) & 0xFF),
|
||||
UInt8((ipv4Parsed >> 16) & 0xFF),
|
||||
UInt8((ipv4Parsed >> 8) & 0xFF),
|
||||
UInt8(ipv4Parsed & 0xFF),
|
||||
]
|
||||
#expect(Array(parsed.bytes[12..<16]) == ipv4Bytes, "Last 4 bytes should match IPv4")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"RFC 4291 Section 2.2 - IPv4 mixed notation with zone identifier",
|
||||
arguments: [
|
||||
"::ffff:192.168.1.1%eth0",
|
||||
"fe80::192.168.1.1%lo0",
|
||||
]
|
||||
)
|
||||
func testRFC4291Section22IPv4MixedNotationWithZone(testCase: String) throws {
|
||||
#expect(throws: Never.self, "IPv4 mixed notation with zone should parse: \(testCase)") {
|
||||
let parsed = try IPv6Address.parse(testCase)
|
||||
#expect(parsed.zone != nil, "Zone should be preserved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array Extension for Prefix Validation
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
/// Checks if this byte array has another array as a prefix up to the specified number of bits
|
||||
/// - Parameters:
|
||||
/// - prefix: The potential prefix array
|
||||
/// - bits: Number of bits to compare (0-128 for IPv6)
|
||||
/// - Returns: true if the prefix matches for the specified number of bits
|
||||
func hasPrefix(_ prefix: [UInt8], upToBits bits: Int) -> Bool {
|
||||
guard self.count >= 16 && prefix.count >= 16 else { return false }
|
||||
guard bits >= 0 && bits <= 128 else { return false }
|
||||
|
||||
let fullBytes = bits / 8
|
||||
let remainingBits = bits % 8
|
||||
|
||||
// Compare full bytes
|
||||
for i in 0..<fullBytes {
|
||||
if self[i] != prefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Compare partial byte if needed
|
||||
if remainingBits > 0 && fullBytes < 16 {
|
||||
let shiftAmount = 8 - remainingBits
|
||||
let mask: UInt8 = shiftAmount >= 8 ? 0 : (0xFF << shiftAmount)
|
||||
return (self[fullBytes] & mask) == (prefix[fullBytes] & mask)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("IPv6 Address Tests")
|
||||
struct IPv6AddressTests {
|
||||
|
||||
// MARK: - String Representation Tests (RFC 5952)
|
||||
|
||||
@Test(
|
||||
"IPv6 address string representation - RFC 5952",
|
||||
arguments: [
|
||||
// Zero compression algorithm tests
|
||||
("0:0:0:0:0:0:0:0", "::", "all zeros - unspecified address"),
|
||||
("0:0:0:0:0:0:0:1", "::1", "leading zeros - loopback"),
|
||||
("2001:0db8:0:0:0:0:0:0", "2001:db8::", "trailing zeros"),
|
||||
("2001:0:0:0:0:0:0db8:1", "2001::db8:1", "middle zeros - longest run"),
|
||||
("2001:0:0:0:0:0db8:0:1", "2001::db8:0:1", "multiple runs - prefer longest"),
|
||||
("2001:0:0db8:0:1:0:0:2", "2001:0:db8:0:1::2", "tie-breaking - first occurrence wins"),
|
||||
("2001:0:0db8:1:2:3:4:5", "2001:0:db8:1:2:3:4:5", "single zero - no compression (min 2 required)"),
|
||||
("2001:0db8:0:0:1:2:3:4", "2001:db8::1:2:3:4", "exactly 2 zeros - minimum for compression"),
|
||||
("0:0:0:0:1234:0:0:0", "::1234:0:0:0", "tie-breaking - first run wins (4 vs 3 zeros)"),
|
||||
|
||||
// RFC 5952 formatting rules
|
||||
(
|
||||
"ABCD:EF01:2345:6789:9ABC:DEF0:1122:3344", "abcd:ef01:2345:6789:9abc:def0:1122:3344",
|
||||
"lowercase hex (Section 4.3)"
|
||||
),
|
||||
("0001:0002:0003:0004:0005:0006:0007:0008", "1:2:3:4:5:6:7:8", "no leading zeros (Section 4.1)"),
|
||||
|
||||
// Edge cases
|
||||
("2001:a:a:a:0:0db8:0:1", "2001:a:a:a:0:db8:0:1", "only single zeros scattered - no compression"),
|
||||
]
|
||||
)
|
||||
func testIPv6StringRepresentation(input: String, expected: String, description: String) throws {
|
||||
let addr = try IPv6Address.parse(input)
|
||||
#expect(
|
||||
addr.description == expected,
|
||||
"Expected '\(expected)' but got '\(addr.description)' for input: '\(input)' (\(description))"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isUnspecified Tests
|
||||
|
||||
@Test(
|
||||
"isUnspecified - RFC 4291 Section 2.5.2",
|
||||
arguments: [
|
||||
("::", true, "unspecified address (short form)"),
|
||||
("0:0:0:0:0:0:0:0", true, "unspecified address (full form)"),
|
||||
(IPv6Address.unspecified.description, true, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("0:0:0:0:0:0:0:1", false, "loopback (full form)"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsUnspecified(address: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(address)
|
||||
#expect(
|
||||
addr.isUnspecified == expected,
|
||||
"Address \(address) (\(description)) should\(expected ? "" : " not") be unspecified"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isLoopback Tests
|
||||
|
||||
@Test(
|
||||
"isLoopback - RFC 4291 Section 2.5.3",
|
||||
arguments: [
|
||||
("::1", true, "loopback (short form)"),
|
||||
("0:0:0:0:0:0:0:1", true, "loopback (full form)"),
|
||||
(IPv6Address.loopback.description, true, "loopback var"),
|
||||
("::", false, "unspecified"),
|
||||
("::2", false, "not loopback"),
|
||||
("0:0:0:0:0:0:0:2", false, "not loopback (full form)"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsLoopback(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isLoopback == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be loopback"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isMulticast Tests
|
||||
|
||||
@Test(
|
||||
"isMulticast - RFC 4291 Section 2.7",
|
||||
arguments: [
|
||||
// Positive cases - all multicast addresses start with ff
|
||||
("ff00::1", true, "Reserved multicast"),
|
||||
("ff01::1", true, "Interface-local multicast"),
|
||||
("ff02::1", true, "Link-local multicast (all nodes)"),
|
||||
("ff02::2", true, "Link-local multicast (all routers)"),
|
||||
("ff05::1", true, "Site-local multicast"),
|
||||
("ff0e::1", true, "Global multicast"),
|
||||
("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Max multicast"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local unicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsMulticast(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isMulticast == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be multicast"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isLinkLocal Tests
|
||||
|
||||
@Test(
|
||||
"isLinkLocal - RFC 4291 Section 2.5.6",
|
||||
arguments: [
|
||||
// Positive cases - fe80::/10
|
||||
("fe80::1", true, "basic link-local"),
|
||||
("fe80::dead:beef", true, "link-local with hex"),
|
||||
("fe80:0:0:0:0:0:0:1", true, "link-local (full form)"),
|
||||
("fe80::1234:5678:90ab:cdef", true, "link-local with interface ID"),
|
||||
("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "Last address in fe80::/10"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fec0::1", false, "site-local (deprecated)"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsLinkLocal(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isLinkLocal == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be link-local"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isUniqueLocal Tests
|
||||
|
||||
@Test(
|
||||
"isUniqueLocal - RFC 4193",
|
||||
arguments: [
|
||||
// Positive cases - fc00::/7 (fc00::/8 and fd00::/8)
|
||||
("fc00::1", true, "fc00 unique local"),
|
||||
("fc00:dead:beef::1", true, "fc00 with hex"),
|
||||
("fd00::1", true, "fd00 unique local"),
|
||||
("fd12:3456:789a::1", true, "fd00 with prefix"),
|
||||
("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true, "max unique local"),
|
||||
// Negative cases
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("2001:db8::1", false, "global unicast"),
|
||||
]
|
||||
)
|
||||
func testIsUniqueLocal(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isUniqueLocal == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be unique local"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isGlobalUnicast Tests
|
||||
|
||||
@Test(
|
||||
"isGlobalUnicast - RFC 4291 Section 2.5.4",
|
||||
arguments: [
|
||||
// Positive cases - routable on the global internet
|
||||
("2001:db8::1", true, "Documentation (but still global unicast format)"),
|
||||
("2001:4860:4860::8888", true, "Google DNS"),
|
||||
("2606:4700:4700::1111", true, "Cloudflare DNS"),
|
||||
("2001:500::1", true, "Root DNS server"),
|
||||
("2a00:1450:4001::1", true, "Google"),
|
||||
// Negative cases - special addresses
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
("fe80::1", false, "link-local"),
|
||||
("ff02::1", false, "multicast"),
|
||||
("fc00::1", false, "unique local"),
|
||||
("fd00::1", false, "unique local"),
|
||||
]
|
||||
)
|
||||
func testIsGlobalUnicast(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isGlobalUnicast == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be global unicast"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - isDocumentation Tests
|
||||
|
||||
@Test(
|
||||
"isDocumentation - RFC 3849",
|
||||
arguments: [
|
||||
// Positive cases - 2001:db8::/32
|
||||
("2001:db8::1", true, "basic documentation address"),
|
||||
("2001:db8::", true, "documentation prefix"),
|
||||
("2001:db8:0:0:0:0:0:1", true, "documentation (full form)"),
|
||||
("2001:db8:1234:5678:90ab:cdef:1234:5678", true, "documentation with all fields"),
|
||||
("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff", true, "max documentation address"),
|
||||
// Negative cases
|
||||
("2001:db7::1", false, "Just before documentation range"),
|
||||
("2001:db9::1", false, "Just after documentation range"),
|
||||
("2001:4860:4860::8888", false, "Google DNS"),
|
||||
("::", false, "unspecified"),
|
||||
("::1", false, "loopback"),
|
||||
]
|
||||
)
|
||||
func testIsDocumentation(addressString: String, expected: Bool, description: String) throws {
|
||||
let addr = try IPv6Address.parse(addressString)
|
||||
#expect(
|
||||
addr.isDocumentation == expected,
|
||||
"Address \(addressString) (\(description)) should\(expected ? "" : " not") be documentation"
|
||||
)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
("::1", "::1"),
|
||||
("2001:db8::1", "2001:db8::1"),
|
||||
("::", "::"),
|
||||
("fe80::1", "fe80::1"),
|
||||
]
|
||||
)
|
||||
func testCodableEncode(input: String, expected: String) throws {
|
||||
let original = try IPv6Address(input)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(expected)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"::1",
|
||||
"2001:db8::1",
|
||||
"::",
|
||||
"fe80::1",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(IPv6Address.self, from: json)
|
||||
let expected = try IPv6Address(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("IPv6 Mixed IPv4 Notation Tests")
|
||||
struct IPv6IPv4ParsingTests {
|
||||
|
||||
@Test(
|
||||
"Extract IPv4 suffix from various IPv6 formats",
|
||||
arguments: [
|
||||
("::192.168.1.1", "::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("::ffff:192.0.2.1", "::ffff", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
|
||||
("fe80::192.168.1.1", "fe80::", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("2001:db8::192.0.2.1", "2001:db8::", [UInt8(192), UInt8(0), UInt8(2), UInt8(1)]),
|
||||
("0:0:0:0:0:0:192.168.1.1", "0:0:0:0:0:0", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
]
|
||||
)
|
||||
func testIPv4SuffixExtraction(input: String, expectedIPv6: String, expectedIPv4: [UInt8]) throws {
|
||||
let result = try #require(try IPv6Address.extractIPv4Suffix(from: input))
|
||||
#expect(result.0 == expectedIPv6)
|
||||
#expect(result.1 == expectedIPv4)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"No IPv4 suffix for pure IPv6 addresses",
|
||||
arguments: [
|
||||
"2001:db8::1",
|
||||
"fe80::1",
|
||||
"::",
|
||||
"::1",
|
||||
]
|
||||
)
|
||||
func testPureIPv6ReturnsNil(address: String) throws {
|
||||
#expect(try IPv6Address.extractIPv4Suffix(from: address) == nil)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Invalid IPv4 suffix throws error",
|
||||
arguments: [
|
||||
"::256.1.1.1",
|
||||
"::192.168.1",
|
||||
"::192.168.001.1",
|
||||
]
|
||||
)
|
||||
func testInvalidIPv4Throws(invalid: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
_ = try IPv6Address.extractIPv4Suffix(from: invalid)
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"IPv4 bytes always at positions 12-15",
|
||||
arguments: [
|
||||
"::192.168.1.1",
|
||||
"::ffff:127.0.0.1",
|
||||
"fe80::10.0.0.1",
|
||||
]
|
||||
)
|
||||
func testIPv4BytePlacement(address: String) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
let ipv4String = String(address.split(separator: ":").last!)
|
||||
let ipv4 = try IPv4Address.parse(ipv4String)
|
||||
|
||||
#expect(parsed.bytes[12] == UInt8((ipv4 >> 24) & 0xFF))
|
||||
#expect(parsed.bytes[13] == UInt8((ipv4 >> 16) & 0xFF))
|
||||
#expect(parsed.bytes[14] == UInt8((ipv4 >> 8) & 0xFF))
|
||||
#expect(parsed.bytes[15] == UInt8(ipv4 & 0xFF))
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Unspecified address with IPv4 suffix",
|
||||
arguments: [
|
||||
("::192.168.1.1", [UInt8(192), UInt8(168), UInt8(1), UInt8(1)]),
|
||||
("::0.0.0.1", [UInt8(0), UInt8(0), UInt8(0), UInt8(1)]),
|
||||
("::255.255.255.255", [UInt8(255), UInt8(255), UInt8(255), UInt8(255)]),
|
||||
]
|
||||
)
|
||||
func testUnspecifiedWithIPv4(address: String, ipv4: [UInt8]) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
#expect(parsed.bytes[0..<12].allSatisfy { $0 == 0 })
|
||||
#expect(Array(parsed.bytes[12..<16]) == ipv4)
|
||||
}
|
||||
|
||||
@Test("IPv4 with zone identifier")
|
||||
func testIPv4WithZone() throws {
|
||||
let parsed = try IPv6Address.parse("::192.168.1.1%lo0")
|
||||
|
||||
#expect(parsed.zone == "lo0")
|
||||
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
|
||||
}
|
||||
|
||||
@Test(
|
||||
"IPv4-mapped addresses (::ffff:x.x.x.x)",
|
||||
arguments: [
|
||||
"::ffff:127.0.0.1",
|
||||
"::ffff:192.168.1.1",
|
||||
]
|
||||
)
|
||||
func testIPv4MappedAddresses(address: String) throws {
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
|
||||
#expect(parsed.bytes[0..<10].allSatisfy { $0 == 0 })
|
||||
#expect(parsed.bytes[10] == 0xff && parsed.bytes[11] == 0xff)
|
||||
}
|
||||
|
||||
@Test("Complex ellipsis with IPv4")
|
||||
func testComplexEllipsisWithIPv4() throws {
|
||||
let address = "2001:db8:85a3::8a2e:192.168.1.1"
|
||||
let parsed = try IPv6Address.parse(address)
|
||||
|
||||
#expect(parsed.bytes[10] == 0x8a && parsed.bytes[11] == 0x2e)
|
||||
#expect(Array(parsed.bytes[12..<16]) == [192, 168, 1, 1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
@Suite("MACAddress Tests")
|
||||
struct MACAddressTests {
|
||||
|
||||
// MARK: - Initializer Tests
|
||||
|
||||
@Suite("Initializers")
|
||||
struct InitializerTests {
|
||||
|
||||
@Test(
|
||||
"UInt64 initializer - valid addresses",
|
||||
arguments: [
|
||||
//(0x0123_4567_89ab, "01:23:45:67:89:ab"), // a valid address
|
||||
//(0x0000_0000_0000, "00:00:00:00:00:00"), // zero address
|
||||
//(0xFFFF_FFFF_FFFF, "ff:ff:ff:ff:ff:ff"), // max address
|
||||
(0xffff_0123_4567_89ab, "01:23:45:67:89:ab") // drops the most significant 16 bits
|
||||
]
|
||||
)
|
||||
func testUInt64InitializerValid(inputValue: UInt64, description: String) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.value == inputValue & 0x0000_ffff_ffff_ffff)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - valid addresses",
|
||||
arguments: [
|
||||
("01:23:45:67:89:ab", 0x0123_4567_89ab), // colon separators
|
||||
("01-23-45-67-89-ab", 0x0123_4567_89ab), // dash separators
|
||||
("ab:cd:ef:AB:CD:EF", 0xabcd_efab_cdef), // mixed case
|
||||
("00:00:00:00:00:00", 0x0000_0000_0000), // zero address
|
||||
("ff:ff:ff:ff:ff:ff", 0xffff_ffff_ffff), // max address
|
||||
]
|
||||
)
|
||||
func testStringInitializerValid(addressString: String, expectedValue: UInt64) throws {
|
||||
let address = try MACAddress(addressString)
|
||||
#expect(address.value == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"String initializer - invalid addresses",
|
||||
arguments: [
|
||||
"", // empty string
|
||||
"01:23:45:67:89", // too few octets
|
||||
"01:23:45:67:89:ab:cd", // too many octets
|
||||
"01:23:45:67:89:", // empty octet
|
||||
":23:45:67:89:ab", // empty octet
|
||||
"01::45:67:89:ab", // empty octet
|
||||
"01:23:45:67:89:a", // short octet
|
||||
"1:23:45:67:89:ab", // short octet
|
||||
"01:2:45:67:89:ab", // short octet
|
||||
"01:23:45:67:89:abc", // long octet
|
||||
"012:23:45:67:89:ab", // long octet
|
||||
"01:234:45:67:89:ab", // long octet
|
||||
"01:23:45:67:89:@G", // invalid content 0x40, 0x47
|
||||
"`g:23:45:67:89:ab", // invalid content 0x60, 0x67
|
||||
"01:hi:45:67:89:ab", // invalid content
|
||||
" 01:23:45:67:89:ab", // leading whitespace
|
||||
"01:23:45:67:89:ab ", // trailing whitespace
|
||||
"01: 23:45:67:89:ab", // internal whitespace
|
||||
]
|
||||
)
|
||||
func testStringInitializerInvalid(invalidAddress: String) {
|
||||
#expect(throws: AddressError.self) {
|
||||
try MACAddress(invalidAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Property Tests
|
||||
|
||||
@Suite("Properties")
|
||||
struct PropertyTests {
|
||||
|
||||
@Test(
|
||||
"bytes property",
|
||||
arguments: [
|
||||
(
|
||||
UInt64(0x0123_4567_89ab),
|
||||
[UInt8(0x01), UInt8(0x23), UInt8(0x45), UInt8(0x67), UInt8(0x89), UInt8(0xab)]
|
||||
),
|
||||
(
|
||||
UInt64(0x0000_0000_0000),
|
||||
[UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00), UInt8(0x00)]
|
||||
),
|
||||
(
|
||||
UInt64(0xffff_ffff_ffff),
|
||||
[UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff), UInt8(0xff)]
|
||||
),
|
||||
(
|
||||
UInt64(0xffff_0123_4567_89ab),
|
||||
[UInt8(0x01), UInt8(0x23), UInt8(0x45), UInt8(0x67), UInt8(0x89), UInt8(0xab)]
|
||||
),
|
||||
]
|
||||
)
|
||||
func testBytesProperty(inputValue: UInt64, expectedBytes: [UInt8]) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.bytes == expectedBytes)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"description property",
|
||||
arguments: [
|
||||
(0x0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
(0x0000_0000_0000, "00:00:00:00:00:00"),
|
||||
(0xffff_ffff_ffff, "ff:ff:ff:ff:ff:ff"),
|
||||
(0xffff_0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
]
|
||||
)
|
||||
func testDescriptionProperty(inputValue: UInt64, expectedDescription: String) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.description == expectedDescription)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"isLocallyAdministered property",
|
||||
arguments: [
|
||||
(0x0000_1234_5678, false),
|
||||
(0x0200_1234_5678, true),
|
||||
]
|
||||
)
|
||||
func testIsLocallyAdministeredProperty(inputValue: UInt64, expectedValue: Bool) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.isLocallyAdministered == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"isMulticast property",
|
||||
arguments: [
|
||||
(0x0000_1234_5678, false),
|
||||
(0x0100_1234_5678, true),
|
||||
]
|
||||
)
|
||||
func testIsMulticastProperty(inputValue: UInt64, expectedValue: Bool) {
|
||||
let address = MACAddress(inputValue)
|
||||
#expect(address.isMulticast == expectedValue)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"round-trip string conversion",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
"01-23-45-67-89-AB",
|
||||
]
|
||||
)
|
||||
func testRoundTripStringConversion(addressString: String) throws {
|
||||
let address = try MACAddress(addressString)
|
||||
#expect(address.description == addressString.lowercased().replacingOccurrences(of: "-", with: ":"))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Link Local Address Tests
|
||||
|
||||
@Suite("Link Local Addresses")
|
||||
struct LinkLocalAddressTests {
|
||||
|
||||
@Test(
|
||||
"Link local address",
|
||||
arguments: [
|
||||
(0x39a7_9407_cbd0, 0xfd97_7b15_d62e_75ac_3ba7_94ff_fe07_cbd0),
|
||||
(0x5e3b_68d7_e510, 0xfd97_7b15_d62e_75ac_5c3b_68ff_fed7_e510),
|
||||
]
|
||||
)
|
||||
func testLinkLocalAddress(mac: UInt64, ipv6: UInt128) throws {
|
||||
let mac = MACAddress(mac)
|
||||
let ipv6Prefix = IPv6Address(ipv6 & 0xffff_ffff_ffff_ffff_0000_0000_0000_0000)
|
||||
let ipv6Address = try mac.ipv6Address(network: ipv6Prefix)
|
||||
#expect(ipv6Address == IPv6Address(ipv6))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Conformance Tests
|
||||
|
||||
@Suite("Protocol Conformances")
|
||||
struct ProtocolConformanceTests {
|
||||
|
||||
@Test("Equatable conformance")
|
||||
func testEquatableConformance() {
|
||||
let addr1 = MACAddress(0x0123_4567_89ab)
|
||||
let addr2 = MACAddress(0x0123_4567_89ab)
|
||||
let addr3 = MACAddress(0x0123_4567_89ac)
|
||||
|
||||
#expect(addr1 == addr2)
|
||||
#expect(addr1 != addr3)
|
||||
#expect(addr2 != addr3)
|
||||
}
|
||||
|
||||
@Test("Hashable conformance")
|
||||
func testHashableConformance() {
|
||||
let addr1 = MACAddress(0x0123_4567_89ab)
|
||||
let addr2 = MACAddress(0x0123_4567_89ab)
|
||||
let addr3 = MACAddress(0x0123_4567_89ac)
|
||||
|
||||
// Equal objects should have equal hash values
|
||||
#expect(addr1.hashValue == addr2.hashValue)
|
||||
|
||||
// Different objects should ideally have different hash values
|
||||
// (though this is not guaranteed, it's very likely for these values)
|
||||
#expect(addr1.hashValue != addr3.hashValue)
|
||||
|
||||
// Test that addresses can be used in Sets and Dictionaries
|
||||
let addressSet: Set<MACAddress> = [addr1, addr2, addr3]
|
||||
#expect(addressSet.count == 2) // addr1 and addr2 are equal
|
||||
|
||||
let addressDict = [addr1: "localhost", addr3: "private"]
|
||||
#expect(addressDict[addr2] == "localhost") // addr2 equals addr1
|
||||
}
|
||||
|
||||
@Test("CustomStringConvertible conformance")
|
||||
func testCustomStringConvertibleConformance() {
|
||||
let address = MACAddress(0x0123_4567_89ab)
|
||||
let stringRepresentation = String(describing: address)
|
||||
#expect(stringRepresentation == "01:23:45:67:89:ab")
|
||||
}
|
||||
|
||||
@Test("Sendable conformance")
|
||||
func testSendableConformance() {
|
||||
// This test verifies that MACAddress can be safely passed across concurrency boundaries
|
||||
let address = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
Task {
|
||||
let taskAddress = address
|
||||
#expect(taskAddress.value == 0x0123_4567_89ab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Performance Tests
|
||||
|
||||
@Suite("Performance")
|
||||
struct PerformanceTests {
|
||||
|
||||
@Test("parsing performance")
|
||||
func testParsingPerformance() throws {
|
||||
let testAddresses = [
|
||||
"01:23:45:67:89:ab",
|
||||
"01-23-45-67-89-ab",
|
||||
"01-23-45-67-89-a",
|
||||
"01-23-45-67-89-abc",
|
||||
]
|
||||
|
||||
// Warm up
|
||||
for _ in 0..<100 {
|
||||
for address in testAddresses {
|
||||
_ = try? MACAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Measure performance
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
for address in testAddresses {
|
||||
_ = try? MACAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations * testAddresses.count)
|
||||
|
||||
// Should be very fast - less than 1ms per parse on average
|
||||
#expect(averageTime < 0.001, "Parsing should be fast: \(averageTime)s per address")
|
||||
}
|
||||
|
||||
@Test("bytes property performance")
|
||||
func testBytesPropertyPerformance() {
|
||||
let address = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
let iterations = 100000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.bytes
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be very fast - less than 0.1ms per call on average
|
||||
#expect(averageTime < 0.0001, "Bytes property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
|
||||
@Test("description property performance")
|
||||
func testDescriptionPropertyPerformance() {
|
||||
let address = MACAddress(0x0123_4567_89ab)
|
||||
|
||||
let iterations = 10000
|
||||
let startTime = Date()
|
||||
|
||||
for _ in 0..<iterations {
|
||||
_ = address.description
|
||||
}
|
||||
|
||||
let endTime = Date()
|
||||
let totalTime = endTime.timeIntervalSince(startTime)
|
||||
let averageTime = totalTime / Double(iterations)
|
||||
|
||||
// Should be reasonably fast - less than 1ms per call on average
|
||||
#expect(averageTime < 0.001, "Description property should be fast: \(averageTime)s per call")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Integration Tests
|
||||
|
||||
@Suite("Integration")
|
||||
struct IntegrationTests {
|
||||
|
||||
@Test(
|
||||
"comprehensive round-trip test",
|
||||
arguments: [
|
||||
(0x0123_4567_89ab, "01:23:45:67:89:ab"),
|
||||
(0x0000_0000_0000, "00:00:00:00:00:00"),
|
||||
(0xffff_ffff_ffff, "ff:ff:ff:ff:ff:ff"),
|
||||
]
|
||||
)
|
||||
func testComprehensiveRoundTrip(expectedValue: UInt64, expectedString: String) throws {
|
||||
// Test UInt32 -> String
|
||||
let addressFromUInt32 = MACAddress(expectedValue)
|
||||
#expect(addressFromUInt32.description == expectedString)
|
||||
|
||||
// Test String -> UInt32
|
||||
let addressFromString = try MACAddress(expectedString)
|
||||
#expect(addressFromString.value == expectedValue)
|
||||
|
||||
// Test equality
|
||||
#expect(addressFromUInt32 == addressFromString)
|
||||
}
|
||||
|
||||
@Test(
|
||||
"error message consistency",
|
||||
arguments: [
|
||||
"",
|
||||
"hi:00:00:00:00:00",
|
||||
"01:23:45:67:89",
|
||||
"01:23:45:67:89:ab:cd",
|
||||
"001:23:45:67:89:ab:cd",
|
||||
" 01:23:45:67:89:ab:cd",
|
||||
"01:23:45:67:89:ab:cd ",
|
||||
]
|
||||
)
|
||||
func testErrorMessageConsistency(invalidInput: String) {
|
||||
do {
|
||||
_ = try MACAddress(invalidInput)
|
||||
#expect(Bool(false), "Should have thrown for input: \(invalidInput)")
|
||||
} catch let error as AddressError {
|
||||
#expect(error == AddressError.unableToParse)
|
||||
} catch {
|
||||
#expect(Bool(false), "Should have thrown AddressError, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable encodes to string representation",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
]
|
||||
)
|
||||
func testCodableEncode(address: String) throws {
|
||||
let original = try MACAddress(address)
|
||||
let encoded = try JSONEncoder().encode(original)
|
||||
#expect(String(data: encoded, encoding: .utf8) == "\"\(address)\"")
|
||||
}
|
||||
|
||||
@Test(
|
||||
"Codable decodes from string representation",
|
||||
arguments: [
|
||||
"01:23:45:67:89:ab",
|
||||
"00:00:00:00:00:00",
|
||||
"ff:ff:ff:ff:ff:ff",
|
||||
]
|
||||
)
|
||||
func testCodableDecode(address: String) throws {
|
||||
let json = Data("\"\(address)\"".utf8)
|
||||
let decoded = try JSONDecoder().decode(MACAddress.self, from: json)
|
||||
let expected = try MACAddress(address)
|
||||
#expect(decoded == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
final class TestAddressAllocators {
|
||||
@Test
|
||||
func testIPv4AddressAllocatorZeroSize() throws {
|
||||
_ = try IPv4Address.allocator(lower: 0xffff_ffff, size: 1)
|
||||
do {
|
||||
_ = try IPv4Address.allocator(lower: 0xffff_ffff, size: 0)
|
||||
#expect(Bool(false), "Expected AllocatorError.rangeExceeded to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .rangeExceeded, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testIPv4AddressAllocatorOverflow() throws {
|
||||
_ = try IPv4Address.allocator(lower: 0xffff_ff00, size: 256)
|
||||
do {
|
||||
_ = try IPv4Address.allocator(lower: 0xffff_ff00, size: 257)
|
||||
#expect(Bool(false), "Expected AllocatorError.rangeExceeded to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .rangeExceeded, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testUInt16AllocatorOverflow() throws {
|
||||
_ = try UInt16.allocator(lower: 0xfff0, size: 16)
|
||||
do {
|
||||
_ = try UInt16.allocator(lower: 0xfff0, size: 17)
|
||||
#expect(Bool(false), "Expected AllocatorError.rangeExceeded to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .rangeExceeded, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testUInt32AllocatorOverflow() throws {
|
||||
_ = try UInt32.allocator(lower: 0xffff_fff0, size: 16)
|
||||
do {
|
||||
_ = try UInt32.allocator(lower: 0xffff_fff0, size: 17)
|
||||
#expect(Bool(false), "Expected AllocatorError.rangeExceeded to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .rangeExceeded, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testFreeUnallocated() throws {
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: 0xc0a8_4000, size: 256)
|
||||
do {
|
||||
_ = try allocator.release(IPv4Address("192.168.64.2"))
|
||||
#expect(Bool(false), "Expected AllocatorError.notAllocated to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .notAllocated("192.168.64.2"), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testChoose() throws {
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: 0xc0a8_4000, size: 2)
|
||||
try allocator.reserve(IPv4Address("192.168.64.1"))
|
||||
do {
|
||||
_ = try allocator.reserve(IPv4Address("192.168.64.1"))
|
||||
#expect(Bool(false), "Expected AllocatorError.alreadyAllocated to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .alreadyAllocated("192.168.64.1"), "Unexpected error thrown: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testipv4AddressAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = try IPv4Address("192.168.64.1").value & Prefix(length: 24)!.prefixMask32
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: lower, size: 3)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
do {
|
||||
_ = try allocator.allocate()
|
||||
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
|
||||
let address = try IPv4Address("192.168.64.2")
|
||||
try allocator.release(address)
|
||||
|
||||
let value = try allocator.allocate()
|
||||
#expect(value == address)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testHighestIPv4AddressAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = try IPv4Address("255.255.255.255").value & Prefix(length: 32)!.prefixMask32
|
||||
let allocator = try IPv4Address.allocator(
|
||||
lower: lower, size: 1)
|
||||
allocations.insert(try allocator.allocate().value)
|
||||
do {
|
||||
_ = try allocator.allocate()
|
||||
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
|
||||
let address = try IPv4Address("255.255.255.255")
|
||||
try allocator.release(address)
|
||||
let value = try allocator.allocate()
|
||||
#expect(value == address)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testLargestIPv4AddressAllocator() throws {
|
||||
// NOTE: This allocator should consume about 16MB
|
||||
_ = try IPv4Address.allocator(lower: 0, size: 1 << 32)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testUInt16PortAllocator() throws {
|
||||
var allocations = Set<UInt16>()
|
||||
let lower = UInt16(1024)
|
||||
let allocator = try UInt16.allocator(lower: lower, size: 3)
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
do {
|
||||
_ = try allocator.allocate()
|
||||
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
|
||||
let address = UInt16(1025)
|
||||
try allocator.release(address)
|
||||
let value = try allocator.allocate()
|
||||
#expect(value == address)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testUInt32PortAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = UInt32(5000)
|
||||
let allocator = try UInt32.allocator(lower: lower, size: 3)
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
do {
|
||||
_ = try allocator.allocate()
|
||||
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
|
||||
let address = UInt32(5001)
|
||||
try allocator.release(address)
|
||||
let value = try allocator.allocate()
|
||||
#expect(value == address)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testRotatingUInt32PortAllocator() throws {
|
||||
var allocations = Set<UInt32>()
|
||||
let lower = UInt32(5000)
|
||||
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
allocations.insert(try allocator.allocate())
|
||||
do {
|
||||
_ = try allocator.allocate()
|
||||
#expect(Bool(false), "Expected AllocatorError.allocatorFull to be thrown")
|
||||
} catch {
|
||||
#expect(error as? AllocatorError == .allocatorFull, "Unexpected error thrown: \(error)")
|
||||
}
|
||||
|
||||
let address = UInt32(5001)
|
||||
try allocator.release(address)
|
||||
let value = try allocator.allocate()
|
||||
#expect(value == address)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testRotatingFIFOUInt32PortAllocator() throws {
|
||||
let lower = UInt32(5000)
|
||||
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
|
||||
let first = try allocator.allocate()
|
||||
#expect(first == 5000)
|
||||
let second = try allocator.allocate()
|
||||
#expect(second == 5001)
|
||||
|
||||
try allocator.release(first)
|
||||
let third = try allocator.allocate()
|
||||
// even after a release, it should continue to allocate in the range
|
||||
// before reusing a previous allocation on the stack.
|
||||
#expect(third == 5002)
|
||||
|
||||
// now the next allocation should be our first port
|
||||
let reused = try allocator.allocate()
|
||||
#expect(reused == first)
|
||||
|
||||
try allocator.release(third)
|
||||
let thirdReused = try allocator.allocate()
|
||||
#expect(thirdReused == third)
|
||||
}
|
||||
|
||||
@Test
|
||||
func testRotatingReservedUInt32PortAllocator() throws {
|
||||
let lower = UInt32(5000)
|
||||
let allocator = try UInt32.rotatingAllocator(lower: lower, size: 3)
|
||||
|
||||
try allocator.reserve(5001)
|
||||
let first = try allocator.allocate()
|
||||
#expect(first == 5000)
|
||||
// this should skip the reserved 5001
|
||||
let second = try allocator.allocate()
|
||||
#expect(second == 5002)
|
||||
|
||||
// no release our reserved
|
||||
try allocator.release(5001)
|
||||
|
||||
let third = try allocator.allocate()
|
||||
#expect(third == 5001)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
struct TestPrefix {
|
||||
|
||||
// MARK: - IPv4 Mask Tests
|
||||
|
||||
struct IPv4Mask {
|
||||
let length: UInt8
|
||||
let expectedPrefix: UInt32
|
||||
let expectedSuffix: UInt32
|
||||
}
|
||||
|
||||
@Test(arguments: [
|
||||
IPv4Mask(
|
||||
length: 0,
|
||||
expectedPrefix: 0x0000_0000,
|
||||
expectedSuffix: 0xFFFF_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 8,
|
||||
expectedPrefix: 0xFF00_0000,
|
||||
expectedSuffix: 0x00FF_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 16,
|
||||
expectedPrefix: 0xFFFF_0000,
|
||||
expectedSuffix: 0x0000_FFFF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 24,
|
||||
expectedPrefix: 0xFFFF_FF00,
|
||||
expectedSuffix: 0x0000_00FF
|
||||
),
|
||||
IPv4Mask(
|
||||
length: 32,
|
||||
expectedPrefix: 0xFFFF_FFFF,
|
||||
expectedSuffix: 0x0000_0000
|
||||
),
|
||||
])
|
||||
func testIPv4Masks(testCase: IPv4Mask) {
|
||||
let prefix = Prefix(length: testCase.length)!
|
||||
#expect(prefix.prefixMask32 == testCase.expectedPrefix)
|
||||
#expect(prefix.suffixMask32 == testCase.expectedSuffix)
|
||||
}
|
||||
|
||||
@Test func testMasksAreInverses32() {
|
||||
for length in 0...32 {
|
||||
let prefix = Prefix(length: UInt8(length))!
|
||||
#expect(~prefix.prefixMask32 == prefix.suffixMask32)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - IPv6 Mask Tests
|
||||
|
||||
struct IPv6Mask {
|
||||
let length: UInt8
|
||||
let expectedPrefix: UInt128
|
||||
let expectedSuffix: UInt128
|
||||
}
|
||||
|
||||
@Test func testIPv6Masks() {
|
||||
let cases = [
|
||||
IPv6Mask(
|
||||
length: 0,
|
||||
expectedPrefix: UInt128(0),
|
||||
expectedSuffix: UInt128.max
|
||||
),
|
||||
IPv6Mask(
|
||||
length: 64,
|
||||
expectedPrefix: 0xFFFF_FFFF_FFFF_FFFF_0000_0000_0000_0000,
|
||||
expectedSuffix: 0x0000_0000_0000_0000_FFFF_FFFF_FFFF_FFFF
|
||||
),
|
||||
IPv6Mask(
|
||||
length: 128,
|
||||
expectedPrefix: UInt128.max,
|
||||
expectedSuffix: UInt128(0)
|
||||
),
|
||||
]
|
||||
for testCase in cases {
|
||||
let prefix = Prefix(length: testCase.length)!
|
||||
#expect(prefix.prefixMask128 == testCase.expectedPrefix)
|
||||
#expect(prefix.suffixMask128 == testCase.expectedSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func testMasksAreInverses128() {
|
||||
for length in stride(from: 0, through: 128, by: 8) {
|
||||
let prefix = Prefix(length: UInt8(length))!
|
||||
#expect(~prefix.prefixMask128 == prefix.suffixMask128)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Description Tests
|
||||
|
||||
@Test func testDescription() {
|
||||
#expect(Prefix(length: 24)!.description == "24")
|
||||
#expect(Prefix(length: 0)!.description == "0")
|
||||
#expect(Prefix(length: 128)!.description == "128")
|
||||
}
|
||||
|
||||
// MARK: - Validation Tests
|
||||
|
||||
@Test func testValidationRejectsInvalidLengths() {
|
||||
// Valid ranges
|
||||
#expect(Prefix(length: 0) != nil)
|
||||
#expect(Prefix(length: 32) != nil)
|
||||
#expect(Prefix(length: 128) != nil)
|
||||
|
||||
// Invalid ranges
|
||||
#expect(Prefix(length: 129) == nil)
|
||||
#expect(Prefix(length: 200) == nil)
|
||||
#expect(Prefix(length: 255) == nil)
|
||||
}
|
||||
|
||||
@Test func testIPv4SpecificValidation() {
|
||||
// Valid IPv4 prefixes
|
||||
#expect(Prefix.ipv4(0) != nil)
|
||||
#expect(Prefix.ipv4(16) != nil)
|
||||
#expect(Prefix.ipv4(32) != nil)
|
||||
|
||||
// Invalid IPv4 prefixes
|
||||
#expect(Prefix.ipv4(33) == nil)
|
||||
#expect(Prefix.ipv4(64) == nil)
|
||||
#expect(Prefix.ipv4(128) == nil)
|
||||
}
|
||||
|
||||
@Test func testIPv6SpecificValidation() {
|
||||
// Valid IPv6 prefixes
|
||||
#expect(Prefix.ipv6(0) != nil)
|
||||
#expect(Prefix.ipv6(64) != nil)
|
||||
#expect(Prefix.ipv6(128) != nil)
|
||||
|
||||
// Invalid IPv6 prefixes
|
||||
#expect(Prefix.ipv6(129) == nil)
|
||||
#expect(Prefix.ipv6(255) == nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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 ContainerizationExtras
|
||||
|
||||
final class TestTimeoutType {
|
||||
@Test
|
||||
func testNoCancellation() async throws {
|
||||
await #expect(throws: Never.self) {
|
||||
try await Timeout.run(
|
||||
for: .seconds(5),
|
||||
operation: {
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testCancellationError() async throws {
|
||||
await #expect(throws: CancellationError.self) {
|
||||
try await Timeout.run(
|
||||
for: .milliseconds(50),
|
||||
operation: {
|
||||
try await Task.sleep(for: .seconds(2))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func testClosureError() async throws {
|
||||
// Check that we get the closures error if we don't timeout, but
|
||||
// the closure does throw before.
|
||||
await #expect(throws: POSIXError.self) {
|
||||
try await Timeout.run(
|
||||
for: .seconds(10),
|
||||
operation: {
|
||||
throw POSIXError(.E2BIG)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025-2026 Apple Inc. and the Containerization 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
|
||||
|
||||
@testable import ContainerizationExtras
|
||||
|
||||
struct BufferTest {
|
||||
// MARK: - hexEncodedString Tests
|
||||
|
||||
@Test func testArrayHexEncodedStringEmpty() {
|
||||
let buffer: [UInt8] = []
|
||||
#expect(buffer.hexEncodedString() == "")
|
||||
}
|
||||
|
||||
@Test func testArrayHexEncodedStringSingleByte() {
|
||||
let buffer: [UInt8] = [0xFF]
|
||||
#expect(buffer.hexEncodedString() == "ff")
|
||||
}
|
||||
|
||||
@Test func testArrayHexEncodedStringMultipleBytes() {
|
||||
let buffer: [UInt8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]
|
||||
#expect(buffer.hexEncodedString() == "0123456789abcdef")
|
||||
}
|
||||
|
||||
@Test func testArrayHexEncodedStringZeroes() {
|
||||
let buffer: [UInt8] = [0x00, 0x00, 0x00]
|
||||
#expect(buffer.hexEncodedString() == "000000")
|
||||
}
|
||||
|
||||
@Test func testArraySliceHexEncodedStringEmpty() {
|
||||
let buffer: [UInt8] = [0x01, 0x02, 0x03]
|
||||
let slice = buffer[0..<0]
|
||||
#expect(slice.hexEncodedString() == "")
|
||||
}
|
||||
|
||||
@Test func testArraySliceHexEncodedStringSingleByte() {
|
||||
let buffer: [UInt8] = [0x01, 0x02, 0x03]
|
||||
let slice = buffer[1..<2]
|
||||
#expect(slice.hexEncodedString() == "02")
|
||||
}
|
||||
|
||||
@Test func testArraySliceHexEncodedStringMultipleBytes() {
|
||||
let buffer: [UInt8] = [0x00, 0xAA, 0xBB, 0xCC, 0x00]
|
||||
let slice = buffer[1..<4]
|
||||
#expect(slice.hexEncodedString() == "aabbcc")
|
||||
}
|
||||
|
||||
// MARK: - bind<T> Tests
|
||||
|
||||
@Test func testBufferBind() throws {
|
||||
let expectedValue: UInt64 = 0x0102_0304_0506_0708
|
||||
let expectedBuffer: [UInt8] = [
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
var buffer = [UInt8](repeating: 0, count: 3 * MemoryLayout<UInt64>.size)
|
||||
guard let ptr = buffer.bind(as: UInt64.self, offset: 2 * MemoryLayout<UInt64>.size) else {
|
||||
#expect(Bool(false), "could not bind value to buffer")
|
||||
return
|
||||
}
|
||||
|
||||
ptr.pointee = expectedValue
|
||||
#expect(buffer == expectedBuffer)
|
||||
}
|
||||
|
||||
@Test func testBufferBindZeroOffset() {
|
||||
let expectedValue: UInt32 = 0x1234_5678
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
guard let ptr = buffer.bind(as: UInt32.self, offset: 0) else {
|
||||
#expect(Bool(false), "could not bind value to buffer at offset 0")
|
||||
return
|
||||
}
|
||||
|
||||
ptr.pointee = expectedValue
|
||||
#expect(buffer[0] == 0x78)
|
||||
#expect(buffer[1] == 0x56)
|
||||
#expect(buffer[2] == 0x34)
|
||||
#expect(buffer[3] == 0x12)
|
||||
}
|
||||
|
||||
@Test func testBufferBindRangeError() throws {
|
||||
var buffer = [UInt8](repeating: 0, count: 3 * MemoryLayout<UInt64>.size)
|
||||
#expect(buffer.bind(as: UInt64.self, offset: 2 * MemoryLayout<UInt64>.size + 1) == nil)
|
||||
}
|
||||
|
||||
@Test func testBufferBindRangeErrorExactBoundary() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
// Trying to bind UInt64 at offset 1 requires 9 bytes total
|
||||
#expect(buffer.bind(as: UInt64.self, offset: 1) == nil)
|
||||
}
|
||||
|
||||
@Test func testBufferBindWithCustomSize() {
|
||||
var buffer = [UInt8](repeating: 0, count: 16)
|
||||
// Request a size larger than the type
|
||||
guard let ptr = buffer.bind(as: UInt32.self, offset: 4, size: 8) else {
|
||||
#expect(Bool(false), "could not bind with custom size")
|
||||
return
|
||||
}
|
||||
|
||||
ptr.pointee = 0xAABB_CCDD
|
||||
#expect(buffer[4] == 0xDD)
|
||||
#expect(buffer[5] == 0xCC)
|
||||
#expect(buffer[6] == 0xBB)
|
||||
#expect(buffer[7] == 0xAA)
|
||||
}
|
||||
|
||||
@Test func testBufferBindWithCustomSizeRangeError() {
|
||||
var buffer = [UInt8](repeating: 0, count: 10)
|
||||
// Request size 8 at offset 4 would require 12 bytes total
|
||||
#expect(buffer.bind(as: UInt32.self, offset: 4, size: 8) == nil)
|
||||
}
|
||||
|
||||
// MARK: - copyIn<T> Tests
|
||||
|
||||
@Test func testCopyInUInt8() {
|
||||
var buffer = [UInt8](repeating: 0, count: 4)
|
||||
let value: UInt8 = 0x42
|
||||
|
||||
guard let offset = buffer.copyIn(as: UInt8.self, value: value, offset: 2) else {
|
||||
#expect(Bool(false), "could not copy UInt8 to buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 3)
|
||||
#expect(buffer[2] == 0x42)
|
||||
}
|
||||
|
||||
@Test func testCopyInUInt16() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
let value: UInt16 = 0x1234
|
||||
|
||||
guard let offset = buffer.copyIn(as: UInt16.self, value: value, offset: 3) else {
|
||||
#expect(Bool(false), "could not copy UInt16 to buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 5)
|
||||
#expect(buffer[3] == 0x34)
|
||||
#expect(buffer[4] == 0x12)
|
||||
}
|
||||
|
||||
@Test func testCopyInUInt32() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
let value: UInt32 = 0x1234_5678
|
||||
|
||||
guard let offset = buffer.copyIn(as: UInt32.self, value: value, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy UInt32 to buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 4)
|
||||
#expect(buffer[0] == 0x78)
|
||||
#expect(buffer[1] == 0x56)
|
||||
#expect(buffer[2] == 0x34)
|
||||
#expect(buffer[3] == 0x12)
|
||||
}
|
||||
|
||||
@Test func testCopyInUInt64() {
|
||||
var buffer = [UInt8](repeating: 0, count: 16)
|
||||
let value: UInt64 = 0x0102_0304_0506_0708
|
||||
|
||||
guard let offset = buffer.copyIn(as: UInt64.self, value: value, offset: 4) else {
|
||||
#expect(Bool(false), "could not copy UInt64 to buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 12)
|
||||
#expect(buffer[4] == 0x08)
|
||||
#expect(buffer[5] == 0x07)
|
||||
#expect(buffer[6] == 0x06)
|
||||
#expect(buffer[7] == 0x05)
|
||||
#expect(buffer[8] == 0x04)
|
||||
#expect(buffer[9] == 0x03)
|
||||
#expect(buffer[10] == 0x02)
|
||||
#expect(buffer[11] == 0x01)
|
||||
}
|
||||
|
||||
@Test func testCopyInRangeError() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
let value: UInt64 = 0x1234_5678_90AB_CDEF
|
||||
|
||||
// Offset 4 + size 8 = 12, but buffer only has 8 bytes
|
||||
#expect(buffer.copyIn(as: UInt64.self, value: value, offset: 4) == nil)
|
||||
}
|
||||
|
||||
@Test func testCopyInExactBoundary() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
let value: UInt64 = 0xFEDC_BA98_7654_3210
|
||||
|
||||
guard let offset = buffer.copyIn(as: UInt64.self, value: value, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy UInt64 at exact boundary")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 8)
|
||||
}
|
||||
|
||||
@Test func testCopyInWithCustomSize() {
|
||||
var buffer = [UInt8](repeating: 0, count: 16)
|
||||
let value: UInt32 = 0xAABB_CCDD
|
||||
|
||||
// Copy with custom size of 8 (larger than UInt32's 4 bytes)
|
||||
guard let offset = buffer.copyIn(as: UInt32.self, value: value, offset: 2, size: 8) else {
|
||||
#expect(Bool(false), "could not copy with custom size")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 6) // offset + MemoryLayout<UInt32>.size
|
||||
#expect(buffer[2] == 0xDD)
|
||||
#expect(buffer[3] == 0xCC)
|
||||
#expect(buffer[4] == 0xBB)
|
||||
#expect(buffer[5] == 0xAA)
|
||||
}
|
||||
|
||||
@Test func testCopyInWithCustomSizeRangeError() {
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
let value: UInt32 = 0x1234_5678
|
||||
|
||||
// Request size 8 at offset 2 would require 10 bytes total
|
||||
#expect(buffer.copyIn(as: UInt32.self, value: value, offset: 2, size: 8) == nil)
|
||||
}
|
||||
|
||||
// MARK: - copyOut<T> Tests
|
||||
|
||||
@Test func testCopyOutUInt8() {
|
||||
let buffer: [UInt8] = [0x00, 0x11, 0x22, 0x33]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt8.self, offset: 2) else {
|
||||
#expect(Bool(false), "could not copy out UInt8")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 3)
|
||||
#expect(value == 0x22)
|
||||
}
|
||||
|
||||
@Test func testCopyOutUInt16() {
|
||||
let buffer: [UInt8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt16.self, offset: 2) else {
|
||||
#expect(Bool(false), "could not copy out UInt16")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 4)
|
||||
#expect(value == 0x3322)
|
||||
}
|
||||
|
||||
@Test func testCopyOutUInt32() {
|
||||
let buffer: [UInt8] = [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt32.self, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy out UInt32")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 4)
|
||||
#expect(value == 0x7856_3412)
|
||||
}
|
||||
|
||||
@Test func testCopyOutUInt64() {
|
||||
let buffer: [UInt8] = [
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||
0xFF, 0xFF,
|
||||
]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: 4) else {
|
||||
#expect(Bool(false), "could not copy out UInt64")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 12)
|
||||
#expect(value == 0x8877_6655_4433_2211)
|
||||
}
|
||||
|
||||
@Test func testCopyOutRangeError() {
|
||||
let buffer: [UInt8] = [0x00, 0x11, 0x22, 0x33]
|
||||
|
||||
// Trying to read UInt64 from offset 0 with only 4 bytes
|
||||
#expect(buffer.copyOut(as: UInt64.self, offset: 0) == nil)
|
||||
}
|
||||
|
||||
@Test func testCopyOutExactBoundary() {
|
||||
let buffer: [UInt8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt64.self, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy out at exact boundary")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 8)
|
||||
#expect(value == 0x0807_0605_0403_0201)
|
||||
}
|
||||
|
||||
@Test func testCopyOutWithCustomSize() {
|
||||
let buffer: [UInt8] = [0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xFF]
|
||||
|
||||
guard let (offset, value) = buffer.copyOut(as: UInt32.self, offset: 2, size: 8) else {
|
||||
#expect(Bool(false), "could not copy out with custom size")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 6) // offset + MemoryLayout<UInt32>.size
|
||||
#expect(value == 0x4433_2211)
|
||||
}
|
||||
|
||||
@Test func testCopyOutWithCustomSizeRangeError() {
|
||||
let buffer: [UInt8] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]
|
||||
|
||||
// Request size 8 at offset 2 would require 10 bytes total, but buffer only has 6
|
||||
#expect(buffer.copyOut(as: UInt32.self, offset: 2, size: 8) == nil)
|
||||
}
|
||||
|
||||
// MARK: - copyIn(buffer:) and copyOut(buffer:) Tests
|
||||
|
||||
@Test func testBufferCopy() throws {
|
||||
let inputBuffer: [UInt8] = [0x01, 0x02, 0x03]
|
||||
var buffer = [UInt8](repeating: 0, count: 9)
|
||||
|
||||
guard let offset = buffer.copyIn(buffer: inputBuffer, offset: 4) else {
|
||||
#expect(Bool(false), "could not copy to buffer")
|
||||
return
|
||||
}
|
||||
#expect(offset == 7)
|
||||
|
||||
guard let offset = buffer.copyIn(buffer: inputBuffer, offset: 6) else {
|
||||
#expect(Bool(false), "could not copy to buffer")
|
||||
return
|
||||
}
|
||||
#expect(offset == 9)
|
||||
|
||||
let expectedBuffer: [UInt8] = [
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01, 0x02, 0x03,
|
||||
]
|
||||
#expect(expectedBuffer == buffer)
|
||||
|
||||
var outputBuffer = [UInt8](repeating: 0, count: 3)
|
||||
guard let offset = buffer.copyOut(buffer: &outputBuffer, offset: 6) else {
|
||||
#expect(Bool(false), "could not copy to buffer")
|
||||
return
|
||||
}
|
||||
#expect(offset == 9)
|
||||
|
||||
let expectedOutputBuffer: [UInt8] = [
|
||||
0x01, 0x02, 0x03,
|
||||
]
|
||||
#expect(expectedOutputBuffer == outputBuffer)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyZeroOffset() {
|
||||
let inputBuffer: [UInt8] = [0xAA, 0xBB, 0xCC]
|
||||
var buffer = [UInt8](repeating: 0, count: 5)
|
||||
|
||||
guard let offset = buffer.copyIn(buffer: inputBuffer, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy to buffer at offset 0")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 3)
|
||||
#expect(buffer[0] == 0xAA)
|
||||
#expect(buffer[1] == 0xBB)
|
||||
#expect(buffer[2] == 0xCC)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyEmptyBuffer() {
|
||||
let inputBuffer: [UInt8] = []
|
||||
var buffer = [UInt8](repeating: 0, count: 5)
|
||||
|
||||
guard let offset = buffer.copyIn(buffer: inputBuffer, offset: 2) else {
|
||||
#expect(Bool(false), "could not copy empty buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 2)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyExactFit() {
|
||||
let inputBuffer: [UInt8] = [0x01, 0x02, 0x03]
|
||||
var buffer = [UInt8](repeating: 0, count: 6)
|
||||
|
||||
guard let offset = buffer.copyIn(buffer: inputBuffer, offset: 3) else {
|
||||
#expect(Bool(false), "could not copy to exact fit")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 6)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyRangeError() throws {
|
||||
let inputBuffer: [UInt8] = [0x01, 0x02, 0x03]
|
||||
var buffer = [UInt8](repeating: 0, count: 9)
|
||||
|
||||
#expect(buffer.copyIn(buffer: inputBuffer, offset: 7) == nil)
|
||||
|
||||
var outputBuffer = [UInt8](repeating: 0, count: 3)
|
||||
#expect(buffer.copyOut(buffer: &outputBuffer, offset: 7) == nil)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyOutZeroOffset() {
|
||||
let buffer: [UInt8] = [0x11, 0x22, 0x33, 0x44, 0x55]
|
||||
var outputBuffer = [UInt8](repeating: 0, count: 3)
|
||||
|
||||
guard let offset = buffer.copyOut(buffer: &outputBuffer, offset: 0) else {
|
||||
#expect(Bool(false), "could not copy out at offset 0")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 3)
|
||||
#expect(outputBuffer[0] == 0x11)
|
||||
#expect(outputBuffer[1] == 0x22)
|
||||
#expect(outputBuffer[2] == 0x33)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyOutEmptyBuffer() {
|
||||
let buffer: [UInt8] = [0x11, 0x22, 0x33]
|
||||
var outputBuffer: [UInt8] = []
|
||||
|
||||
guard let offset = buffer.copyOut(buffer: &outputBuffer, offset: 1) else {
|
||||
#expect(Bool(false), "could not copy out to empty buffer")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 1)
|
||||
}
|
||||
|
||||
@Test func testBufferCopyOutExactFit() {
|
||||
let buffer: [UInt8] = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]
|
||||
var outputBuffer = [UInt8](repeating: 0, count: 3)
|
||||
|
||||
guard let offset = buffer.copyOut(buffer: &outputBuffer, offset: 3) else {
|
||||
#expect(Bool(false), "could not copy out exact fit")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(offset == 6)
|
||||
#expect(outputBuffer[0] == 0xDD)
|
||||
#expect(outputBuffer[1] == 0xEE)
|
||||
#expect(outputBuffer[2] == 0xFF)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user