An open API service indexing awesome lists of open source software.

https://github.com/rnapier/supportcode

Example project for test support code linking problem
https://github.com/rnapier/supportcode

Last synced: about 2 months ago
JSON representation

Example project for test support code linking problem

Awesome Lists containing this project

README

        

# Link collisions due to diamond across packages

When test support code relies on production code, a diamond can occur. If this occurs across packages, it can lead to duplicated symbols and incorrect behavior at runtime despite no warnings at build time.

This sample project is a minimized and abstracted configuration to demonstrate the issue. It contains the minimum code required to build a failing test case. While dependencies are expressed in the package files, there are few actual `import` statements because very little code is required to cause the problem.

Build and test this project. It should fail, indicating that there are two copies of `Keychain.shared`. This can be seen in the logs by observing the object identifier logs.

## Dependencies Graph

```mermaid
graph

App --- Dependencies/AppDependencies --- Core/Core
Test --- Dependencies/TestDependencies --- Feature/FeatureTestSupport

Core/Core --- Core/Keychain
Core/CoreTestSupport --- Core/Keychain

Feature/FeatureTestSupport --- Core/Core
Feature/FeatureTestSupport --- Core/CoreTestSupport
```

## Overview

App-level dependencies are managed by placing them all into a single `Dependencies` package. Features are broken out into their own packages, with a Core package at the bottom. Packages contain `...TestSupport` modules to share code used by unit tests. These import `XCTest` normally.

As an example of test support code, `CoreTestSupport` provides an `XCTestCase` subclass that is used throughout the tests.

## Packages

* Dependencies: Top-level modules linked from the app and unit tests. The application links the AppDependencies module and the unit tests link the TestDependencies module.

* Core: Module with core logic. Contains Core and Keychain production modules. Also contains CoreTestSupport module that provides test support.

* Feature: Module for a feature that relies on Core. Includes its own test support module that relies on both Core and CoreTestSupport.

## Result

`Core.framework` shows up three times in DerivedData:

```
./App.app/PlugIns/AppTests.xctest/Frameworks/Core_59974D35D_PackageProduct.framework
./App.app/Frameworks/Core_59974D35D_PackageProduct.framework
./PackageFrameworks/Core_59974D35D_PackageProduct.framework
```

When unit tests are run, there is a collision on the symbol `Keychain`:

```
objc[48914]: Class _TtC8Keychain8Keychain is implemented in both /Users/ornapier/Library/Developer/Xcode/DerivedData/App-grdjljgevqofhqgflgtrqvhvbtej/Build/Products/Debug-iphonesimulator/PackageFrameworks/Core_59974D35D_PackageProduct.framework/Core_59974D35D_PackageProduct (0x100a98118) and /Users/ornapier/Library/Developer/CoreSimulator/Devices/216C441E-4AE5-45EC-8E52-FA42D8562365/data/Containers/Bundle/Application/7197F2F2-EB26-42FF-B7DB-67116159897D/App.app/PlugIns/AppTests.xctest/AppTests (0x1011002c0). One of the two will be used. Which one is undefined.
```

This is not benign. There are two distinct copies of `_TtC8Keychain8Keychain` and test cases will access one and the app will access a different one. This leads to mismatches when accessing static instances such as `.shared`.

## Goal

`AppTests.xctest` should not have its own copy of `Core.framework`. It should rely on the app to provide it, just as it relies on the app to provide all other production code. However, FeatureTestSupport needs to access things in Core to configure test cases. It is unclear how to implement this.

Generally speaking this all works. If all of these modules are in the same package, for example, there is no problem. It requires this cross-package dependency to break. There are no warnings during build. This also works if the top level is a Swift package rather than an iOS app. It also works if the impacted code is moved into a module. It is only a problem when the top-level app tests rely on support code.