Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<GestureOutput>.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<GestureOutput>.size)
ptr.storeBytes(of: reason, as: GestureOutputEmptyReason.self)
let metadataOffset = MemoryLayout<GestureOutputEmptyReason>.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<GestureOutput>.size)
ptr.initializeMemory(as: Value.self, repeating: v, count: 1)
let metadataOffset = MemoryLayout<Value>.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<GestureOutput>.size)
ptr.initializeMemory(as: Value.self, repeating: v, count: 1)
let metadataOffset = MemoryLayout<Value>.stride
(ptr + metadataOffset).initializeMemory(as: GestureOutputMetadata?.self, repeating: metadata, count: 1)
}
makeEnum(tag: 2, payload: (v, metadata))
}
}

Expand Down Expand Up @@ -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<String>.value("bridged", metadata: nil)
#expect(output.isEmpty == false)
#expect(output.isFinal == false)
#expect(output.value == "bridged")
}

@Test
func finalValueWithStringPayload() {
let output = GestureOutput<String>.finalValue("bridged-final", metadata: nil)
#expect(output.isEmpty == false)
#expect(output.isFinal == true)
#expect(output.value == "bridged-final")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<GesturePhase>.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<GesturePhase>.size) }
makeEnum(tag: 4, payload: ())
}

static func possible() -> GesturePhase {
make(tag: 5) { $0.initializeMemory(as: UInt8.self, repeating: 0, count: MemoryLayout<GesturePhase>.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<Value>.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)
}
}

Expand Down Expand Up @@ -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<String>.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<String>.blocked(
value: "bridged",
blockedBy: GestureNodeID(rawValue: 7)
)
#expect(phase.isBlocked == true)
#expect(phase.description == "blocked(by: 7)")
}
}

// MARK: - GestureFailureReasonCompatibilityTests
Expand Down
43 changes: 43 additions & 0 deletions Tests/OpenGesturesCompatibilityTests/Metadata+Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Payload>(
tag: Int,
payload: Payload
) -> T {
precondition(
MemoryLayout<Payload>.size <= MemoryLayout<T>.size,
"Case payload must fit inside \(T.self)'s enum payload"
)
let slot = UnsafeMutablePointer<T>.allocate(capacity: 1)
defer { slot.deallocate() }
let raw = UnsafeMutableRawPointer(slot)

if MemoryLayout<Payload>.size > 0 {
raw.bindMemory(to: Payload.self, capacity: 1).initialize(to: payload)
}

let payloadStride = MemoryLayout<Payload>.stride
let totalStride = MemoryLayout<T>.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()
}
Loading