5.1さらうどん

@giginetの技術ブログ。ゲーム開発、iOS開発、その他いろいろ

XCTestCaseをswift-testingに自動変換するswift-testing-revolutionaryを作った

swift-testing-revolutionary

ほぼタイトル通りですが、先日XCTestで書かれたテストケースを、swift-testingのテストケースに自動変換する、swift-testing-revolutionaryというユーティリティを公開しました。Xだと流れてしまうため、せっかくなのでブログ記事でも告知しておきます。

自動変換されて便利

使い方

基本的にはREADMEをご覧ください。このツールは、Xcodeコマンドプラグイン、Packageプラグイン、Command Line Interfaceの3つの使い方をサポートしています。

超大雑把に以下のようなイメージを持っていただくと良いと思います。

導入方法 適応対象 導入の手間 カスタマイズ性
Xcodeコマンドプラグイン Xcodeプロジェクト
Packageプラグイン Swift Package
Command Line Interface 両方

アプリのテストケースなど、Xcodeプロジェクト内のテストケースを移行するにはXcodeコマンドプラグインとして利用するのがオススメ。GUIのみで簡単に導入、実行できます。

Xcodeコマンドプラグインの様子

ただし、Xcodeのコマンドプラグインの仕様が微妙すぎて、UnitTestターゲット全体しか変換対象に指定できませんGUIからファイル1つずつを変換対象に指定できれば便利だったのですが、Xcode側のサポートがされていないのでこれが限界です。

テストケース1つずつを変換したい場合は、CLIツールとして導入し、自分でファイル名を指定してもらうのが良いです。

$ swift-testing-revolutionary path/to/Tests/ViewModelTests.swift path/to/Tests/RepositoryTests.swift

また、プロダクトの性質上、一度変換してしまうともう使わないツールとなってしまうので、アプリケーション・パッケージの依存関係に含めるよりも、自分でバイナリを管理した方が利用しやすいかもしれません。Swift Packageをバイナリとして管理する方法は標準の良い方法がなく、現状Mintなどを使って入れるのが楽に思います。

swift-testingを導入していく

swift-testingの一番の利点は、既存のテストケースに変更を加えずに、今後追加するテストケースのみを簡単に新しいものに移行できる点です。また、簡単なブリッジングを書けば、Xcode 15.3 + Swift 5.10でも利用することができます。というわけでXcode 16を待たずとも今日から使えます!*1

今後は、新規テストはswift-testingで記入していくようにして、既存のテストコードの移行にこのツールを活用してもらえれば良いと思います。

ただ、このツールは既存のテストケースを機械的にswift-testingのコードに変換するだけなので、読みやすく、メンテしやすいテストに変更するにはさらなる書き換えは不可欠です。 (例えばParameterized Testsを利用するなど)

書き換えを行う場合も、このツールで一度機械的に変換してから、新機能の対応を行っていくと楽なため、ぜひ第一歩として役立てていただければと思います。

作ってみて

実装の大部分はswift-syntaxのSyntaxRewriterを使って、良い感じにシンタックスを書き換えただけ。

SyntaxRewriterの利用方法は、id:KishikawaKatsumi 先生のMastering SwiftSyntaxのセッションが大変役に立った。

expectationの置き換えなど、似たような変換が繰り返される処理は良い感じに抽象化して書けたと思う。

細かいところでは、Swift Syntaxにおけるtrivia*2の扱いに苦戦した。

例えば、テストメソッドにMacroやグローバルアクターのような予めattributeがついていて、さらに改行やスペースが含まれているケースを綺麗に置換するのがかなり大変だった。

@MainActor
func testCanFetchProducts() async await
@MainActor
@Test
func canFetchProducts() async await

その他、さまざまなシンタックスを考慮する必要があった。Swiftは言語機能が多すぎる。いろいろ抜け漏れがあるかも知れないので、気付いたらissueを立ててもらいたい。全体的に大分テストケースが足りていない。

このプロダクトは今年のGWに勢いで作り始め、概ね完成していたのだけど、当時はWWDC24前で、Xcode 16に同等機能が含まれていたらすぐにオワコンになるじゃん、とドキュメントまで書いて1ヶ月ほどお蔵入りにしていた。WWDC24のあと、Xcode 16に同等機能がないことを確認して、次の日にはすぐにpublicにすることができた。

ちょうどWWDC24期間中は現地にいたこともあり、パーティーで出会った開発者に挨拶代わりに紹介するなど、会話のきっかけになって良かった。

しつこいぐらいに宣伝しまくった結果、誰かがSwift Forumで言及してくれたりしていて、とてもありがたい 😊

こういうものを作ったら、あらゆるところで発信しまくるのが重要だなと再実感した。このエントリもその一環。

どうぞご利用ください

どうぞご利用ください。利用報告などお寄せいただけたら嬉しいです。

*1:ただしXcode 15で実行するとIDEサポートがないため、とても見づらい

*2:改行やスペースなど、意味解析には影響を及ぼさないtoken

WWDC24に参加してきた

WWDC24に行った

今年は運良く当選したので、WWDC24に参加してきた。17, 19以来5年ぶり3回目の出場(23も会場には入れなかったが、現地にはいた)

著者近影

23年は仕事で現地まで行ったが、会場には入れず、参加者の様子を見聞きしただけで厳密な比較はできないけど、今年は昨年に比べて参加者がかなり増え、日本からの渡航者も増えていた印象だった。

COVID以降初の参加だったが、今年は大分COVID前のコンベンションセンター時代の盛り上がりが戻っていたように感じた。

0日目:registration

初日はInfinite Loop(旧Apple本社)にてregistration。バッヂを受け取って終わりかと思ったら、そのままビールが提供されたり、参加者と懇親できるようになっていた。COVID前のWWDCを想起させるノリだった。

ビールを片手にご機嫌の著者

公式のコンテンツはないが、知り合いや初対面の方含め、いろんな人と雑談する。

夜はパーティーで紹介してもらった韓国から来たDeveloper集団の宿にお邪魔して交流したりとわいわいしていた。

1日目:Keynote + Special Event

現地時間の月曜はKeynote、朝6時半前には現着してApple Park前で待機。かなり早く行ったけど、8時前ぐらいまで入場はできずに、列の形成もファジーな感じだった。ここまで張りきらなくても結果的には前の方に座れたので、7時半ぐらいに行くので十分かもしれない。 (とはいえコンベンションセンター時代には徹夜組もいたので、本当に毎年いつから並べば良いか読めない)

8時過ぎたらPark内に入場できて、朝食などが振る舞われた。 期間中は事あるごとに食事が提供されたが、どれも美味しくて驚く。以前のWWDCは雑なランチボックスが5日間提供され続けていて、かなり食傷していたけど、とても食事の質が上がっていた。しかも参加費が無料だからビックリ。

Keynote前には朝食も用意されてメッチャ美味かった

ようやっとKeynote。発表内容は他の記事に譲るけど、やはり毎年とてもプラットフォームの進化にワクワクする。茶番のクオリティも年々上がっていて楽しませてくれる。

Tim CookとFederighiが出てきてテンション上がる

その後、後述のLabに行ったり、Xcode 16を試したりしていた。

同僚の@___freddi___ と仕事中

今回、Swiftのメジャーバージョンがあることは自明であり、開発中のアプリのビルドがちゃんと通るかを心配していたが、事前の予想に反してSwift 5モードの互換がかなりしっかりしており、少しの変更で開発環境を作ることができた。終わった後のビールが美味い。

電源、ネットワークのブースが屋外で、炎天下の中作業していたけど、熱中症のリスクがあるので気をつけた方が良い。

In Person Lab

今年の特徴として、わずか3時間ほどではあったが、COVID前のIn Person Labが部分的に復活していたのは良かった。ビルド+Xcode Labがあったので、予め用意していたld(linker)周りの質問を持って行った。

In-Person Labでビルドについてディスカッションした

In Person Lab、やはりOnline Labと比べて体験が段違い。対面であることのコミュニケーションのしやすさもそうだが、その場で操作を変わってくれたり、ホワイトボードでディスカッションできたりと、Online Labで同等の体験は難しい。

また、Online Labではミーティングの参加者しか対応してくれないが、In Person Labでは、わからないところは周りのエンジニアを呼んできてくれて教えてくれるのも大きい。

最終的に屈強なビルドエンジニアが4人も集まってきて見てくれた

ここでわいわいすることこそがWWDCの真骨頂だと思うので、来年はぜひ日数を伸ばしてほしい。

2日目:Special Session

2日目火曜日はわずかに1時間ほどのセッションがあるのみ。昨年は「Apple Vision Proのアプリ作ってね〜」という基調講演的な内容だったと聞いていて、今年はApple Intelligence(以下AI)関連かな、と思っていたが、案の定そんな感じだった。

セッション自体は撮影や録画禁止だったので、中身の記録はないが、会場は小規模なホールで、登壇者が生で話してくれるのは体験が良かった。

Special Session、小規模なホールで開催された

撮影禁止とは言え、コンフィデンシャルな話がされていたわけではなく、AIに対応していくための既存のセッションの内容の再紹介といった感じで、気になる方はIntent周りのセッションを見ると、大体内容が把握できると思う。終盤はApple Vision ProでのARや、AIを使った体験設計の話で、セッションにはない内容だった。

技術的な話は、要約すると「App Intent作ってほしい〜」と言った話だった。App Intent自体はSiriやSpotlight、Widgetsなどを連携するために以前からiOSに存在するが、AIの登場により「通貨」のような側面が強くなってきたなと感じた。今後数年のAIの普及や、将来的なApp Intentの用途の拡大を見越すと、徐々にIntentを充実させる投資を行っていかないと、将来的なアプリの進化が行き詰まってしまうかもしれない。

他にもAssistant Schemaの解説があった。

Integrating your app with Siri and Apple Intelligence | Apple Developer Documentation

Assistant Schemaはアプリ上の手続きをSiriやAIが解釈可能な形で提供するためのブリッジの役割を果たす仕組みだ。 Intelligenceというと、なにか抽象的なインプットで、魔法のように全て良い感じにしてくれるものを想像してしまうが、実際は開発者が泥臭く手続きを記述するものなんだなという印象だった。開発者が泣くことでエンドユーザーが魔法のような体験を得ることができている。

開催期間中

開催期間中は、日中はAirbnbで仕事をしつつも、今年は日本人も多く、日々様々なミートアップが開かれていたので、毎日パーティーピープルをしていた。

到着日に開かれた日本人nomikaiや、世界中の参加者とのMeetup、Swiftコミュニティによる10周年イベントと暇をする時間がなかった。

Swift 10周年おめでとう〜

昨年は週末までいたが、後半時間を持て余したので、今年は木曜昼には現地を離れた。そのため、Online Labやセッション動画の鑑賞、新技術の検証がほとんどできなかったのが心残り。 例年に比べて全体的に薄味な印象だったのでなんとかなった。

今年の総評

正直、昨年の新プラットフォーム(Apple Vision Pro)の発表が大きすぎて、今年はそれに比べると大きな発表はなかったと思う。

しかし、例年の傾向から大きな発表は数年に一度なので「今年は繋ぎの年っぽいなあ」という察しは事前にあって、良くも悪くも予想の範疇ではあった。

Swift AssistやApple Intelligenceも現段階では触れるところが限られているし、OS機能の強化も順当なものであった。 開発環境もSwift 6が出ることや、言語の新機能は、ほとんどオープンソースコミュニティで発表や議論が尽くされていたし、Xcodeの変化も目新しいものはなかった。

WWDCの前から、生成AIの普及とデバイスの高性能化に伴って、クラウドからローカルAIへの再評価が始まりつつあるな、と感じていたが、Apple Intelligenceの発表によりそのトレンドが強固なものになったのを感じた。 この領域は、ネイティブエンジニアに利があると思うし、ネイティブに投資していくのはまだまだ将来性が高いのではないかと考えている。

Apple Intelligenceは現時点では、A17 Proを始めとする高性能端末でしか動作せず、アーリーアダプター向けの機能ではあるけど、今後2〜3年ほどで普及機にも浸透し、生活に欠かせないものになっていく未来が見える。 その時に備えて今からどんどん投資していきたい。

Apple Vision Pro

今回、ついに日本発売が決まったので、帰りの飛行機で速攻注文した。(¥784,200 💸)

レンズの購入などが煩わしく、現地では敢えて購入しなかったので楽しみに発売を待とうと思う。

高い買い物なので、せっかくだから何か開発しようかなあ。

Swift MacroをSwift PackageなしでXcodeで扱う

Swift MacroってXcodeから使えないの?

Swift 5.9からSwift Macroが実用段階になったが、WWDCの動画でも、公式ドキュメントでもSwift Packageから作成することが前提となっている。

targets: [
    // Macro implementation that performs the source transformations.
    .macro(
        name: "MyProjectMacros",
        dependencies: [
            .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
            .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
        ]
    ),


    // Library that exposes a macro as part of its API.
    .target(name: "MyProject", dependencies: ["MyProjectMacros"]),
]

Xcodeで作成したtargetに組み込むときもパッケージの依存を組み込み、マクロフレームワークをimportするだけで利用可能になるようだ。

しかしXcodeGUIではMacroを含むモジュールに依存関係を設定する方法はない。通常の依存関係と異なり、Macroはビルドdestinationが特殊なので、これを内部的にどうやって設定しているのか気になった。 (例えばiOSアプリにMacroを組み込むとき、アプリケーション自体はiOS向けにビルドするが、Macroは常にmacOSで実行可能な形でビルドされなくてはならない)

XcodeやxcodebuildのレイヤーではMacroを依存として指定する方法がないのだが、swiftcのレイヤーでは何らかの方法でmacroへの依存関係を指定しているはずだ。

-load-plugin-executable

調べてみたところ、どうやら -load-plugin-executable というオプションがswiftcに生えていた。

この辺の記事にほとんど書いていた。

$ swiftc --help
  -load-plugin-executable <path>#<module-names>
                          Path to an executable compiler plugins and providing module names such as macros

この -load-plugin-executable でmacroターゲットのバイナリを指定すると良いらしい。Xcodeでは OTHER_SWIFT_FLAGS にこれを指定する。

実際に、適当なMacroを含むパッケージをXcodeプロジェクトに追加し、利用してみたときのログを見ていると、同じフラグを渡していることが確認できる。

Special Thanks : mtj0928/userinfo-representable

Swift MacroをSwift PackageなしでXcodeだけで作る

そして、調べてみると、SwiftPMにおける.macro ターゲットは、実は単なるmacOS向けの executableTarget とほぼ同等なものであることがわかった。

-load-plugin-executable で要求されるバイナリは、実は単に CompilerPlugin に適合した型をエントリーポイントとしたexecutableを要求しているだけのようだ。 あとでswiftc側でどういうバイナリを想定しているかの内部実装も読んでみたい。

ここまでわかると、Swift Packageを使わずとも、Xcodeだけでマクロを捏造できるのでは?と思って試してみた。

Macroターゲット側 (MyMacro)

  1. XcodemacOSのCommand Line Toolターゲットを作る
  2. swift-syntaxをSwift Packageとして追加して依存関係にする
  3. 普通にmacroを書く

ここで実装するマクロはなんでもよいので、公式ドキュメントのサンプルにある fourCharacterCode の実装を使うことにした。

重要なのは、executableのエントリーポイントとして、CompilerPluginを指定することだ。

import SwiftCompilerPlugin

@main
struct MyMacro: CompilerPlugin {
    var providingMacros: [Macro.Type] = [FourCharacterCode.self]
}

アプリターゲット側 (MyApp)

  1. Build Phaseのdependenciesに MyMacro を追加する
    • 単にビルド前にexecutableを作っておきたいだけ
  2. OTHER_SWIFT_FLAGS-load-plugin-executable $BUILT_PRODUCTS_DIR/../$CONFIGURATION/MyMacro#MyMacroを指定
    • これは BUILT_PRODUCTS_DIR に入っている MyMacro の成果物を指定している
    • 単に $BUILT_PRODUCTS_DIR だとアプリ側のdestinationになってしまうので ../$CONFIGURATIONmacOS側のdestinationにする
    • この指定は特殊で、 swiftc --helpによると、<executable_file_path>#<target_name> らしい
  3. シンボルの解決のために#externalMacroを仮宣言する
    • これをすることで、依存解決時はMacroが存在する体で扱い、実際にビルドするときに2で指定したバイナリを実行する

@freestanding(expression)
macro fourCharacterCode<T>(for value: T) -> UInt32? = #externalMacro(module: "MyMacro", type: "FourCharacterCode")

こうすることで、自作のMacroをXcodeターゲットだけで実装することができた 🎉

Swift Macroのビルドキャッシュ

では、このような複雑な構造を取って一体どんなメリットがあるかというと、ビルドキャッシュの利用という面でXcodeターゲットで管理していた方が扱いやすい面があるのではないかなと思う。

現状、Swift Macroはswift-syntaxへの依存が必須で、どの方法で作ってもswift-syntaxを依存として持つ必要がある。swift-syntaxはデカいので、単純なマクロを1つ作っただけで、ビルド時間が激増してしまうという問題があった。

僕の開発しているScipioなどで、swift-syntaxをprebuiltなXCFrameworkとして維持しておくことができるので、Xcodeで作成したMacroターゲットがprebuilt版のswift-syntaxに依存するようにしておけば、ビルド時間を大きく改善できそう。

また、Xcodeを使わないにしても、swift buildで作成したexecutableをキャッシュしておいて、-load-plugin-executableで利用することで、毎回swift-syntaxやmacroをビルドする必要がなくなる(アーキテクチャの問題はあるが)

ScipioはSwift PackageからXCFrameworkを生成するためのビルドツールだが、現在、macroを使ったターゲットからフレームワークを作ることができないという問題がある。理論的には今回紹介した方法で対応できそう。(先にmacroターゲットをexecutableとしてビルドして、framework側からビルドフラグを立てる)

いかがでしたか

業務上で調査して面白かったのでちょっと記事を書いてみた。ここ数年全く技術記事を書いていないという危機感があり今年はこれぐらいの技術記事を定期的に書いていきたいと思っていたのでちょうど良い。 気合入れると続かないので30分以内でガッと書けるやつを目指していきたい。

純粋な技術記事としては8年ぶりの投稿かも・・・・・・