KMP Test

This documentation was generated with the assistance of AI. Please report any inaccuracies.

Run a Kotlin Multiplatform (Android + iOS) project’s tests on each target it actually has — the shared module’s Android host (JVM) tests, jvmTest, the Android app’s testDebugUnitTest, the Kotlin/Native iOS simulator tests and the SwiftUI host app’s XCTest bundle — scoped to whatever the caller asked for, and report a compact per-target summary parsed from JUnit XML (Gradle) and .xcresult (Xcode). This is a report-only skill: it never fixes failing tests or modifies source/test code.

Purpose

iru-kmp-test discovers the KMP modules, Android app modules and iosApp/ host project at runtime, lists the real test task names from ./gradlew <module>:tasks --all (instead of assuming one plugin’s naming), and runs the matching stages. Android/JVM stages run on any host; iOS stages run only on macOS with a full Xcode and an iOS Simulator runtime and are otherwise reported "unverified", never "passed". Every stage that boots a simulator is serialized repository-wide behind the shared atomic-mkdir .integration-test.lock directory (owner file, stale after 60 minutes, released on every exit path). It also detects the "selector matched nothing" cases: Gradle fails the JVM task with "No tests found for given includes", whereas a Kotlin/Native simulator task exits 0 and writes no XML, and an unmatched -only-testing: yields zero tests. It is the KMP equivalent of Android Test and Swift Test.

A ui selector (/iru-kmp-test ui [<scope>], or a ui: yes line) runs the UI tests per the catalog’s UI contract (see Building user interfaces): Compose Multiplatform runComposeUiTest tests on the Android host (Robolectric) and the iOS simulator, ui: native Robolectric Compose tests in androidApp, iosApp ImageRenderer/XCUITest captures, and device flow tests only when an emulator is attached. Step 5a resolves UI_SCREENSHOTS_DIR (default <repo root>/.ui-screenshots, forwarded to the JVM tests and to the simulator through SIMCTL_CHILD_/TEST_RUNNER_ variables), clears it only on a full (unscoped) UI run, forces every Gradle test task to re-run with --rerun (captures are side effects that Gradle’s up-to-date checks and build cache do not track), collects .xcresult attachments and device outputs into the contract’s <kind>/<Id>/<state>--<direction>--<theme>[--<variant>…​] layout, validates the names with an embedded parser, and reports the .ui-quality.yml accessibility level with violation counts. KMP captures carry an android/ios platform variant so the same <Id> does not collide.

Plugin

Shipped by the iru-kmp plugin of the ai-catalog marketplace (install with /plugin install iru-kmp@ai-catalog; see Plugins).

Invocation

/iru-kmp-test
/iru-kmp-test <scope>
/iru-kmp-test <scope>
target: android|ios|all
module: <gradle path>
destination: <xcodebuild destination>
lock: held

Inputs

Argument Required Description Default

<scope>

No

A test class (GreeterTest), class+method (GreeterTest#greets), wildcard/package pattern, a test file path (its source set picks the targets: commonTest runs every target, androidHostTest/androidUnitTest/jvmTest run Android/JVM only, iosTest/appleTest/nativeTest run iOS only, iosApp/…​ runs the XCTest stage), a module (:shared, androidApp) or a source-set name.

None — every test task of every in-scope target.

target

No

android runs the JVM stages only; ios runs the iOS stages only (Kotlin/Native simulator tests and the iosApp XCTest bundle); all runs both.

all

module

No

Restrict the run to one Gradle module (:shared, :androidApp, :composeApp).

All modules

destination

No

An xcodebuild destination overriding the simulator picked for the XCTest stage.

First available iPhone simulator

ui

No

yes, or a leading ui word in <scope> (for example ui GreetingScreenTest): a UI run (Step 5a). With no further scope it is a full UI run that clears the capture directory first.

Not set — ordinary tests only.

lock

No

held when the caller already holds .integration-test.lock; the skill then neither acquires nor releases it.

Not held — the skill acquires it for simulator stages.

Stage-to-task mapping (Step 2):

Stage Task (first that exists)

Android host tests of a KMP module

testAndroidHostTest (or any test<Name>HostTest), else testDebugUnitTest

JVM target

jvmTest, only when a jvm() target exists

Android app module

testDebugUnitTest

Kotlin/Native iOS

iosSimulatorArm64Test on Apple silicon, iosX64Test on Intel

Outputs

  • A plain-text summary in the conversation — never a written file.

  • One line per target (Android host, JVM, Android app, iOS simulator, iosApp XCTest): passed (counts), failed (counts, each failing Class#test, assertion message, first stack line and the XML/.xcresult path), no matching tests for <pattern>, timed out, not applicable, or unverified (reason).

  • An explicit callout when a test fails on one target only, which usually means an actual implementation differs.

  • The lock is mentioned only when it was anything other than an immediate acquire.

  • For a UI run: the capture directory and whether it was cleared (full run) or kept (scoped run); screenshots per kind and per platform (android/ios); malformed names; accessibility violations (total + rule ids with counts) and the .ui-quality.yml level applied (enforce/report/off/none); device flow tests run or unverified (reason).

  • No source or test code is ever modified; raw Gradle/xcodebuild output is never pasted.

Execution flow

flowchart TD A["Start /iru-kmp-test\n(optional scope + key: value lines)"] --> B["Step 0: resolve scope,\ntarget, module, destination, lock"] B --> C["Step 1: detect KMP modules,\nAndroid app, iosApp;\ncheck JDK, Android SDK, gradlew"] C --> D{"KMP module found?"} D -- no --> Z["Stop: not a KMP project\n(point to iru-android-test / iru-java-test)"] D -- yes --> E["Step 2: list test tasks per module\n(./gradlew tasks --all)"] E --> F["Step 3: run Android/JVM stages\n(--tests repeated per task, --continue)"] F --> G{"iOS stages in scope and\nmacOS + Xcode + runtime?"} G -- no --> H["Mark iOS stages unverified\n(state what is missing)"] G -- yes --> I["Step 4: acquire\n.integration-test.lock\n(wait / break stale / time out)"] I --> J["Run Kotlin/Native\niosSimulatorArm64Test"] J --> K["Step 5: xcodebuild test on iosApp\n(XcodeGen if needed)\nparse .xcresult"] K --> L["Release lock"] L --> L2["Step 5a (ui runs): collect .xcresult attachments\nand device outputs; validate capture names;\ncount accessibility violations"] H --> M["Step 6: parse JUnit XML\nper task directory"] L2 --> M M --> N{"Build failed before\nproducing results?"} N -- yes --> O["Report the compile or\nconfiguration error"] N -- no --> P["Step 7: report per-target\npass / fail / no matching tests /\ntimed out / unverified"] O --> P

Dependencies

Invokes

None — iru-kmp-test is a leaf skill; it only runs ./gradlew, xcodebuild, xcrun, xcodegen and small inline python3 parsers, and it does not call any other skill in this catalog.

Invoked by

  • KMP Code One Task Group — Step 3.3 (scoped Android/JVM tests, target: android), Step 3.5 (full Android/JVM suite) and Step 3.7 (iOS stage, target: ios, behind the shared lock) and Step 3.8 (UI stage, with the ui selector), always through the iru-gate-runner agent.

  • KMP Generate All Tests — Step 7, to run the full suite once and confirm the generated tests cause no regressions, and Step 5a, with the ui selector, to run the newly generated UI tests.

  • iru-gate-runner — both callers always run this skill from inside an iru-gate-runner sub-agent they spawn, not inline, so the compact summary is all that reaches their context.

Source

SKILL.md on GitHub — the file this page was generated from.