apollographql / apollo-ios

📱  A strongly-typed, caching GraphQL client for iOS, written in Swift.
https://www.apollographql.com/docs/ios/
MIT License
3.88k stars 721 forks source link

GraphQLEnum and test mock crash #2686

Open Gois opened 1 year ago

Gois commented 1 year ago

Bug report

Creating a test mock with an enum type causes a crash: // Could not cast value of type 'Swift.AnyHashable' to 'ApolloAPI.GraphQLEnum<MySchemaModule.MyEnum>'

Versions

Please fill in the versions you're currently using:

Steps to reproduce

Further details

It's the same crash as #2582

calvincestari commented 1 year ago

Thanks for creating the issue @Gois. We'll take a closer look and see what can be done.

calvincestari commented 1 year ago

@Gois I cannot reproduce this error using 1.0.5.

issue2686.zip is a sample project that links to Apollo iOS with two dependency declarations in the Package:

If you're still experiencing this crash using 1.0.5 can you please alter the sample project to demonstrate the crash.

Gois commented 1 year ago

@calvincestari With the sample project it works for me as well but I've included another sample project. If you go to the apollo_crashTests class you'll see the test testMapGraphQLEnumProperty that one fails for me.

The only time it does work for me is when I also include AppThingMapper in the test target instead of accessing it via the testable import.

apollo-crash.zip

calvincestari commented 1 year ago

Thanks @Gois, that test crashes for me too. I'm not sure quite sure why though, I'll keep digging.

calvincestari commented 1 year ago

Linking to #2668, as it's possibly related.

calvincestari commented 1 year ago

Hi @Gois, I've been looking into this issue this week and I don't have any good resolution to it. I think we're dealing with an issue in Xcode + SPM dependencies which leads to a kind of Diamond Dependency problem. Is this still an issue for you or have you found a way to resolve it?

I think the key message in the debug output is

objc[97411]: Class _TtCO9issue26868MySchema14GetThingsQuery is implemented in both /Users/calvincestari/Library/Developer/XCTestDevices/59D4379F-CC44-4010-A3BB-2A179AFB310A/data/Containers/Bundle/Application/C1106A64-8760-48FC-8076-E7C670D1FF16/apollo-crash.app/apollo-crash (0x1023ee820) and /Users/calvincestari/Library/Developer/XCTestDevices/59D4379F-CC44-4010-A3BB-2A179AFB310A/data/Containers/Bundle/Application/C1106A64-8760-48FC-8076-E7C670D1FF16/apollo-crash.app/PlugIns/apollo-crashTests.xctest/apollo-crashTests (0x109b78a10). One of the two will be used. Which one is undefined.

I suspect the dependency configuration and this Xcode+SPM issue is causing the schema module to be included twice resulting in two types that are the same but different because they are defined in differently loaded modules, resulting in

Could not cast value of type 'Swift.AnyHashable' (0x1baf23698) to 'ApolloAPI.GraphQLEnum<issue2686.MySchema.Size>' (0x139066a88).

When you update to use the main branch, which includes some unreleased fixes, the error message becomes clearer as

Could not cast value of type 'ApolloAPI.GraphQLEnum<issue2686.MySchema.Size>' (0x12906d920) to 'ApolloAPI.GraphQLEnum<issue2686.MySchema.Size>' (0x129070088).

Creating a purely SPM project and configuring the code the same does not result in this error. From what I've found online in the Swift Forums it looks like SPM handles this internally by using a dynamic framework and it's able to do this because it understands the whole dependency chain when building the binaries. I haven't build a demo of it yet but I suspect an Xcode-only project, without SPM, would also have a working test case. We tried splitting the generated schema module into two packages which didn't resolve the issue and it still crashed. The only thing that worked for us in the Xcode + SPM project was to use dynamic packages.

We'll keep digging to see what else we can do to try resolve but I'm at a bit of a dead end right now.

Gois commented 1 year ago

Thanks for the update @calvincestari. I'm still looking into it, I've also noticed that problem where it loads the schema module twice. I'll keep you posted if I find anything.

calvincestari commented 1 year ago

Thank you. I'm surprised we haven't had more reports of this; it seems a very common use case and easy problem to run into.

calvincestari commented 1 year ago

@Gois we have another user with the same kind of issue and they recently posted an update of how they got past it. Their situation and project restructure may not be completely applicable to yours but it's worth looking at their solution - https://github.com/apollographql/apollo-ios/issues/2813#issuecomment-1433401025.

Gois commented 1 year ago

Yes, that looks promising. For now we decided to just use the String value of that type and map it to our own enum.

trevor-holliday-instacart commented 1 year ago

Hi @calvincestari. I am running into a similar crash on 1.2.1 and I have reviewed the targeting issues described in #2813 and #2668. I have reviewed the console logs and I am not seeing reports of Duplicate symbols for the generated classes. I see some duplicate symbol warnings for unrelated sdks, but I am not sure if that should be causing a problem. The mock instantiates with the give params just fine, but whenever I call ...Data.from(mock: Mock) an empty DataDict is returned.

Is this the same issue?

calvincestari commented 1 year ago

Hey @trevor-holliday-instacart, I'm away until the 27th unfortunately. @AnthonyMDev could you take a look at this please - thanks.

AnthonyMDev commented 1 year ago

I've never heard any reports of what you're saying here @trevor-holliday-instacart. This seems to be a separate issue. Please file a new issue report for that. I'm likely going to need a reproduction case to investigate, as this is something I have no reference for yet.

It's also worth noting that there is a complete alternative to using Test Mocks by using generated selection set initializers. You can set that option in your codegen config as shown here.

trevor-holliday-instacart commented 1 year ago

@AnthonyMDev is there documentation built of for that option?

AnthonyMDev commented 1 year ago

Something is wrong with our Swift DocC setup currently and it's not building the documentation for that option. We're trying to sort that one out. But the documentation is there in the code in ApolloCodegenConfiguration.swift

tbbruno commented 4 months ago

Hey folks, do you happen to have any updates on this issue? I'm in the middle of updating Apollo from 0.38.0 (long overdue) to the most recent version and I'm facing the same issue originally reported, but when running the main application, which wasn't a problem before.

The error I'm getting is:

Could not cast value of type 'Swift.AnyHashable' to 'ApolloAPI.GraphQLEnum<MySchema.MyEnum>'.

The crash happens on this line when trying to get the value for a property of the type GraphQLEnum<MySchema.MyEnum>?. It's worth pointing that even though the property is optional, the data that it's trying to decode from has a valid value (and worked without any issues in previous versions of the SDK).

I'm using Apollo installed via SPM, with the schema files being generated as a separate SPM package.

BobaFetters commented 4 months ago

@tbbruno No new updates at the moment, let me take a look into seeing if using mergable libraries might help solve this issue.

BobaFetters commented 4 months ago

@tbbruno Looks like the old test case we had that reproduced the issue no longer does so when updated to the latest library version, possible you can provide a sample project that demonstrates the issue you are seeing that we can test with?

tbbruno commented 4 months ago

@BobaFetters Thanks for taking some time to look into this! I can't share the project where I'm having this issue, but I'll see if I can reproduce it in a new project based on the sample one and get back to you.

tt-pkaminski commented 2 months ago

Just +1'ing this issue to say that I am attempting to set up a local SPM package in our project with our generated Apollo / GQL types, and I'm getting a runtime crash in our application with this error.

Could not cast value of type 'Swift.AnyHashable' (0x123456) to 'ApolloAPI.GraphQLEnum<Namespace.Subpackage.Type>' (0x09876).

Previously our generated types libraries were being added as a .framework target within our application, but we wanted to swap over to SPM so we didn't have to keep resolving merge conflicts in the project.pbxproj file.

BobaFetters commented 2 months ago

@tt-pkaminski We have had trouble in reproducing this issue to investigate the cause, is it possible for you to provide a sample project that demonstrates the issue?

tt-pkaminski commented 2 months ago

Unfortunately our repo is private and I'd have a hard time creating a sample that was 1-1.

The local package structure for our GQL models looks like

GraphQLModels
-- CommonGraphQL (e.g CustomScalars, Enums, Interfaces, Objects, etc)
-- ApplicationAGraphQL
-- ApplicationBGraphQL
-- GQLMocks

**Application A**

Targets:
ApplicationA
- dependencies (SPM): Apollo, ApolloAPI, ApplicationAGraphQL, and CommonGraphQL
ApplicationASnapshotTests
- dependencies (SPM): Apollo, ApolloTestSupport, ApplicationAGraphQL, CommonGraphQL, GQLMocks

**Application B**

Targets:
ApplicationB
- dependencies (SPM): Apollo, ApolloAPI, ApplicationBGraphQL, and CommonGraphQL
ApplicationBSnapshotTests
- dependencies (SPM): Apollo, ApolloTestSupport, ApplicationBGraphQL, CommonGraphQL, GQLMocks

**Shared project**
Defines some common .frameworks shared amongst apps A and B

Targets:
SharedModuleA
- dependencies: Apollo, ApolloAPI, CommonGraphQL
SharedModuleB
- dependencies: Apollo, ApolloAPI, CommonGraphQL
tt-pkaminski commented 2 months ago

@tbbruno did you find any workarounds for this?

tt-pkaminski commented 2 months ago

I'm pretty sure the issue is caused by our project setup, and the root cause seems to be duplicate symbols. We have two apps in our project and after migrating our GQL generated types to local SPM packages, one of them crashes while the other runs without issue.

I've tried several things re: static vs dynamic linking, embedding, etc. Still stumped as to why some of the generated GQL symbols are getting duplicated in our app target.

Related => https://github.com/apollographql/apollo-ios/issues/2813

tt-pkaminski commented 2 months ago

I was able to resolve the duplicate symbols issue in my company's project by splitting our local GQL modules into two separate packages. It would appear that targets within the same Package.swift cannot reference other products defined by that package, so this set up

products: [
        .library(name: "CommonGraphQL", type: .dynamic, targets: ["CommonGraphQL"]),
        .library(name: "AppAGraphQL", type: .dynamic, targets: ["AppAGraphQL"]),
        .library(name: "AppBGraphQL", type: .dynamic, targets: ["AppBGraphQL"]),
    ],

unfortunately will still statically link the symbols from CommonGraphQL to AppAGraphQL.

In order to get around this, you need two separate Package.swift and you can reference the dynamic product in this way:

// CommonGraphQL Package.swift

let package = Package(
    name: "CommonGraphQL",
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(name: "CommonGraphQL", type: .dynamic, targets: ["CommonGraphQL"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apollographql/apollo-ios.git", exact: Version("1.10.0")),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "CommonGraphQL",
            dependencies: [
                .product(name: "Apollo", package: "apollo-ios"),
                .product(name: "ApolloAPI", package: "apollo-ios"),
            ]
        ),
    ]
)

// AppGraphQL Package.swift
products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(name: "AppAGraphQL", type: .dynamic, targets: ["AppAGraphQL"]),
        .library(name: "AppBGraphQL", type: .dynamic, targets: ["AppBGraphQL"]),
        .library(name: "CommonGraphQLMocks", type: .dynamic, targets: ["CommonGraphQLMocks"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apollographql/apollo-ios.git", exact: Version("1.10.0")),
        .package(path: "./CommonGraphQL"),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "AppAGraphQL",
            dependencies: [
                .product(name: "CommonGraphQL", package: "commongraphql"),
                .product(name: "Apollo", package: "apollo-ios"),
                .product(name: "ApolloAPI", package: "apollo-ios"),
            ]
        ),

I'd recommend this article to anyone that is trying to do anything within SPM for the GQL packages so it's easier to wrap your head around this solution https://bpoplauschi.github.io/2021/10/24/Intro-to-static-and-dynamic-libraries-frameworks.html