Weird Crash Involving Swift Packages and Xcode

Hello,

We have recently came across a very strange issue related to Swift Packages and Xcode, which also involves Generics

I was able to recreate a minimum reproducible example of the issue we are encountering which I will use the describe the issue we have

I created a 'New Project' using Xcode 14.3 and have configured it with unit tests. In the new project I have added a local Swift Package 'MyLibrary' (default name) as you can see below

Package.swift

let package = Package(
    name: "MyLibrary",
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]
        ),
        .library(
            name: "MyLibraryMocks",
            targets: ["MyLibraryMocks"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: []
        ),
        .target(
            name: "MyLibraryMocks",
            dependencies: [
                "MyLibrary"
            ]
        )
    ]
)

The package is made up of a "main" target called 'MyLibrary', and a mocks target called 'MyLibraryMocks' which has a dependency on the MyLibrary target. MyLibraryMocks is intended to be used only for unit testing, so it will not be included in the main application target, but rather in the unit tests target of the Xcode project.

MyLibrary.swift contains a simple 1:1 relationship between a 'Component' protocol and its 'Model' counterpart

MyLibrary.swift

public protocol Component<M> where M.C == Self {
    associatedtype M: Model
}

public protocol Model<C>: Hashable where C.M == Self {
    associatedtype C: Component
}

public final class A: Component {
    public typealias M = B

    public init() { }
}

public struct B: Model {
    public typealias C = A

    public init() { }
}

MyLibraryMocks.swift contains mock implementations for both the Component and the Model protocols

MyLibraryMocks.swift

import MyLibrary

public struct Mock: Component {
    public typealias M = MockModel

    public init() { }
}

public struct MockModel: Model {
    public typealias C = Mock

    public init() { }
}

I have added MyLibrary as a dependency in the main application target and have created and ComponentAdapter class which follows the Adapter design pattern.

ComponentAdapter.swift

final class Adapter {
    func adapt() -> some Model {
        B()
    }
}

As you can see, Adapter.adapt method returns an opaque implementation of the Model protocol, because we are really not interested in the actual type that is returned, we only care that it conforms to the Model protocol. This gives us the flexibility to change the object returned by the method, in the future, without breaking the code of the consumer, as the actual type is only an implementation detail.

In the project's unit tests target, we have added MyLibraryMocks as a dependency, since it is not included in the application's target.

MyAppTestsTarget

Strangely enough, when testing the Adapter class, we are not able to cast the opaque some Model returned by the adapt method to its actual implementation, even though we know for sure what the actual type is

// AdapterTests.swift

import XCTest
@testable import MyLibrary
@testable import MyLibraryMocks
@testable import MyApp

final class AdapterTests: XCTestCase {
    func testExample() throws {
        let expectedModel = B()
        let adapter = Adapter()
        let model = adapter.adapt() as! B  // Thread 1: signal SIGABRT

        XCTAssertEqual(expectedModel, model)
    }
}

Running the above test actually crashes at runtime and we are left completely clueless on why that is so.

Xcode console shows the following output

objc[33111]: Class _TtC9MyLibrary1A is implemented in both /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/MyApp (0x1005555b0) and /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/PlugIns/MyAppTests.xctest/MyAppTests (0x1018cc2d0). One of the two will be used. Which one is undefined.
Test Suite 'AdapterTests' started at 2023-04-21 14:04:45.614
Test Case '-[MyAppTests.AdapterTests testExample]' started.
Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).
2023-04-21 14:04:45.614851+0400 MyApp[33111:22124448] Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).

While still connected to the debugger, when running the following command in the console po model as! B, the debugger doesn't crash and it actually prints the correct output MyLibrary.B()

the following test also fails

func testExample2() throws {
        let expectedModel = B()
        let adapter = Adapter()
        let model = adapter.adapt()
        let modelType = type(of: model)
        print(modelType) // B
        XCTAssertEqual(
            ObjectIdentifier(B.self),
            ObjectIdentifier(modelType)
        ) // testExample2(): XCTAssertEqual failed: ("ObjectIdentifier(0x00000001037982c0)") is not equal to ("ObjectIdentifier(0x00000001025b8338)")
    }

I have uploaded the sample project to github https://github.com/vykut/SwiftPackageDuplicateSymbols

Has anyone else faced this before? Are we misusing the Swift Packages somehow?

Replies

We ran into a very similar issue yesterday.

  • also Xcode 14.3
  • tests that require host app
  • duplicate symbol warning when running app target tests only
  • SwiftPM modules pulled in by multiple targets and app tests run in a host application
  • at the end we are having issues with casting a concrete type to the protocol type (yours is the other way around, but same concept)

In the debugger we can cast to a different type that also implements the protocol that is failing to be cast to correctly

If I run the same code that fails to be cast correctly in a SwiftPM Module test target, it works just fine.

Here are the relevant types

protocol FMLoadable {}
protocol LayerProtocol: FMLoadable {}
protocol FeatureLayerProtocol: LayerProtocol {}

class FeatureLayerMock: FeatureLayerProtocol {}

In the code below, baseLayers is [LayerProtocol] and flattenedOperationalLayers is [LayerProtocol] but contains FeatureLayerMock instances

let items: [Loadable] = (baseLayers + map.flattenedOperationalLayers).filter { !($0 is UnsupportedLayerProtocol) }

This code emits the following when run in the app-level test target, with host application

Could not cast value of type '<moduleA>.FeatureLayerMock' (0x14fd582a0) to '<moduleB>.FMLoadable' (0x141b334c0).

This same code runs fine in a SwiftPM test target test

Add a Comment