8da5f2306d
- Bindable is generally useful, so it makes sense to move it out of the netlink library. Removed Equatable conformance from Bindable as this isn't generally necessary (it's only used for unit tests in netlink). - Fix some offset checks in a couple data binding functions and improve the unit tests.
406 lines
14 KiB
Swift
406 lines
14 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
// 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)
|
|
}
|
|
}
|
|
}
|