[Swift 5.1] Swift Package ManagerのテストをLinux上でも実行する

## Tl;Dr
Swift 5.1 から追加された`--enable-test-discovery`オプションを渡せばよい。

```bash
$ swift test --enable-test-discovery
```

ただし、**正しく検出できないバグも残っているらしい**ので、心配ならば Swift 4.1 から追加された`--generate-linuxmain`を利用してテスト実行用のソースを自動生成できる。

```bash
$ swift test --generate-linuxmain # 自動生成
$ swift test # 実行
```

つまり、**Swift 4.1 以降であればテスト一覧を自分で列挙する必要はない**。

## Linux環境で実行すべきテストを列挙する（Swift 5.1未満）

Swift 5.1 において`swift package init`でプロジェクトを初期化すると、`Tests`ディレクトリ配下が以下のように生成される（生成時のルートディレクトリは`SwiftPackageExample`）。

```bash
$ tree Tests 
Tests
├── LinuxMain.swift
└── SwiftPackageExampleTests
    ├── SwiftPackageExampleTests.swift
    └── XCTestManifests.swift
```

このうち`LinuxMain.swift`と`XCTestManifests.swift`は、Linux などの macOS 以外の OS でテストを実行するためのファイルであり、前者が『エントリポイント』、後者が『実行すべきテストの一覧を列挙』するためのものになっている。

これらが必要なのは**macOS以外の環境では実行すべきテストの一覧を自動的に検出できなかった**ことにある。

それぞれのソースを簡単に見てみる。

### LinuxMain.swift

`LinuxMain.swift`では、後者の`XCTestManifests.swift`に定義されたメソッドを呼び出すことで、実行すべきテスト一覧を取得し、`XCTMain()`関数でテストを実行している。

```swift:LinuxMain.swift
import XCTest

import SwiftPackageExampleTests

var tests = [XCTestCaseEntry]()
tests += SwiftPackageExampleTests.__allTests() // XCTestManifests.swiftに定義されたメソッドを呼び出し

XCTMain(tests)
```

### XCTestManifests.swift

`XCTestManifests.swift`では、①extensionを利用して各テストクラスの『テスト名』と『テストメソッドの参照』のタプルの一覧を返すように定義され、②さらに前述の`LinuxMain.swift`から呼び出される`__allTests()`関数において`testCase()`関数で`XCTestCaseEntry`に変換して実行すべきテストの一覧を返すようになっている。

```swift:XCTestManifests.swift
#if !canImport(ObjectiveC)
import XCTest

// ①
extension SwiftPackageExampleTests {
    // DO NOT MODIFY: This is autogenerated, use:
    //   `swift test --generate-linuxmain`
    // to regenerate.
    static let __allTests__SwiftPackageExampleTests = [
        ("testExample", testExample),
    ]
}

// ②
public func __allTests() -> [XCTestCaseEntry] {
    return [
        testCase(SwiftPackageExampleTests.__allTests__SwiftPackageExampleTests),
    ]
}
#endif
```

## `--generate-linuxmain`で自動生成する（Swift 4.1+）

Swift 4.1未満では`XCTestManifests.swift`を自分でメンテする必要があり、テストメソッドを新たに追加するごとにテスト一覧の定義を更新する必要があった。

それが [Swift 4.1 において`--generate-linuxmain`というオプションが追加](https://github.com/apple/swift-package-manager/pull/1485)され、完全に自動生成することが可能になった。

```bash
$ swift test --help
...
  --generate-linuxmain    Generate LinuxMain.swift entries for the package
```

以下のコマンドで`LinuxMain.swift`と`XCTManifests.swift`が自動生成される。

```bash
$ swift test --generate-linuxmain
```

前述の`XCTestManifests.swift`のコメントをみると`DO NOT MODIFY: This is autogenerated`と記述されており、自動生成されていることがコメントからも読み取れる。

```swift
// DO NOT MODIFY: This is autogenerated, use:
//   `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__SwiftPackageExampleTests = [
```

## `--enable-test-discovery`で自動検出する（Swift 5.1+）

Swift 5.1 では`--enable-test-discovery`という macOS 以外において、テストの一覧を自動的に検出してテストを実行するオプションが追加された。

```bash
$ swift test --help
...
  --enable-test-discovery
                          Enable test discovery on platforms without Objective-C runtime
```

これを利用すれば前述した`LinuxMain.swift`と`XCTestManifests.swift`は不要となり、Linux環境でも以下のコマンドで実行できるようになる。

```bash
$ swift test --enable-test-discovery
```

しかし、このオプションにはまだバグが残っているらしい（執筆時点）。

* https://bugs.swift.org/browse/SR-11951
* https://bugs.swift.org/browse/SR-12008

自身のプロダクトで正しく検出できるのかは、`--list-tests`オプションでテスト一覧を列挙し、それらの件数が一致するかを確認するとよい。

```bash
# on mac
$ swift test --list-tests 
SwiftPackageExampleTests.SwiftPackageExampleTests/testExample

# on Linux
$ swift test --enable-test-discovery --list-tests
SwiftPackageExampleTests.SwiftPackageExampleTests/testExample
```

もっとも、これはこの時点において正しく検出できるかの確認に過ぎないので、自身のプロダクトにおいてLinux環境でのテストが重要なものであれば、このオプションがデフォルトになる（つまり指定が不要になる）まで見送ったほうがよいかもしれない。

## Dockerで実行できるようにする

mac のローカル環境でもDockerで実行できるようにしておくと何かと便利である。

以下はSwift 5.1のコンテナで実行する`Makefile`の例である。

```:Makefile
linux-test:
	docker run --rm \
		--volume "$(CURDIR):/src" \
		--workdir "/src" \
		swift:5.1 \
		swift test --enable-test-discovery
```

これは次のように実行できる。

```bash
$ make linux-test
```

ついでにコンテナにログインするターゲットを用意しておいてもよいかもしれない。

```:Makefile
linux:
	docker run --rm -it \
		--volume "$(CURDIR):/src" \
		--workdir "/src" \
		swift:5.1
```

`--volume`でマウントしているので、ソースの変更などはすべてmac側で行える。

```bash
$ make linux
docker run --rm -it \
                --volume "/Users/hosonumayuusuke/go/src/github.com/YusukeHosonuma/SwiftPrettyPrint:/src" \
                --workdir "/src" \
                swift:5.1
root@ff97680b040b:/src# 
```

## GitHub Actionsで実行できるようにする

以下のように`runs-on`に`ubuntu-latest`、`container`に`swift:5.1`を指定してジョブを定義すればよい。

```yaml
name: Test

on:
  push:
    branches:
      - master
  pull_request:

jobs:
  unit-test-in-linux:
    runs-on: ubuntu-latest
    container: swift:5.1

    steps:
    - uses: actions/checkout@v2

    - name: Run Unit Test
      run: swift test --enable-test-discovery
```

## 参考PR

自身のOSSプロダクトについて、以下のPRで対応したので参考までに。
https://github.com/YusukeHosonuma/SwiftParamTest/pull/34

[LinuxMain.swiftを残してしまっている](https://github.com/YusukeHosonuma/SwiftParamTest/pull/34/files#diff-4e60993c248db049dff70bb5c5205bc5R1)が、これは本来であれば不要なのでご留意を。

## 参考記事

* https://oleb.net/2020/swift-test-discovery/
* https://oleb.net/blog/2017/03/keeping-xctest-in-sync/

## 終わりに

Swift Package Manager は Swift と共に進化してきたこともあり、古いバージョンについて言及された記事も多数残っているため、参考情報として利用する際には注意が必要なように思う。

かくいう私も Swift 4.1 の時点ですでに追加されていた自動生成用の`--generate-linuxmain`の存在を知らず、 **30個以上もあるテストの定義を手書きしてしまうところであった** （というより、この記事に書いたLinux環境でのテストについて知ったのも最近のことだ）。

今であれば公式のパーサである [swift-syntax](https://github.com/apple/swift-syntax) もあるので、自動生成用のツールを書いてしまうかと思い、ふとすでに存在しないかとググってみたら参考記事に貼ったような情報を得られたという次第であった。

日本語圏においては Swift Package Manager の情報は英語圏に比べてさらに少なく、私自身も『LinuxMain.swiftとは』でググっても殆ど出てこない状況に遭遇したことを思い出し、筆を執ることにした。