Arm-poweredのLarger RunnerにAndroid SDKがインストールされていない

GitHubが管理している通常のUbuntu 24.04のイメージでは、Android SDKがインストールされています。

https://github.com/actions/runner-images/blob/main/images/ubuntu/Ubuntu2404-Readme.md

しかし、Arm-poweredのRunnerは話が違うらしく、Android SDKを始めとするいくつかのソフトウェアが入っていない、かつ管理者もGitHubではないようです。

https://github.com/actions/partner-runner-images/blob/main/images/arm-ubuntu-24-image.md#not-installed-software

github.com

Issueが立っているようなので、今後に期待です。

github.com

MockKExceptionが発生する場合、必ず同例外でテストが落ちると思っていた話

概要

モックライブラリの一つであるMockKを使用していたところ、relaxed周りで当初の認識と違う挙動があったため備忘録として書いています。

発生した問題

当初の理解

class Class1 {
  fun methodUnit(): Unit {}
  fun methodNotUnit(): Int { return 1 }
}

// 通常のMock
val mock1: Class1 = mockk()
mock1.methodUnit() // exception
mock1.methodNotUnit() // exception

// relaxUnitFun = trueのMock
val mock2: Class1 = mockk(relaxedUnitFun = true)
mock1.methodUnit() // exception
mock1.methodNotUnit() // exception

// relaxed = trueのMock
val mock3: Class1 =  mockk(relaxed = true)
mock1.methodUnit() 
mock1.methodNotUnit() 

上記のように、relaxをtrueにしていない箇所では即MockKExceptionが返却され、必ずテストが失敗すると思っていました。

失敗せずにテストが続行されるケース

class HogeViewModel(): ViewModel {
  val exceptionHandler = CoroutineExceptionHandler { _,_ -> }
  fun method(hoge: Hoge) {
     viewModelScope(exceptionHandler).launch { 
       hoge.call()
       // do something
     }
  }
}

// test file
fun test() = runTest {
  val hoge: Hoge = mockk()
  viewModel.method(hoge)

  // 他の処理やアサーション
}

上記のテストコードにおいて、Hogeクラスはrelaxedなモックオブジェクトではないため、callメソッドを呼んだタイミングでMockKExceptionによってテストが失敗しそうです。 しかし、同コードを実行すると、他の処理やアサーションに進んでしまいます。

そのため、当初はモックオブジェクト以外に原因があると思い込んでしまいました。

原因

原因はViewModelScopeとCoroutineExceptionHandlerです。 SuperVisorJobを持つViewModelScopeとCoroutineExceptionHandlerがあることによって、scope内で発生した例外は親Coroutineに伝播しません。 つまり、runTestで生成されたCoroutineには伝播せず、MockKExceptionが発生していることを認識しづらくなっています。

そもそもMockKはどうやって動いているのかの参考

zenn.dev

2024年の振り返り

日々のお仕事

 仕事は変わらずAndroidアプリ開発をやっていました。また、開発以外にもチームの課題解決的なことを頑張っていました。チームで動くことが増えてきて、全体としてどうやってコミュニケーションを進めるべきか、どうすれば成果を出せるのか日々悩んでいます。特に振り返りが難しいですね、、、。時間内に観点を整理する&チームとしての次の行動に繋げていくということの奥深さを痛感しています。

それ以外

会社の技術イベント

 ExceptionHandlerのLTをしました。もうすぐ一年経つことに驚きを隠せません。

CoroutineExceptionHandlerと仲良くなる / Grasping the basics of CoroutineExceptionHandler - Speaker Deck

Kotlin Fest 2024

 一般参加という形で参加する初めてのカンファレンスでした。色々なセッションを聴けて楽しかったです。特にCompiler Pluginの話が面白かったですね。

DroidKaigiのスタッフ

 英語を始めコミュニケーション関連の施策を色々と担当しました。無事に終わって本当に良かったです。

技術書典

 KSPの技術同人誌を書きました。執筆当時は日本語で書かれたKSP2の情報がほぼ存在していなかったため、KotlinのSlackや英語記事、各種ライブラリのPRを見ながら執筆を進めました。日本語の情報が少なくても何とかなるのだと、少しだけ自信が湧きました。

techbookfest.org

また、こちらの同人誌にも少しだけ参加しました。わいわい。

techbooster.booth.pm

勉強

 放送大学での勉強も変わらず続けています。2024年度は以下の科目を取りました。

  • コンピュータの動作と管理(’17)
  • Webのしくみと応用(’19)
  • コンピュータとソフトウェア(’18)
  • 続・C言語基礎演習(’23)
  • 数理最適化法演習(’20)
  • 問題解決の数理(’21)
  • 情報社会のユニバーサルデザイン(’19)
  • 博物館情報・メディア論(’18)
  • 映像コンテンツの制作技術(’20)

2025年度の卒業を目指して引き続き頑張ります。

旅行

 今年も色々な場所に旅行に行きました。

特にイギリスはずっと行きたいと思っていた場所だったので、街並みを見ただけでテンションが上がりました。ソンドハイムシアターでのレ・ミゼラブルは圧巻でした。

LiVE

 今年もLiSAのLiVEを観に行きました。

とても盛りだくさんな一年でした。来年のLiVEも楽しみです。

来年は?

 大学院に行きたいと考えているため、その準備として放送大学での勉強と英語学習に力を入れる予定です。

Google Docsで数式を書く時

数式の入れ方

画面上のタブから挿入→計算式で数式を入力できる。

特殊記号の入れ方

同じく画面上のタブから挿入→特殊文字で数式を入力できる。 探すのが難しい場合は、自分で書いて検索できる。

添字の入れ方

  • 通常のテキストであれば、表示形式→テキスト→下付き文字を選択する
  • 数式であれば、Spaceを押した後に数字を入力する

PRへのレビューをトリガーにしてActionを動かすとき

詰まったポイントのメモ

diffをPythonに渡す

メインの処理をPythonで書いていたため、値をPython側で扱えるようにする必要がありました。 いくつか方法はあると思いますが、今回はenvに設定しました。

しかし、コンテキストからdiff_hunkを取り出し、envに設定したところ、特殊文字に関するエラーが出てしまいました。

  • +やその他の記号が特殊な意味を持つため、渡せない
  • {hoge}の後の単語がコマンドとして認識される

前者はエスケープすれば良いですが、後者の楽な解決方法が思い浮かびませんでした。 そのため、 commentプロパティ全体をJSON形式でenvに設定する方法に変えています。

env:
  COMMENT: ${{ toJson(github.event.comment) }}

一行選択だと開始行が空になる

通常だとstart_lineとlilneのように開始と終了で対となる値が設定されていますが、一行選択の場合は終了の行にしか値が入りません。 レビュー時に選択した行のみを取り出したい時は、上記の事項を考慮する必要があります。

参考

Webhook のイベントとペイロード - GitHub Docs

2021年の自作PCメモ

概要

組立時の情報を残していなかったので、メモ

2021/10/01購入&組立

  • ケース P10 FLUX 9073円
  • 電源 Antec NeoEco750GOLD 9091円
  • GPU GeForce RTX3060 Ti 67701円
  • CPUクーラー SCMG5100 無限五 5037円
  • RAM XPG GAMMIX D20 DDR4 Memory 16GBx2 15800円
  • マザボ TUF GAMING B560-PLUS WIFI 16245円
  • CPU Intel i7-11700 35346円
  • DVD GSH24NSD5BLBLH 日立LGデータストレージ 1980円
  • HDD ST2000DM005 SEAGATE BARRACUDA 2TB 5346円
  • SSD MZV8V1TOBIT SAMSUMG SSD 980 MVMe M.2 11800円

メモ

ちょうどグラボが高騰してた。 WIFIが機能せず、USBに挿すタイプで代用した。

KSP2で遊ぶ

KSP2における変更

android-developers.googleblog.com

上のAndroid Developersのブログ記事にて様々な変更点が紹介されています。その中でも特にスタンドアローンに実行できる点が気になったため試してみました。

エントリーポイントのあるライブラリとして実装されているため、デバッグやテストが容易になっているらしいです。

テストを書いてみる

試しにコード生成の単体テストを書いてみます。

依存関係を追加する

まずは依存関係を追加します。

implementation("com.google.devtools.ksp:symbol-processing-aa-embeddable:<version>")

// もし足りない場合は以下の依存関係も追加する
implementation("com.google.devtools.ksp:symbol-processing-api:<version>")
implementation("com.google.devtools.ksp:symbol-processing-common-deps:<version>")

実行したいモジュールに任意のバージョンのsymbol-processing-aa-embeddableを追加します(執筆時点の最新は2.0.0-RC2-1.0.20)。 筆者の手元の環境では、KSPLoggerとKSPJvmConfigの情報が足りなかったため、symbol-processing-apiとsymbol-processing-common-depsも追加しています。

テストを書く

Processorを書いている前提で簡単なテストコードを書いてみます。

以下が最終的なテストコードです。

class SampleTest {
    // 生成ファイル用のDirの生成
    @JvmField
    @Rule
    val kotlinOutputDirFolder = TemporaryFolder()
    // その他のファイル用のDir生成
    @JvmField
    @Rule
    val dummyFolder = TemporaryFolder()

    @Test
    fun sample_test() {
        // 生成ファイル用のFileインスタンス生成
        val annotationFolder = kotlinOutputDirFolder.newFolder(
            "com", "sample", "annotation"
        )
        val generatedFile = File(annotationFolder, "GeneratedFile.kt")

        // Processorが処理するファイルの準備
        val projectFile = File(dummyFolder.root, "Project.kt")
        projectFile.writeText(
            """
            package com.sample.ksp

            @TargetAnnotation
            Interface Hoge

            annotation class TargetAnnotation()
            """.trimIndent(),
        )

        // Configの生成
        val kspConfig = KSPJvmConfig.Builder().apply {
            kotlinOutputDir = kotlinOutputDirFolder.root
            javaOutputDir = dummyFolder.newFolder("java")
            outputBaseDir = dummyFolder.newFolder("base")
            resourceOutputDir = dummyFolder.newFolder("resource")
            cachesDir = dummyFolder.newFolder("cache")
            classOutputDir =  dummyFolder.newFolder("class")
            jvmTarget = JvmTarget.DEFAULT.description
            moduleName = "<module_name>"
            sourceRoots = listOf(projectFile)
            javaSourceRoots = listOf()
            commonSourceRoots = listOf()
            projectBaseDir = dummyFolder.root
            languageVersion = "<version>"
            apiVersion = "<version>"
        }.build()
        val exitCode = KotlinSymbolProcessing(
            kspConfig, 
            listOf(HogeProcessorProvider()),
            TestKSPLogger()
        ).execute()

        assertTrue(
            exitCode == KotlinSymbolProcessing.ExitCode.OK 
                && generatedFile.exists()
        )
    }
}

class TestKSPLogger: KSPLogger {
    override fun error(message: String, symbol: KSNode?) { // operation }
    override fun exception(e: Throwable) { // operation }
    override fun info(message: String, symbol: KSNode?) { // operation }
    override fun logging(message: String, symbol: KSNode?) { // operation }
    override fun warn(message: String, symbol: KSNode?) { // operation }
}

順番に見ていきます。

フォルダを用意する

Configを作成する際、ファイルの読み込み先と出力先となるディレクトリを渡す必要があるようです。今回はTemporaryFolderを使用してフォルダを生成しました。

@JvmField
@Rule
val dummyFolder = TemporaryFolder()

テストメソッド毎にフォルダを再作成できるように、Ruleアノテーションを付けておきます。 また、Kotlinで使用する場合はJvmFieldアノテーションも必要みたいです。

読み込むファイルを用意する

SymbolProcessorの処理対象となるファイルを用意します。

val projectFile = File(dummyFolder.root, "Project.kt")
projectFile.writeText(
    """
    package com.example.ksp

    @TargetAnnotation
    Interface Hoge

    annotation class TargetAnnotation()
    """.trimIndent(),
)

Configを用意する

諸々の設定情報を渡すためにKSPJvmConfigを用意する必要があります。

val kspConfig = KSPJvmConfig.Builder().apply {
    kotlinOutputDir = kotlinOutputDirFolder.root
    javaOutputDir = dummyFolder.newFolder("java")
    outputBaseDir = dummyFolder.newFolder("base")
    resourceOutputDir = dummyFolder.newFolder("resource")
    cachesDir = dummyFolder.newFolder("cache")
    classOutputDir =  dummyFolder.newFolder("class")
    jvmTarget = JvmTarget.DEFAULT.description
    moduleName = "<module_name>"
    sourceRoots = listOf(projectFile)
    javaSourceRoots = listOf()
    commonSourceRoots = listOf()
    projectBaseDir = dummyFolder.root
    languageVersion = "<version>"
    apiVersion = "<version>"
}.build()

先ほど作成した出力先のフォルダをkotlinOutputDirに、読み込むファイルをsourceRootsに設定します。残りはよしなに埋めておきます。

Loggerを用意する

KotlinSymbolProcessingにKSPLoggerを実装したクラスを渡す必要があります。

class TestKSPLogger: KSPLogger {
    override fun error(message: String, symbol: KSNode?) { // operation }
    override fun exception(e: Throwable) { // operation }
    override fun info(message: String, symbol: KSNode?) { // operation }
    override fun logging(message: String, symbol: KSNode?) { // operation }
    override fun warn(message: String, symbol: KSNode?) { // operation }
}

KSPの処理を実行する

作成しておいたConfigとSymbolProcessorProvider、Loggerを渡して、executeメソッドを呼びます。今回は、返り値のExitCodeと生成ファイルを用いてassertしておきます。

val exitCode = KotlinSymbolProcessing(
    kspConfig, 
    listOf(HogeProcessorProvider()),
    TestKSPLogger()
).execute()
assertTrue(
    exitCode == KotlinSymbolProcessing.ExitCode.OK 
        && generatedFile.exists()
)

参考リンク

Linen Community

Update to K2 by ZacSweers · Pull Request #196 · ZacSweers/kotlin-compile-testing · GitHub