diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift index 78a573e..fa8fde5 100644 --- a/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift +++ b/Tests/OpenGesturesCompatibilityTests/Core/GestureOutputCompatibilityTests.swift @@ -8,47 +8,19 @@ import Testing // MARK: - GestureOutput Static Constructors extension GestureOutput { - @inline(__always) - private static func make(tag: Int, _ body: (UnsafeMutableRawPointer) -> Void) -> GestureOutput { - let layout = MemoryLayout.self - let ptr = UnsafeMutableRawPointer.allocate( - byteCount: layout.size, - alignment: layout.alignment - ) - defer { ptr.deallocate() } - body(ptr) - Metadata(GestureOutput.self).injectEnumTag(tag: UInt32(tag), ptr) - return ptr.load(as: GestureOutput.self) - } - // case 0: .empty(reason, metadata:) static func empty(_ reason: GestureOutputEmptyReason, metadata: GestureOutputMetadata?) -> GestureOutput { - make(tag: 0) { ptr in - ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) - ptr.storeBytes(of: reason, as: GestureOutputEmptyReason.self) - let metadataOffset = MemoryLayout.stride - (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) - } + makeEnum(tag: 0, payload: (reason, metadata)) } // case 1: .value(v, metadata:) static func value(_ v: Value, metadata: GestureOutputMetadata?) -> GestureOutput { - make(tag: 1) { ptr in - ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) - ptr.initializeMemory(as: Value.self, repeating: v, count: 1) - let metadataOffset = MemoryLayout.stride - (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) - } + makeEnum(tag: 1, payload: (v, metadata)) } // case 2: .finalValue(v, metadata:) static func finalValue(_ v: Value, metadata: GestureOutputMetadata?) -> GestureOutput { - make(tag: 2) { ptr in - ptr.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) - ptr.initializeMemory(as: Value.self, repeating: v, count: 1) - let metadataOffset = MemoryLayout.stride - (ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1) - } + makeEnum(tag: 2, payload: (v, metadata)) } } @@ -76,4 +48,27 @@ struct GestureOutputCompatibilityTests { #expect(output.value == expectedValue) #expect("\(output)".contains(descriptionContains)) } + + // MARK: - Bridged payload (String Value) + + // Exercises a two-field payload `(String, GestureOutputMetadata?)` where the + // String field carries a bridgeObject retain that must survive + // initializeWithCopy during array construction. The previous load+deallocate + // pattern would leak the retain initializeMemory wrote in place. + + @Test + func valueWithStringPayload() { + let output = GestureOutput.value("bridged", metadata: nil) + #expect(output.isEmpty == false) + #expect(output.isFinal == false) + #expect(output.value == "bridged") + } + + @Test + func finalValueWithStringPayload() { + let output = GestureOutput.finalValue("bridged-final", metadata: nil) + #expect(output.isEmpty == false) + #expect(output.isFinal == true) + #expect(output.value == "bridged-final") + } } diff --git a/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift index 3bcd717..db9602d 100644 --- a/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift +++ b/Tests/OpenGesturesCompatibilityTests/Core/GesturePhaseCompatibilityTests.swift @@ -5,49 +5,31 @@ import OpenAttributeGraphShims import Testing -// MARK: - GesturePhase Static Constructors to fix the link issue -// Note: we can't use package/@_spi(Private) to hide the case in swiftinterface. -// Otherwize we'll got a "Will never be executed" warning, and `ptr.load(as: GesturePhase.self)` will result a crash. +// MARK: - GesturePhase Static Constructors extension GesturePhase { - @inline(__always) - private static func make(tag: Int, _ body: (UnsafeMutableRawPointer) -> Void) -> GesturePhase { - let layout = MemoryLayout.self - let ptr = UnsafeMutableRawPointer.allocate( - byteCount: layout.size, - alignment: layout.alignment - ) - defer { ptr.deallocate() } - body(ptr) - Metadata(GesturePhase.self).injectEnumTag(tag: UInt32(tag), ptr) - return ptr.load(as: GesturePhase.self) - } - static func idle() -> GesturePhase { - make(tag: 4) { $0.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) } + makeEnum(tag: 4, payload: ()) } static func possible() -> GesturePhase { - make(tag: 5) { $0.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout.size) } + makeEnum(tag: 5, payload: ()) } static func active(value: Value) -> GesturePhase { - make(tag: 1) { $0.initializeMemory(as: Value.self, repeating: value, count: 1) } + makeEnum(tag: 1, payload: value) } static func blocked(value: Value, blockedBy: GestureNodeID) -> GesturePhase { - make(tag: 0) { ptr in - ptr.initializeMemory(as: Value.self, repeating: value, count: 1) - (ptr + MemoryLayout.stride).initializeMemory(as: GestureNodeID.self, repeating: blockedBy, count: 1) - } + makeEnum(tag: 0, payload: (value, blockedBy)) } static func ended(value: Value) -> GesturePhase { - make(tag: 2) { $0.initializeMemory(as: Value.self, repeating: value, count: 1) } + makeEnum(tag: 2, payload: value) } static func failed(reason: GestureFailureReason) -> GesturePhase { - make(tag: 3) { $0.initializeMemory(as: GestureFailureReason.self, repeating: reason, count: 1) } + makeEnum(tag: 3, payload: reason) } } @@ -100,6 +82,32 @@ struct GesturePhaseCompatibilityTests { let mappedIdle = idle.mapValue { String($0) } #expect(mappedIdle.isIdle == true) } + + // MARK: - Bridged payload (String Value) + + // These tests carry a refcounted Value through the fixture. The previous + // load+deallocate pattern would leak the retain initializeMemory wrote in + // place; correct handling round-trips the String without heap corruption. + + @Test + func activeWithStringPayload() { + let phase = GesturePhase.active(value: "hello") + #expect(phase.isActive == true) + #expect(phase.mapValue { $0.count }.isActive == true) + } + + @Test + func blockedWithStringPayload() { + // Exercises a two-field payload `(String, GestureNodeID)` where the + // String field carries a bridgeObject retain that must survive + // initializeWithCopy during array construction. + let phase = GesturePhase.blocked( + value: "bridged", + blockedBy: GestureNodeID(rawValue: 7) + ) + #expect(phase.isBlocked == true) + #expect(phase.description == "blocked(by: 7)") + } } // MARK: - GestureFailureReasonCompatibilityTests diff --git a/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift b/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift index 5d9f6ca..0b9b8a4 100644 --- a/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift +++ b/Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift @@ -16,3 +16,46 @@ extension Metadata { injectEnumTag(tag: UInt32(tag), UnsafeMutableRawPointer(mutating: ptr)) } } + +/// Builds a value of the enum type `T` by writing the case payload at offset 0 +/// and injecting the enum tag. The payload is typed as a Swift value (typically +/// a tuple for multi-field cases, `Void` for no-payload cases) so the compiler +/// computes the correct field offsets and refcount handling. +/// +/// This helper exists to manufacture enum values whose cases aren't callable +/// directly — e.g. compatibility-test targets that link against Apple's +/// Gestures.framework where case initializers are hidden. Marking those cases +/// `package` or `@_spi(Private)` to hide them from the swiftinterface is not +/// an option: the compiler then emits a "Will never be executed" warning for +/// case-site usage and `ptr.load(as: T.self)` (the previous fixture pattern) +/// crashes. +@inline(__always) +package func makeEnum( + tag: Int, + payload: Payload +) -> T { + precondition( + MemoryLayout.size <= MemoryLayout.size, + "Case payload must fit inside \(T.self)'s enum payload" + ) + let slot = UnsafeMutablePointer.allocate(capacity: 1) + defer { slot.deallocate() } + let raw = UnsafeMutableRawPointer(slot) + + if MemoryLayout.size > 0 { + raw.bindMemory(to: Payload.self, capacity: 1).initialize(to: payload) + } + + let payloadStride = MemoryLayout.stride + let totalStride = MemoryLayout.stride + if totalStride > payloadStride { + (raw + payloadStride).initializeMemory( + as: UInt8.self, + repeating: 0, + count: totalStride - payloadStride + ) + } + + Metadata(T.self).injectEnumTag(tag: UInt32(tag), raw) + return slot.move() +}